commit b64109fd5dbb62f1265316c5da08ecb7460349ab Author: Flatlogic Bot Date: Wed Mar 5 11:14:55 2025 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3eb8c11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +*/node_modules/ +app-shell/ +*/build/ diff --git a/backend/.prettierrc b/backend/.prettierrc new file mode 100644 index 0000000..bb087f2 --- /dev/null +++ b/backend/.prettierrc @@ -0,0 +1,11 @@ +{ + "singleQuote": true, + "tabWidth": 2, + "printWidth": 80, + "trailingComma": "all", + "quoteProps": "as-needed", + "jsxSingleQuote": true, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always" +} diff --git a/backend/.sequelizerc b/backend/.sequelizerc new file mode 100644 index 0000000..fe89188 --- /dev/null +++ b/backend/.sequelizerc @@ -0,0 +1,7 @@ +const path = require('path'); +module.exports = { + "config": path.resolve("src", "db", "db.config.js"), + "models-path": path.resolve("src", "db", "models"), + "seeders-path": path.resolve("src", "db", "seeders"), + "migrations-path": path.resolve("src", "db", "migrations") +}; \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..581cb98 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,23 @@ +FROM node:20.15.1-alpine + +RUN apk update && apk add bash +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN yarn install +# If you are building your code for production +# RUN npm ci --only=production + + +# Bundle app source +COPY . . + + +EXPOSE 8080 + +CMD [ "yarn", "start" ] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..a6209f8 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,67 @@ +#sev - template backend, + +#### Run App on local machine: + +##### Install local dependencies: + +- `yarn install` + +--- + +##### Adjust local db: + +###### 1. Install postgres: + +- MacOS: + + - `brew install postgres` + +- Ubuntu: + - `sudo apt update` + - `sudo apt install postgresql postgresql-contrib` + +###### 2. Create db and admin user: + +- Before run and test connection, make sure you have created a database as described in the above configuration. You can use the `psql` command to create a user and database. + + - `psql postgres --u postgres` + +- Next, type this command for creating a new user with password then give access for creating the database. + + - `postgres-# CREATE ROLE admin WITH LOGIN PASSWORD 'admin_pass';` + - `postgres-# ALTER ROLE admin CREATEDB;` + +- Quit `psql` then log in again using the new user that previously created. + + - `postgres-# \q` + - `psql postgres -U admin` + +- Type this command to creating a new database. + + - `postgres=> CREATE DATABASE db_sev;` + +- Then give that new user privileges to the new database then quit the `psql`. + - `postgres=> GRANT ALL PRIVILEGES ON DATABASE db_sev TO admin;` + - `postgres=> \q` + +--- + +#### Api Documentation (Swagger) + +http://localhost:8080/api-docs (local host) + +http://host_name/api-docs + +--- + +##### Setup database tables or update after schema change + +- `yarn db:migrate` + +##### Seed the initial data (admin accounts, relevant for the first setup): + +- `yarn db:seed` + +##### Start build: + +- `yarn start` diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..e899e2b --- /dev/null +++ b/backend/package.json @@ -0,0 +1,51 @@ +{ + "name": "sev", + "description": "sev - template backend", + "scripts": { + "start": "npm run db:migrate && npm run db:seed && nodemon ./src/index.js", + "db:migrate": "sequelize-cli db:migrate", + "db:seed": "sequelize-cli db:seed:all", + "db:drop": "sequelize-cli db:drop", + "db:create": "sequelize-cli db:create" + }, + "dependencies": { + "@google-cloud/storage": "^5.18.2", + "axios": "^1.6.7", + "bcrypt": "5.1.1", + "cors": "2.8.5", + "csv-parser": "^3.0.0", + "express": "4.18.2", + "formidable": "1.2.2", + "helmet": "4.1.1", + "json2csv": "^5.0.7", + "jsonwebtoken": "8.5.1", + "lodash": "4.17.21", + "moment": "2.30.1", + "multer": "^1.4.4", + "mysql2": "2.2.5", + "nodemailer": "6.9.9", + "passport": "^0.7.0", + "passport-google-oauth2": "^0.2.0", + "passport-jwt": "^4.0.1", + "passport-microsoft": "^0.1.0", + "pg": "8.4.1", + "pg-hstore": "2.3.4", + "sequelize": "6.35.2", + "sequelize-json-schema": "^2.1.1", + "sqlite": "4.0.15", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0", + "tedious": "^18.2.4" + }, + "engines": { + "node": ">=18" + }, + "private": true, + "devDependencies": { + "cross-env": "7.0.3", + "mocha": "8.1.3", + "node-mocks-http": "1.9.0", + "nodemon": "2.0.5", + "sequelize-cli": "6.6.2" + } +} diff --git a/backend/src/auth/auth.js b/backend/src/auth/auth.js new file mode 100644 index 0000000..0634d88 --- /dev/null +++ b/backend/src/auth/auth.js @@ -0,0 +1,79 @@ +const config = require('../config'); +const providers = config.providers; +const helpers = require('../helpers'); +const db = require('../db/models'); + +const passport = require('passport'); +const JWTstrategy = require('passport-jwt').Strategy; +const ExtractJWT = require('passport-jwt').ExtractJwt; +const GoogleStrategy = require('passport-google-oauth2').Strategy; +const MicrosoftStrategy = require('passport-microsoft').Strategy; +const UsersDBApi = require('../db/api/users'); + +passport.use( + new JWTstrategy( + { + passReqToCallback: true, + secretOrKey: config.secret_key, + jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(), + }, + async (req, token, done) => { + try { + const user = await UsersDBApi.findBy({ email: token.user.email }); + + if (user && user.disabled) { + return done(new Error(`User '${user.email}' is disabled`)); + } + + req.currentUser = user; + + return done(null, user); + } catch (error) { + done(error); + } + }, + ), +); + +passport.use( + new GoogleStrategy( + { + clientID: config.google.clientId, + clientSecret: config.google.clientSecret, + callbackURL: config.apiUrl + '/auth/signin/google/callback', + passReqToCallback: true, + }, + function (request, accessToken, refreshToken, profile, done) { + socialStrategy(profile.email, profile, providers.GOOGLE, done); + }, + ), +); + +passport.use( + new MicrosoftStrategy( + { + clientID: config.microsoft.clientId, + clientSecret: config.microsoft.clientSecret, + callbackURL: config.apiUrl + '/auth/signin/microsoft/callback', + passReqToCallback: true, + }, + function (request, accessToken, refreshToken, profile, done) { + const email = profile._json.mail || profile._json.userPrincipalName; + socialStrategy(email, profile, providers.MICROSOFT, done); + }, + ), +); + +function socialStrategy(email, profile, provider, done) { + db.users + .findOrCreate({ where: { email, provider } }) + .then(([user, created]) => { + const body = { + id: user.id, + email: user.email, + name: profile.displayName, + }; + const token = helpers.jwtSign({ user: body }); + return done(null, { token }); + }); +} diff --git a/backend/src/config.js b/backend/src/config.js new file mode 100644 index 0000000..8df8809 --- /dev/null +++ b/backend/src/config.js @@ -0,0 +1,71 @@ +const os = require('os'); + +const config = { + gcloud: { + bucket: 'fldemo-files', + hash: '2f573dc6fda7f2285dc662300ae44e90', + }, + bcrypt: { + saltRounds: 12, + }, + admin_pass: 'password', + admin_email: 'admin@flatlogic.com', + providers: { + LOCAL: 'local', + GOOGLE: 'google', + MICROSOFT: 'microsoft', + }, + secret_key: 'HUEyqESqgQ1yTwzVlO6wprC9Kf1J1xuA', + remote: '', + port: process.env.NODE_ENV === 'production' ? '' : '8080', + hostUI: process.env.NODE_ENV === 'production' ? '' : 'http://localhost', + portUI: process.env.NODE_ENV === 'production' ? '' : '3000', + + portUIProd: process.env.NODE_ENV === 'production' ? '' : ':3000', + + swaggerUI: process.env.NODE_ENV === 'production' ? '' : 'http://localhost', + swaggerPort: process.env.NODE_ENV === 'production' ? '' : ':8080', + google: { + clientId: + '671001533244-kf1k1gmp6mnl0r030qmvdu6v36ghmim6.apps.googleusercontent.com', + clientSecret: 'Yo4qbKZniqvojzUQ60iKlxqR', + }, + microsoft: { + clientId: '4696f457-31af-40de-897c-e00d7d4cff73', + clientSecret: 'm8jzZ.5UpHF3=-dXzyxiZ4e[F8OF54@p', + }, + uploadDir: os.tmpdir(), + email: { + from: 'sev ', + host: 'email-smtp.us-east-1.amazonaws.com', + port: 587, + auth: { + user: 'AKIAVEW7G4PQUBGM52OF', + pass: process.env.EMAIL_PASS, + }, + tls: { + rejectUnauthorized: false, + }, + }, + roles: { + admin: 'Administrator', + user: 'User', + }, + + project_uuid: '7ebce954-3f5c-4250-bd81-c1d75d45bc5b', + flHost: + process.env.NODE_ENV === 'production' || + process.env.NODE_ENV === 'dev_stage' + ? 'https://flatlogic.com/projects' + : 'http://localhost:3000/projects', +}; +config.pexelsKey = 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18'; +config.pexelsQuery = 'abstract technology background'; +config.host = + process.env.NODE_ENV === 'production' ? config.remote : 'http://localhost'; +config.apiUrl = `${config.host}${config.port ? `:${config.port}` : ``}/api`; +config.swaggerUrl = `${config.swaggerUI}${config.swaggerPort}`; +config.uiUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}/#`; +config.backUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}`; + +module.exports = config; diff --git a/backend/src/db/api/access_logs.js b/backend/src/db/api/access_logs.js new file mode 100644 index 0000000..d97486c --- /dev/null +++ b/backend/src/db/api/access_logs.js @@ -0,0 +1,355 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Access_logsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const access_logs = await db.access_logs.create( + { + id: data.id || undefined, + + access_time: data.access_time || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await access_logs.setUser(data.user || null, { + transaction, + }); + + await access_logs.setQr_code(data.qr_code || null, { + transaction, + }); + + return access_logs; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const access_logsData = data.map((item, index) => ({ + id: item.id || undefined, + + access_time: item.access_time || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const access_logs = await db.access_logs.bulkCreate(access_logsData, { + transaction, + }); + + // For each item created, replace relation files + + return access_logs; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const access_logs = await db.access_logs.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.access_time !== undefined) + updatePayload.access_time = data.access_time; + + updatePayload.updatedById = currentUser.id; + + await access_logs.update(updatePayload, { transaction }); + + if (data.user !== undefined) { + await access_logs.setUser( + data.user, + + { transaction }, + ); + } + + if (data.qr_code !== undefined) { + await access_logs.setQr_code( + data.qr_code, + + { transaction }, + ); + } + + return access_logs; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const access_logs = await db.access_logs.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of access_logs) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of access_logs) { + await record.destroy({ transaction }); + } + }); + + return access_logs; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const access_logs = await db.access_logs.findByPk(id, options); + + await access_logs.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await access_logs.destroy({ + transaction, + }); + + return access_logs; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const access_logs = await db.access_logs.findOne( + { where }, + { transaction }, + ); + + if (!access_logs) { + return access_logs; + } + + const output = access_logs.get({ plain: true }); + + output.user = await access_logs.getUser({ + transaction, + }); + + output.qr_code = await access_logs.getQr_code({ + transaction, + }); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.users, + as: 'user', + + where: filter.user + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.user + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + firstName: { + [Op.or]: filter.user + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.qr_codes, + as: 'qr_code', + + where: filter.qr_code + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.qr_code + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + code: { + [Op.or]: filter.qr_code + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.access_timeRange) { + const [start, end] = filter.access_timeRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + access_time: { + ...where.access_time, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + access_time: { + ...where.access_time, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.access_logs.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('access_logs', 'access_time', query), + ], + }; + } + + const records = await db.access_logs.findAll({ + attributes: ['id', 'access_time'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['access_time', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.access_time, + })); + } +}; diff --git a/backend/src/db/api/file.js b/backend/src/db/api/file.js new file mode 100644 index 0000000..22f9b6f --- /dev/null +++ b/backend/src/db/api/file.js @@ -0,0 +1,73 @@ +const db = require('../models'); +const assert = require('assert'); +const services = require('../../services/file'); + +module.exports = class FileDBApi { + static async replaceRelationFiles(relation, rawFiles, options) { + assert(relation.belongsTo, 'belongsTo is required'); + assert(relation.belongsToColumn, 'belongsToColumn is required'); + assert(relation.belongsToId, 'belongsToId is required'); + + let files = []; + + if (Array.isArray(rawFiles)) { + files = rawFiles; + } else { + files = rawFiles ? [rawFiles] : []; + } + + await this._removeLegacyFiles(relation, files, options); + await this._addFiles(relation, files, options); + } + + static async _addFiles(relation, files, options) { + const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || { id: null }; + + const inexistentFiles = files.filter((file) => !!file.new); + + for (const file of inexistentFiles) { + await db.file.create( + { + belongsTo: relation.belongsTo, + belongsToColumn: relation.belongsToColumn, + belongsToId: relation.belongsToId, + name: file.name, + sizeInBytes: file.sizeInBytes, + privateUrl: file.privateUrl, + publicUrl: file.publicUrl, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { + transaction, + }, + ); + } + } + + static async _removeLegacyFiles(relation, files, options) { + const transaction = (options && options.transaction) || undefined; + + const filesToDelete = await db.file.findAll({ + where: { + belongsTo: relation.belongsTo, + belongsToId: relation.belongsToId, + belongsToColumn: relation.belongsToColumn, + id: { + [db.Sequelize.Op.notIn]: files + .filter((file) => !file.new) + .map((file) => file.id), + }, + }, + transaction, + }); + + for (let file of filesToDelete) { + await services.deleteGCloud(file.privateUrl); + await file.destroy({ + transaction, + }); + } + } +}; diff --git a/backend/src/db/api/permissions.js b/backend/src/db/api/permissions.js new file mode 100644 index 0000000..c3017f2 --- /dev/null +++ b/backend/src/db/api/permissions.js @@ -0,0 +1,253 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class PermissionsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const permissions = await db.permissions.create( + { + id: data.id || undefined, + + name: data.name || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return permissions; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const permissionsData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const permissions = await db.permissions.bulkCreate(permissionsData, { + transaction, + }); + + // For each item created, replace relation files + + return permissions; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const permissions = await db.permissions.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + updatePayload.updatedById = currentUser.id; + + await permissions.update(updatePayload, { transaction }); + + return permissions; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const permissions = await db.permissions.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of permissions) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of permissions) { + await record.destroy({ transaction }); + } + }); + + return permissions; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const permissions = await db.permissions.findByPk(id, options); + + await permissions.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await permissions.destroy({ + transaction, + }); + + return permissions; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const permissions = await db.permissions.findOne( + { where }, + { transaction }, + ); + + if (!permissions) { + return permissions; + } + + const output = permissions.get({ plain: true }); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('permissions', 'name', filter.name), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.permissions.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('permissions', 'name', query), + ], + }; + } + + const records = await db.permissions.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/qr_codes.js b/backend/src/db/api/qr_codes.js new file mode 100644 index 0000000..4da36c8 --- /dev/null +++ b/backend/src/db/api/qr_codes.js @@ -0,0 +1,370 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Qr_codesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const qr_codes = await db.qr_codes.create( + { + id: data.id || undefined, + + code: data.code || null, + valid_from: data.valid_from || null, + valid_until: data.valid_until || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await qr_codes.setVideo(data.video || null, { + transaction, + }); + + return qr_codes; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const qr_codesData = data.map((item, index) => ({ + id: item.id || undefined, + + code: item.code || null, + valid_from: item.valid_from || null, + valid_until: item.valid_until || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const qr_codes = await db.qr_codes.bulkCreate(qr_codesData, { + transaction, + }); + + // For each item created, replace relation files + + return qr_codes; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const qr_codes = await db.qr_codes.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.code !== undefined) updatePayload.code = data.code; + + if (data.valid_from !== undefined) + updatePayload.valid_from = data.valid_from; + + if (data.valid_until !== undefined) + updatePayload.valid_until = data.valid_until; + + updatePayload.updatedById = currentUser.id; + + await qr_codes.update(updatePayload, { transaction }); + + if (data.video !== undefined) { + await qr_codes.setVideo( + data.video, + + { transaction }, + ); + } + + return qr_codes; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const qr_codes = await db.qr_codes.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of qr_codes) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of qr_codes) { + await record.destroy({ transaction }); + } + }); + + return qr_codes; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const qr_codes = await db.qr_codes.findByPk(id, options); + + await qr_codes.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await qr_codes.destroy({ + transaction, + }); + + return qr_codes; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const qr_codes = await db.qr_codes.findOne({ where }, { transaction }); + + if (!qr_codes) { + return qr_codes; + } + + const output = qr_codes.get({ plain: true }); + + output.access_logs_qr_code = await qr_codes.getAccess_logs_qr_code({ + transaction, + }); + + output.video = await qr_codes.getVideo({ + transaction, + }); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.videos, + as: 'video', + + where: filter.video + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.video + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + title: { + [Op.or]: filter.video + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.code) { + where = { + ...where, + [Op.and]: Utils.ilike('qr_codes', 'code', filter.code), + }; + } + + if (filter.calendarStart && filter.calendarEnd) { + where = { + ...where, + [Op.or]: [ + { + valid_from: { + [Op.between]: [filter.calendarStart, filter.calendarEnd], + }, + }, + { + valid_until: { + [Op.between]: [filter.calendarStart, filter.calendarEnd], + }, + }, + ], + }; + } + + if (filter.valid_fromRange) { + const [start, end] = filter.valid_fromRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + valid_from: { + ...where.valid_from, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + valid_from: { + ...where.valid_from, + [Op.lte]: end, + }, + }; + } + } + + if (filter.valid_untilRange) { + const [start, end] = filter.valid_untilRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + valid_until: { + ...where.valid_until, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + valid_until: { + ...where.valid_until, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.qr_codes.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('qr_codes', 'code', query), + ], + }; + } + + const records = await db.qr_codes.findAll({ + attributes: ['id', 'code'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['code', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.code, + })); + } +}; diff --git a/backend/src/db/api/roles.js b/backend/src/db/api/roles.js new file mode 100644 index 0000000..b59d342 --- /dev/null +++ b/backend/src/db/api/roles.js @@ -0,0 +1,315 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class RolesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const roles = await db.roles.create( + { + id: data.id || undefined, + + name: data.name || null, + role_customization: data.role_customization || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await roles.setPermissions(data.permissions || [], { + transaction, + }); + + return roles; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const rolesData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + role_customization: item.role_customization || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const roles = await db.roles.bulkCreate(rolesData, { transaction }); + + // For each item created, replace relation files + + return roles; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const roles = await db.roles.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.role_customization !== undefined) + updatePayload.role_customization = data.role_customization; + + updatePayload.updatedById = currentUser.id; + + await roles.update(updatePayload, { transaction }); + + if (data.permissions !== undefined) { + await roles.setPermissions(data.permissions, { transaction }); + } + + return roles; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const roles = await db.roles.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of roles) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of roles) { + await record.destroy({ transaction }); + } + }); + + return roles; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const roles = await db.roles.findByPk(id, options); + + await roles.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await roles.destroy({ + transaction, + }); + + return roles; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const roles = await db.roles.findOne({ where }, { transaction }); + + if (!roles) { + return roles; + } + + const output = roles.get({ plain: true }); + + output.users_app_role = await roles.getUsers_app_role({ + transaction, + }); + + output.permissions = await roles.getPermissions({ + transaction, + }); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.permissions, + as: 'permissions', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('roles', 'name', filter.name), + }; + } + + if (filter.role_customization) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'roles', + 'role_customization', + filter.role_customization, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.permissions) { + const searchTerms = filter.permissions.split('|'); + + include = [ + { + model: db.permissions, + as: 'permissions_filter', + required: searchTerms.length > 0, + where: + searchTerms.length > 0 + ? { + [Op.or]: [ + { + id: { + [Op.in]: searchTerms.map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: searchTerms.map((term) => ({ + [Op.iLike]: `%${term}%`, + })), + }, + }, + ], + } + : undefined, + }, + ...include, + ]; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.roles.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('roles', 'name', query), + ], + }; + } + + const records = await db.roles.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js new file mode 100644 index 0000000..b708394 --- /dev/null +++ b/backend/src/db/api/users.js @@ -0,0 +1,746 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const bcrypt = require('bcrypt'); +const config = require('../../config'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class UsersDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.create( + { + id: data.data.id || undefined, + + firstName: data.data.firstName || null, + lastName: data.data.lastName || null, + phoneNumber: data.data.phoneNumber || null, + email: data.data.email || null, + disabled: data.data.disabled || false, + + password: data.data.password || null, + emailVerified: data.data.emailVerified || true, + + emailVerificationToken: data.data.emailVerificationToken || null, + emailVerificationTokenExpiresAt: + data.data.emailVerificationTokenExpiresAt || null, + passwordResetToken: data.data.passwordResetToken || null, + passwordResetTokenExpiresAt: + data.data.passwordResetTokenExpiresAt || null, + provider: data.data.provider || null, + importHash: data.data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + if (!data.data.app_role) { + const role = await db.roles.findOne({ + where: { name: 'User' }, + }); + if (role) { + await users.setApp_role(role, { + transaction, + }); + } + } else { + await users.setApp_role(data.data.app_role || null, { + transaction, + }); + } + + await users.setCustom_permissions(data.data.custom_permissions || [], { + transaction, + }); + + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.users.getTableName(), + belongsToColumn: 'avatar', + belongsToId: users.id, + }, + data.data.avatar, + options, + ); + + return users; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const usersData = data.map((item, index) => ({ + id: item.id || undefined, + + firstName: item.firstName || null, + lastName: item.lastName || null, + phoneNumber: item.phoneNumber || null, + email: item.email || null, + disabled: item.disabled || false, + + password: item.password || null, + emailVerified: item.emailVerified || false, + + emailVerificationToken: item.emailVerificationToken || null, + emailVerificationTokenExpiresAt: + item.emailVerificationTokenExpiresAt || null, + passwordResetToken: item.passwordResetToken || null, + passwordResetTokenExpiresAt: item.passwordResetTokenExpiresAt || null, + provider: item.provider || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const users = await db.users.bulkCreate(usersData, { transaction }); + + // For each item created, replace relation files + + for (let i = 0; i < users.length; i++) { + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.users.getTableName(), + belongsToColumn: 'avatar', + belongsToId: users[i].id, + }, + data[i].avatar, + options, + ); + } + + return users; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findByPk(id, {}, { transaction }); + + if (!data?.app_role) { + data.app_role = users?.app_role?.id; + } + if (!data?.custom_permissions) { + data.custom_permissions = users?.custom_permissions?.map( + (item) => item.id, + ); + } + + if (data.password) { + data.password = bcrypt.hashSync(data.password, config.bcrypt.saltRounds); + } else { + data.password = users.password; + } + + const updatePayload = {}; + + if (data.firstName !== undefined) updatePayload.firstName = data.firstName; + + if (data.lastName !== undefined) updatePayload.lastName = data.lastName; + + if (data.phoneNumber !== undefined) + updatePayload.phoneNumber = data.phoneNumber; + + if (data.email !== undefined) updatePayload.email = data.email; + + if (data.disabled !== undefined) updatePayload.disabled = data.disabled; + + if (data.password !== undefined) updatePayload.password = data.password; + + if (data.emailVerified !== undefined) + updatePayload.emailVerified = data.emailVerified; + else updatePayload.emailVerified = true; + + if (data.emailVerificationToken !== undefined) + updatePayload.emailVerificationToken = data.emailVerificationToken; + + if (data.emailVerificationTokenExpiresAt !== undefined) + updatePayload.emailVerificationTokenExpiresAt = + data.emailVerificationTokenExpiresAt; + + if (data.passwordResetToken !== undefined) + updatePayload.passwordResetToken = data.passwordResetToken; + + if (data.passwordResetTokenExpiresAt !== undefined) + updatePayload.passwordResetTokenExpiresAt = + data.passwordResetTokenExpiresAt; + + if (data.provider !== undefined) updatePayload.provider = data.provider; + + updatePayload.updatedById = currentUser.id; + + await users.update(updatePayload, { transaction }); + + if (data.app_role !== undefined) { + await users.setApp_role( + data.app_role, + + { transaction }, + ); + } + + if (data.custom_permissions !== undefined) { + await users.setCustom_permissions(data.custom_permissions, { + transaction, + }); + } + + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.users.getTableName(), + belongsToColumn: 'avatar', + belongsToId: users.id, + }, + data.avatar, + options, + ); + + return users; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of users) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of users) { + await record.destroy({ transaction }); + } + }); + + return users; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findByPk(id, options); + + await users.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await users.destroy({ + transaction, + }); + + return users; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findOne({ where }, { transaction }); + + if (!users) { + return users; + } + + const output = users.get({ plain: true }); + + output.access_logs_user = await users.getAccess_logs_user({ + transaction, + }); + + output.avatar = await users.getAvatar({ + transaction, + }); + + output.app_role = await users.getApp_role({ + transaction, + }); + + if (output.app_role) { + output.app_role_permissions = await output.app_role.getPermissions({ + transaction, + }); + } + + output.custom_permissions = await users.getCustom_permissions({ + transaction, + }); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.roles, + as: 'app_role', + + where: filter.app_role + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.app_role + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.app_role + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.permissions, + as: 'custom_permissions', + }, + + { + model: db.file, + as: 'avatar', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.firstName) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'firstName', filter.firstName), + }; + } + + if (filter.lastName) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'lastName', filter.lastName), + }; + } + + if (filter.phoneNumber) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'phoneNumber', filter.phoneNumber), + }; + } + + if (filter.email) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'email', filter.email), + }; + } + + if (filter.password) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'password', filter.password), + }; + } + + if (filter.emailVerificationToken) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'emailVerificationToken', + filter.emailVerificationToken, + ), + }; + } + + if (filter.passwordResetToken) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'passwordResetToken', + filter.passwordResetToken, + ), + }; + } + + if (filter.provider) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'provider', filter.provider), + }; + } + + if (filter.emailVerificationTokenExpiresAtRange) { + const [start, end] = filter.emailVerificationTokenExpiresAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + emailVerificationTokenExpiresAt: { + ...where.emailVerificationTokenExpiresAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + emailVerificationTokenExpiresAt: { + ...where.emailVerificationTokenExpiresAt, + [Op.lte]: end, + }, + }; + } + } + + if (filter.passwordResetTokenExpiresAtRange) { + const [start, end] = filter.passwordResetTokenExpiresAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + passwordResetTokenExpiresAt: { + ...where.passwordResetTokenExpiresAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + passwordResetTokenExpiresAt: { + ...where.passwordResetTokenExpiresAt, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.disabled) { + where = { + ...where, + disabled: filter.disabled, + }; + } + + if (filter.emailVerified) { + where = { + ...where, + emailVerified: filter.emailVerified, + }; + } + + if (filter.custom_permissions) { + const searchTerms = filter.custom_permissions.split('|'); + + include = [ + { + model: db.permissions, + as: 'custom_permissions_filter', + required: searchTerms.length > 0, + where: + searchTerms.length > 0 + ? { + [Op.or]: [ + { + id: { + [Op.in]: searchTerms.map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: searchTerms.map((term) => ({ + [Op.iLike]: `%${term}%`, + })), + }, + }, + ], + } + : undefined, + }, + ...include, + ]; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.users.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('users', 'firstName', query), + ], + }; + } + + const records = await db.users.findAll({ + attributes: ['id', 'firstName'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['firstName', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.firstName, + })); + } + + static async createFromAuth(data, options) { + const transaction = (options && options.transaction) || undefined; + const users = await db.users.create( + { + email: data.email, + firstName: data.firstName, + authenticationUid: data.authenticationUid, + password: data.password, + }, + { transaction }, + ); + + const app_role = await db.roles.findOne({ + where: { name: 'User' }, + }); + if (app_role?.id) { + await users.setApp_role(app_role?.id || null, { + transaction, + }); + } + + await users.update( + { + authenticationUid: users.id, + }, + { transaction }, + ); + + delete users.password; + return users; + } + + static async updatePassword(id, password, options) { + const currentUser = (options && options.currentUser) || { id: null }; + + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findByPk(id, { + transaction, + }); + + await users.update( + { + password, + authenticationUid: id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return users; + } + + static async generateEmailVerificationToken(email, options) { + return this._generateToken( + ['emailVerificationToken', 'emailVerificationTokenExpiresAt'], + email, + options, + ); + } + + static async generatePasswordResetToken(email, options) { + return this._generateToken( + ['passwordResetToken', 'passwordResetTokenExpiresAt'], + email, + options, + ); + } + + static async findByPasswordResetToken(token, options) { + const transaction = (options && options.transaction) || undefined; + + return db.users.findOne( + { + where: { + passwordResetToken: token, + passwordResetTokenExpiresAt: { + [db.Sequelize.Op.gt]: Date.now(), + }, + }, + }, + { transaction }, + ); + } + + static async findByEmailVerificationToken(token, options) { + const transaction = (options && options.transaction) || undefined; + return db.users.findOne( + { + where: { + emailVerificationToken: token, + emailVerificationTokenExpiresAt: { + [db.Sequelize.Op.gt]: Date.now(), + }, + }, + }, + { transaction }, + ); + } + + static async markEmailVerified(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findByPk(id, { + transaction, + }); + + await users.update( + { + emailVerified: true, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return true; + } + + static async _generateToken(keyNames, email, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const users = await db.users.findOne( + { + where: { email: email.toLowerCase() }, + }, + { + transaction, + }, + ); + + const token = crypto.randomBytes(20).toString('hex'); + const tokenExpiresAt = Date.now() + 360000; + + if (users) { + await users.update( + { + [keyNames[0]]: token, + [keyNames[1]]: tokenExpiresAt, + updatedById: currentUser.id, + }, + { transaction }, + ); + } + + return token; + } +}; diff --git a/backend/src/db/api/videos.js b/backend/src/db/api/videos.js new file mode 100644 index 0000000..a63e18e --- /dev/null +++ b/backend/src/db/api/videos.js @@ -0,0 +1,272 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class VideosDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const videos = await db.videos.create( + { + id: data.id || undefined, + + title: data.title || null, + url: data.url || null, + status: data.status || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return videos; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const videosData = data.map((item, index) => ({ + id: item.id || undefined, + + title: item.title || null, + url: item.url || null, + status: item.status || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const videos = await db.videos.bulkCreate(videosData, { transaction }); + + // For each item created, replace relation files + + return videos; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const videos = await db.videos.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.title !== undefined) updatePayload.title = data.title; + + if (data.url !== undefined) updatePayload.url = data.url; + + if (data.status !== undefined) updatePayload.status = data.status; + + updatePayload.updatedById = currentUser.id; + + await videos.update(updatePayload, { transaction }); + + return videos; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const videos = await db.videos.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of videos) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of videos) { + await record.destroy({ transaction }); + } + }); + + return videos; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const videos = await db.videos.findByPk(id, options); + + await videos.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await videos.destroy({ + transaction, + }); + + return videos; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const videos = await db.videos.findOne({ where }, { transaction }); + + if (!videos) { + return videos; + } + + const output = videos.get({ plain: true }); + + output.qr_codes_video = await videos.getQr_codes_video({ + transaction, + }); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.title) { + where = { + ...where, + [Op.and]: Utils.ilike('videos', 'title', filter.title), + }; + } + + if (filter.url) { + where = { + ...where, + [Op.and]: Utils.ilike('videos', 'url', filter.url), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.status) { + where = { + ...where, + status: filter.status, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.videos.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('videos', 'title', query), + ], + }; + } + + const records = await db.videos.findAll({ + attributes: ['id', 'title'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['title', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.title, + })); + } +}; diff --git a/backend/src/db/db.config.js b/backend/src/db/db.config.js new file mode 100644 index 0000000..46f4281 --- /dev/null +++ b/backend/src/db/db.config.js @@ -0,0 +1,31 @@ +module.exports = { + production: { + dialect: 'postgres', + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + host: process.env.DB_HOST, + port: process.env.DB_PORT, + logging: console.log, + seederStorage: 'sequelize', + }, + development: { + username: 'postgres', + dialect: 'postgres', + password: '', + database: 'db_sev', + host: process.env.DB_HOST || 'localhost', + logging: console.log, + seederStorage: 'sequelize', + }, + dev_stage: { + dialect: 'postgres', + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + host: process.env.DB_HOST, + port: process.env.DB_PORT, + logging: console.log, + seederStorage: 'sequelize', + }, +}; diff --git a/backend/src/db/migrations/1741172912672.js b/backend/src/db/migrations/1741172912672.js new file mode 100644 index 0000000..cdcc867 --- /dev/null +++ b/backend/src/db/migrations/1741172912672.js @@ -0,0 +1,594 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.createTable( + 'users', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'access_logs', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'qr_codes', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'videos', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'roles', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'permissions', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'firstName', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'lastName', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'phoneNumber', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'email', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'disabled', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'password', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'emailVerified', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'emailVerificationToken', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'emailVerificationTokenExpiresAt', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'passwordResetToken', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'passwordResetTokenExpiresAt', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'provider', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'access_logs', + 'userId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'users', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'access_logs', + 'qr_codeId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'qr_codes', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'access_logs', + 'access_time', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'qr_codes', + 'code', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'qr_codes', + 'videoId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'videos', + key: 'id', + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'qr_codes', + 'valid_from', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'qr_codes', + 'valid_until', + { + type: Sequelize.DataTypes.DATE, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'videos', + 'title', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'videos', + 'url', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'videos', + 'status', + { + type: Sequelize.DataTypes.ENUM, + + values: ['active', 'inactive'], + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'permissions', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'roles', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'roles', + 'role_customization', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'users', + 'app_roleId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'roles', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('users', 'app_roleId', { transaction }); + + await queryInterface.removeColumn('roles', 'role_customization', { + transaction, + }); + + await queryInterface.removeColumn('roles', 'name', { transaction }); + + await queryInterface.removeColumn('permissions', 'name', { transaction }); + + await queryInterface.removeColumn('videos', 'status', { transaction }); + + await queryInterface.removeColumn('videos', 'url', { transaction }); + + await queryInterface.removeColumn('videos', 'title', { transaction }); + + await queryInterface.removeColumn('qr_codes', 'valid_until', { + transaction, + }); + + await queryInterface.removeColumn('qr_codes', 'valid_from', { + transaction, + }); + + await queryInterface.removeColumn('qr_codes', 'videoId', { transaction }); + + await queryInterface.removeColumn('qr_codes', 'code', { transaction }); + + await queryInterface.removeColumn('access_logs', 'access_time', { + transaction, + }); + + await queryInterface.removeColumn('access_logs', 'qr_codeId', { + transaction, + }); + + await queryInterface.removeColumn('access_logs', 'userId', { + transaction, + }); + + await queryInterface.removeColumn('users', 'provider', { transaction }); + + await queryInterface.removeColumn( + 'users', + 'passwordResetTokenExpiresAt', + { transaction }, + ); + + await queryInterface.removeColumn('users', 'passwordResetToken', { + transaction, + }); + + await queryInterface.removeColumn( + 'users', + 'emailVerificationTokenExpiresAt', + { transaction }, + ); + + await queryInterface.removeColumn('users', 'emailVerificationToken', { + transaction, + }); + + await queryInterface.removeColumn('users', 'emailVerified', { + transaction, + }); + + await queryInterface.removeColumn('users', 'password', { transaction }); + + await queryInterface.removeColumn('users', 'disabled', { transaction }); + + await queryInterface.removeColumn('users', 'email', { transaction }); + + await queryInterface.removeColumn('users', 'phoneNumber', { + transaction, + }); + + await queryInterface.removeColumn('users', 'lastName', { transaction }); + + await queryInterface.removeColumn('users', 'firstName', { transaction }); + + await queryInterface.dropTable('permissions', { transaction }); + + await queryInterface.dropTable('roles', { transaction }); + + await queryInterface.dropTable('videos', { transaction }); + + await queryInterface.dropTable('qr_codes', { transaction }); + + await queryInterface.dropTable('access_logs', { transaction }); + + await queryInterface.dropTable('users', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/access_logs.js b/backend/src/db/models/access_logs.js new file mode 100644 index 0000000..9af9d14 --- /dev/null +++ b/backend/src/db/models/access_logs.js @@ -0,0 +1,65 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const access_logs = sequelize.define( + 'access_logs', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + access_time: { + type: DataTypes.DATE, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + access_logs.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.access_logs.belongsTo(db.users, { + as: 'user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + + db.access_logs.belongsTo(db.qr_codes, { + as: 'qr_code', + foreignKey: { + name: 'qr_codeId', + }, + constraints: false, + }); + + db.access_logs.belongsTo(db.users, { + as: 'createdBy', + }); + + db.access_logs.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return access_logs; +}; diff --git a/backend/src/db/models/file.js b/backend/src/db/models/file.js new file mode 100644 index 0000000..84ee670 --- /dev/null +++ b/backend/src/db/models/file.js @@ -0,0 +1,53 @@ +module.exports = function (sequelize, DataTypes) { + const file = sequelize.define( + 'file', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + belongsTo: DataTypes.STRING(255), + belongsToId: DataTypes.UUID, + belongsToColumn: DataTypes.STRING(255), + name: { + type: DataTypes.STRING(2083), + allowNull: false, + validate: { + notEmpty: true, + }, + }, + sizeInBytes: { + type: DataTypes.INTEGER, + allowNull: true, + }, + privateUrl: { + type: DataTypes.STRING(2083), + allowNull: true, + }, + publicUrl: { + type: DataTypes.STRING(2083), + allowNull: false, + validate: { + notEmpty: true, + }, + }, + }, + { + timestamps: true, + paranoid: true, + }, + ); + + file.associate = (db) => { + db.file.belongsTo(db.users, { + as: 'createdBy', + }); + + db.file.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return file; +}; diff --git a/backend/src/db/models/index.js b/backend/src/db/models/index.js new file mode 100644 index 0000000..e326416 --- /dev/null +++ b/backend/src/db/models/index.js @@ -0,0 +1,47 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const Sequelize = require('sequelize'); +const basename = path.basename(__filename); +const env = process.env.NODE_ENV || 'development'; +const config = require('../db.config')[env]; +const db = {}; + +let sequelize; +console.log(env); +if (config.use_env_variable) { + sequelize = new Sequelize(process.env[config.use_env_variable], config); +} else { + sequelize = new Sequelize( + config.database, + config.username, + config.password, + config, + ); +} + +fs.readdirSync(__dirname) + .filter((file) => { + return ( + file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js' + ); + }) + .forEach((file) => { + const model = require(path.join(__dirname, file))( + sequelize, + Sequelize.DataTypes, + ); + db[model.name] = model; + }); + +Object.keys(db).forEach((modelName) => { + if (db[modelName].associate) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; +db.Sequelize = Sequelize; + +module.exports = db; diff --git a/backend/src/db/models/permissions.js b/backend/src/db/models/permissions.js new file mode 100644 index 0000000..d647c73 --- /dev/null +++ b/backend/src/db/models/permissions.js @@ -0,0 +1,49 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const permissions = sequelize.define( + 'permissions', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + permissions.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.permissions.belongsTo(db.users, { + as: 'createdBy', + }); + + db.permissions.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return permissions; +}; diff --git a/backend/src/db/models/qr_codes.js b/backend/src/db/models/qr_codes.js new file mode 100644 index 0000000..ad4acad --- /dev/null +++ b/backend/src/db/models/qr_codes.js @@ -0,0 +1,73 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const qr_codes = sequelize.define( + 'qr_codes', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + code: { + type: DataTypes.TEXT, + }, + + valid_from: { + type: DataTypes.DATE, + }, + + valid_until: { + type: DataTypes.DATE, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + qr_codes.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.qr_codes.hasMany(db.access_logs, { + as: 'access_logs_qr_code', + foreignKey: { + name: 'qr_codeId', + }, + constraints: false, + }); + + //end loop + + db.qr_codes.belongsTo(db.videos, { + as: 'video', + foreignKey: { + name: 'videoId', + }, + constraints: false, + }); + + db.qr_codes.belongsTo(db.users, { + as: 'createdBy', + }); + + db.qr_codes.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return qr_codes; +}; diff --git a/backend/src/db/models/roles.js b/backend/src/db/models/roles.js new file mode 100644 index 0000000..0ff5736 --- /dev/null +++ b/backend/src/db/models/roles.js @@ -0,0 +1,79 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const roles = sequelize.define( + 'roles', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + role_customization: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + roles.associate = (db) => { + db.roles.belongsToMany(db.permissions, { + as: 'permissions', + foreignKey: { + name: 'roles_permissionsId', + }, + constraints: false, + through: 'rolesPermissionsPermissions', + }); + + db.roles.belongsToMany(db.permissions, { + as: 'permissions_filter', + foreignKey: { + name: 'roles_permissionsId', + }, + constraints: false, + through: 'rolesPermissionsPermissions', + }); + + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.roles.hasMany(db.users, { + as: 'users_app_role', + foreignKey: { + name: 'app_roleId', + }, + constraints: false, + }); + + //end loop + + db.roles.belongsTo(db.users, { + as: 'createdBy', + }); + + db.roles.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return roles; +}; diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js new file mode 100644 index 0000000..e08ae97 --- /dev/null +++ b/backend/src/db/models/users.js @@ -0,0 +1,179 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const users = sequelize.define( + 'users', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + firstName: { + type: DataTypes.TEXT, + }, + + lastName: { + type: DataTypes.TEXT, + }, + + phoneNumber: { + type: DataTypes.TEXT, + }, + + email: { + type: DataTypes.TEXT, + }, + + disabled: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + }, + + password: { + type: DataTypes.TEXT, + }, + + emailVerified: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + }, + + emailVerificationToken: { + type: DataTypes.TEXT, + }, + + emailVerificationTokenExpiresAt: { + type: DataTypes.DATE, + }, + + passwordResetToken: { + type: DataTypes.TEXT, + }, + + passwordResetTokenExpiresAt: { + type: DataTypes.DATE, + }, + + provider: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + users.associate = (db) => { + db.users.belongsToMany(db.permissions, { + as: 'custom_permissions', + foreignKey: { + name: 'users_custom_permissionsId', + }, + constraints: false, + through: 'usersCustom_permissionsPermissions', + }); + + db.users.belongsToMany(db.permissions, { + as: 'custom_permissions_filter', + foreignKey: { + name: 'users_custom_permissionsId', + }, + constraints: false, + through: 'usersCustom_permissionsPermissions', + }); + + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.users.hasMany(db.access_logs, { + as: 'access_logs_user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + + //end loop + + db.users.belongsTo(db.roles, { + as: 'app_role', + foreignKey: { + name: 'app_roleId', + }, + constraints: false, + }); + + db.users.hasMany(db.file, { + as: 'avatar', + foreignKey: 'belongsToId', + constraints: false, + scope: { + belongsTo: db.users.getTableName(), + belongsToColumn: 'avatar', + }, + }); + + db.users.belongsTo(db.users, { + as: 'createdBy', + }); + + db.users.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + users.beforeCreate((users, options) => { + users = trimStringFields(users); + + if ( + users.provider !== providers.LOCAL && + Object.values(providers).indexOf(users.provider) > -1 + ) { + users.emailVerified = true; + + if (!users.password) { + const password = crypto.randomBytes(20).toString('hex'); + + const hashedPassword = bcrypt.hashSync( + password, + config.bcrypt.saltRounds, + ); + + users.password = hashedPassword; + } + } + }); + + users.beforeUpdate((users, options) => { + users = trimStringFields(users); + }); + + return users; +}; + +function trimStringFields(users) { + users.email = users.email.trim(); + + users.firstName = users.firstName ? users.firstName.trim() : null; + + users.lastName = users.lastName ? users.lastName.trim() : null; + + return users; +} diff --git a/backend/src/db/models/videos.js b/backend/src/db/models/videos.js new file mode 100644 index 0000000..7bf8ee1 --- /dev/null +++ b/backend/src/db/models/videos.js @@ -0,0 +1,67 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const videos = sequelize.define( + 'videos', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + title: { + type: DataTypes.TEXT, + }, + + url: { + type: DataTypes.TEXT, + }, + + status: { + type: DataTypes.ENUM, + + values: ['active', 'inactive'], + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + videos.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.videos.hasMany(db.qr_codes, { + as: 'qr_codes_video', + foreignKey: { + name: 'videoId', + }, + constraints: false, + }); + + //end loop + + db.videos.belongsTo(db.users, { + as: 'createdBy', + }); + + db.videos.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return videos; +}; diff --git a/backend/src/db/reset.js b/backend/src/db/reset.js new file mode 100644 index 0000000..bc0b5f9 --- /dev/null +++ b/backend/src/db/reset.js @@ -0,0 +1,16 @@ +const db = require('./models'); +const { execSync } = require('child_process'); + +console.log('Resetting Database'); + +db.sequelize + .sync({ force: true }) + .then(() => { + execSync('sequelize db:seed:all'); + console.log('OK'); + process.exit(); + }) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/backend/src/db/seeders/20200430130759-admin-user.js b/backend/src/db/seeders/20200430130759-admin-user.js new file mode 100644 index 0000000..fccd771 --- /dev/null +++ b/backend/src/db/seeders/20200430130759-admin-user.js @@ -0,0 +1,69 @@ +'use strict'; +const bcrypt = require('bcrypt'); +const config = require('../../config'); + +const ids = [ + '193bf4b5-9f07-4bd5-9a43-e7e41f3e96af', + 'af5a87be-8f9c-4630-902a-37a60b7005ba', + '5bc531ab-611f-41f3-9373-b7cc5d09c93d', +]; + +module.exports = { + up: async (queryInterface, Sequelize) => { + let hash = bcrypt.hashSync(config.admin_pass, config.bcrypt.saltRounds); + + try { + await queryInterface.bulkInsert('users', [ + { + id: ids[0], + firstName: 'Admin', + email: config.admin_email, + emailVerified: true, + provider: config.providers.LOCAL, + password: hash, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: ids[1], + firstName: 'John', + email: 'john@doe.com', + emailVerified: true, + provider: config.providers.LOCAL, + password: hash, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: ids[2], + firstName: 'Client', + email: 'client@hello.com', + emailVerified: true, + provider: config.providers.LOCAL, + password: hash, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]); + } catch (error) { + console.error('Error during bulkInsert:', error); + throw error; + } + }, + down: async (queryInterface, Sequelize) => { + try { + await queryInterface.bulkDelete( + 'users', + { + id: { + [Sequelize.Op.in]: ids, + }, + }, + {}, + ); + } catch (error) { + console.error('Error during bulkDelete:', error); + throw error; + } + }, +}; diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js new file mode 100644 index 0000000..5e75b37 --- /dev/null +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -0,0 +1,622 @@ +const { v4: uuid } = require('uuid'); + +module.exports = { + /** + * @param{import("sequelize").QueryInterface} queryInterface + * @return {Promise} + */ + async up(queryInterface) { + const createdAt = new Date(); + const updatedAt = new Date(); + + /** @type {Map} */ + const idMap = new Map(); + + /** + * @param {string} key + * @return {string} + */ + function getId(key) { + if (idMap.has(key)) { + return idMap.get(key); + } + const id = uuid(); + idMap.set(key, id); + return id; + } + + await queryInterface.bulkInsert('roles', [ + { + id: getId('Administrator'), + name: 'Administrator', + createdAt, + updatedAt, + }, + + { + id: getId('SystemManager'), + name: 'System Manager', + createdAt, + updatedAt, + }, + + { + id: getId('ContentSupervisor'), + name: 'Content Supervisor', + createdAt, + updatedAt, + }, + + { id: getId('VideoEditor'), name: 'Video Editor', createdAt, updatedAt }, + + { + id: getId('QRCodeSpecialist'), + name: 'QR Code Specialist', + createdAt, + updatedAt, + }, + + { id: getId('Viewer'), name: 'Viewer', createdAt, updatedAt }, + ]); + + /** + * @param {string} name + */ + function createPermissions(name) { + return [ + { + id: getId(`CREATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `CREATE_${name.toUpperCase()}`, + }, + { + id: getId(`READ_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `READ_${name.toUpperCase()}`, + }, + { + id: getId(`UPDATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `UPDATE_${name.toUpperCase()}`, + }, + { + id: getId(`DELETE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `DELETE_${name.toUpperCase()}`, + }, + ]; + } + + const entities = [ + 'users', + 'access_logs', + 'qr_codes', + 'videos', + 'roles', + 'permissions', + , + ]; + await queryInterface.bulkInsert( + 'permissions', + entities.flatMap(createPermissions), + ); + await queryInterface.bulkInsert('permissions', [ + { + id: getId(`READ_API_DOCS`), + createdAt, + updatedAt, + name: `READ_API_DOCS`, + }, + ]); + await queryInterface.bulkInsert('permissions', [ + { + id: getId(`CREATE_SEARCH`), + createdAt, + updatedAt, + name: `CREATE_SEARCH`, + }, + ]); + + await queryInterface.sequelize + .query(`create table "rolesPermissionsPermissions" +( +"createdAt" timestamp with time zone not null, +"updatedAt" timestamp with time zone not null, +"roles_permissionsId" uuid not null, +"permissionId" uuid not null, +primary key ("roles_permissionsId", "permissionId") +);`); + + await queryInterface.bulkInsert('rolesPermissionsPermissions', [ + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('CREATE_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('READ_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('UPDATE_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('DELETE_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentSupervisor'), + permissionId: getId('READ_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentSupervisor'), + permissionId: getId('UPDATE_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('CREATE_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('READ_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('UPDATE_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('DELETE_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentSupervisor'), + permissionId: getId('READ_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentSupervisor'), + permissionId: getId('UPDATE_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('CREATE_QR_CODES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('READ_QR_CODES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('UPDATE_QR_CODES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('DELETE_QR_CODES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentSupervisor'), + permissionId: getId('CREATE_QR_CODES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentSupervisor'), + permissionId: getId('READ_QR_CODES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentSupervisor'), + permissionId: getId('UPDATE_QR_CODES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('VideoEditor'), + permissionId: getId('READ_QR_CODES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('VideoEditor'), + permissionId: getId('UPDATE_QR_CODES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('QRCodeSpecialist'), + permissionId: getId('CREATE_QR_CODES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('QRCodeSpecialist'), + permissionId: getId('READ_QR_CODES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('QRCodeSpecialist'), + permissionId: getId('UPDATE_QR_CODES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Viewer'), + permissionId: getId('READ_QR_CODES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('CREATE_VIDEOS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('READ_VIDEOS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('UPDATE_VIDEOS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('DELETE_VIDEOS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentSupervisor'), + permissionId: getId('CREATE_VIDEOS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentSupervisor'), + permissionId: getId('READ_VIDEOS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentSupervisor'), + permissionId: getId('UPDATE_VIDEOS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('VideoEditor'), + permissionId: getId('CREATE_VIDEOS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('VideoEditor'), + permissionId: getId('READ_VIDEOS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('VideoEditor'), + permissionId: getId('UPDATE_VIDEOS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('QRCodeSpecialist'), + permissionId: getId('READ_VIDEOS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('QRCodeSpecialist'), + permissionId: getId('UPDATE_VIDEOS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Viewer'), + permissionId: getId('READ_VIDEOS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SystemManager'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentSupervisor'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('VideoEditor'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('QRCodeSpecialist'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Viewer'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_USERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_USERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_USERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_ACCESS_LOGS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_ACCESS_LOGS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_ACCESS_LOGS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_QR_CODES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_QR_CODES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_QR_CODES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_QR_CODES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_VIDEOS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_VIDEOS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_VIDEOS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_VIDEOS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_ROLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_ROLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_ROLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_ROLES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_PERMISSIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_PERMISSIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_PERMISSIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_PERMISSIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_API_DOCS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_SEARCH'), + }, + ]); + + await queryInterface.sequelize.query( + `UPDATE "users" SET "app_roleId"='${getId( + 'SuperAdmin', + )}' WHERE "email"='super_admin@flatlogic.com'`, + ); + await queryInterface.sequelize.query( + `UPDATE "users" SET "app_roleId"='${getId( + 'Administrator', + )}' WHERE "email"='admin@flatlogic.com'`, + ); + + await queryInterface.sequelize.query( + `UPDATE "users" SET "app_roleId"='${getId( + 'SystemManager', + )}' WHERE "email"='client@hello.com'`, + ); + await queryInterface.sequelize.query( + `UPDATE "users" SET "app_roleId"='${getId( + 'ContentSupervisor', + )}' WHERE "email"='john@doe.com'`, + ); + }, +}; diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js new file mode 100644 index 0000000..dca7a9f --- /dev/null +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -0,0 +1,286 @@ +const db = require('../models'); +const Users = db.users; + +const AccessLogs = db.access_logs; + +const QrCodes = db.qr_codes; + +const Videos = db.videos; + +const AccessLogsData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + access_time: new Date('2023-10-01T10:05:00Z'), + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + access_time: new Date('2023-10-02T10:15:00Z'), + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + access_time: new Date('2023-10-03T10:25:00Z'), + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + access_time: new Date('2023-10-04T10:35:00Z'), + }, +]; + +const QrCodesData = [ + { + code: 'QR123456', + + // type code here for "relation_one" field + + valid_from: new Date('2023-10-01T10:00:00Z'), + + valid_until: new Date('2023-10-01T12:00:00Z'), + }, + + { + code: 'QR654321', + + // type code here for "relation_one" field + + valid_from: new Date('2023-10-02T10:00:00Z'), + + valid_until: new Date('2023-10-02T12:00:00Z'), + }, + + { + code: 'QR112233', + + // type code here for "relation_one" field + + valid_from: new Date('2023-10-03T10:00:00Z'), + + valid_until: new Date('2023-10-03T12:00:00Z'), + }, + + { + code: 'QR445566', + + // type code here for "relation_one" field + + valid_from: new Date('2023-10-04T10:00:00Z'), + + valid_until: new Date('2023-10-04T12:00:00Z'), + }, +]; + +const VideosData = [ + { + title: 'Introduction to QR Codes', + + url: 'https://www.example.com/video1', + + status: 'active', + }, + + { + title: 'Advanced QR Code Techniques', + + url: 'https://www.example.com/video2', + + status: 'active', + }, + + { + title: 'QR Code Security', + + url: 'https://www.example.com/video3', + + status: 'active', + }, + + { + title: 'QR Code Applications', + + url: 'https://www.example.com/video4', + + status: 'inactive', + }, +]; + +// Similar logic for "relation_many" + +async function associateAccessLogWithUser() { + const relatedUser0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const AccessLog0 = await AccessLogs.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (AccessLog0?.setUser) { + await AccessLog0.setUser(relatedUser0); + } + + const relatedUser1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const AccessLog1 = await AccessLogs.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (AccessLog1?.setUser) { + await AccessLog1.setUser(relatedUser1); + } + + const relatedUser2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const AccessLog2 = await AccessLogs.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (AccessLog2?.setUser) { + await AccessLog2.setUser(relatedUser2); + } + + const relatedUser3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const AccessLog3 = await AccessLogs.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (AccessLog3?.setUser) { + await AccessLog3.setUser(relatedUser3); + } +} + +async function associateAccessLogWithQr_code() { + const relatedQr_code0 = await QrCodes.findOne({ + offset: Math.floor(Math.random() * (await QrCodes.count())), + }); + const AccessLog0 = await AccessLogs.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (AccessLog0?.setQr_code) { + await AccessLog0.setQr_code(relatedQr_code0); + } + + const relatedQr_code1 = await QrCodes.findOne({ + offset: Math.floor(Math.random() * (await QrCodes.count())), + }); + const AccessLog1 = await AccessLogs.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (AccessLog1?.setQr_code) { + await AccessLog1.setQr_code(relatedQr_code1); + } + + const relatedQr_code2 = await QrCodes.findOne({ + offset: Math.floor(Math.random() * (await QrCodes.count())), + }); + const AccessLog2 = await AccessLogs.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (AccessLog2?.setQr_code) { + await AccessLog2.setQr_code(relatedQr_code2); + } + + const relatedQr_code3 = await QrCodes.findOne({ + offset: Math.floor(Math.random() * (await QrCodes.count())), + }); + const AccessLog3 = await AccessLogs.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (AccessLog3?.setQr_code) { + await AccessLog3.setQr_code(relatedQr_code3); + } +} + +async function associateQrCodeWithVideo() { + const relatedVideo0 = await Videos.findOne({ + offset: Math.floor(Math.random() * (await Videos.count())), + }); + const QrCode0 = await QrCodes.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (QrCode0?.setVideo) { + await QrCode0.setVideo(relatedVideo0); + } + + const relatedVideo1 = await Videos.findOne({ + offset: Math.floor(Math.random() * (await Videos.count())), + }); + const QrCode1 = await QrCodes.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (QrCode1?.setVideo) { + await QrCode1.setVideo(relatedVideo1); + } + + const relatedVideo2 = await Videos.findOne({ + offset: Math.floor(Math.random() * (await Videos.count())), + }); + const QrCode2 = await QrCodes.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (QrCode2?.setVideo) { + await QrCode2.setVideo(relatedVideo2); + } + + const relatedVideo3 = await Videos.findOne({ + offset: Math.floor(Math.random() * (await Videos.count())), + }); + const QrCode3 = await QrCodes.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (QrCode3?.setVideo) { + await QrCode3.setVideo(relatedVideo3); + } +} + +module.exports = { + up: async (queryInterface, Sequelize) => { + await AccessLogs.bulkCreate(AccessLogsData); + + await QrCodes.bulkCreate(QrCodesData); + + await Videos.bulkCreate(VideosData); + + await Promise.all([ + // Similar logic for "relation_many" + + await associateAccessLogWithUser(), + + await associateAccessLogWithQr_code(), + + await associateQrCodeWithVideo(), + ]); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('access_logs', null, {}); + + await queryInterface.bulkDelete('qr_codes', null, {}); + + await queryInterface.bulkDelete('videos', null, {}); + }, +}; diff --git a/backend/src/db/utils.js b/backend/src/db/utils.js new file mode 100644 index 0000000..c257f8d --- /dev/null +++ b/backend/src/db/utils.js @@ -0,0 +1,24 @@ +const validator = require('validator'); +const { v4: uuid } = require('uuid'); +const Sequelize = require('./models').Sequelize; + +module.exports = class Utils { + static uuid(value) { + let id = value; + + if (!validator.isUUID(id)) { + id = uuid(); + } + + return id; + } + + static ilike(model, column, value) { + return Sequelize.where( + Sequelize.fn('lower', Sequelize.col(`${model}.${column}`)), + { + [Sequelize.Op.like]: `%${value}%`.toLowerCase(), + }, + ); + } +}; diff --git a/backend/src/helpers.js b/backend/src/helpers.js new file mode 100644 index 0000000..1d918b5 --- /dev/null +++ b/backend/src/helpers.js @@ -0,0 +1,23 @@ +const jwt = require('jsonwebtoken'); +const config = require('./config'); + +module.exports = class Helpers { + static wrapAsync(fn) { + return function (req, res, next) { + fn(req, res, next).catch(next); + }; + } + + static commonErrorHandler(error, req, res, next) { + if ([400, 403, 404].includes(error.code)) { + return res.status(error.code).send(error.message); + } + + console.error(error); + return res.status(500).send(error.message); + } + + static jwtSign(data) { + return jwt.sign(data, config.secret_key, { expiresIn: '6h' }); + } +}; diff --git a/backend/src/index.js b/backend/src/index.js new file mode 100644 index 0000000..0681547 --- /dev/null +++ b/backend/src/index.js @@ -0,0 +1,161 @@ +const express = require('express'); +const cors = require('cors'); +const app = express(); +const passport = require('passport'); +const path = require('path'); +const fs = require('fs'); +const bodyParser = require('body-parser'); +const db = require('./db/models'); +const config = require('./config'); +const swaggerUI = require('swagger-ui-express'); +const swaggerJsDoc = require('swagger-jsdoc'); + +const authRoutes = require('./routes/auth'); +const fileRoutes = require('./routes/file'); +const searchRoutes = require('./routes/search'); +const pexelsRoutes = require('./routes/pexels'); + +const openaiRoutes = require('./routes/openai'); + +const contactFormRoutes = require('./routes/contactForm'); + +const usersRoutes = require('./routes/users'); + +const access_logsRoutes = require('./routes/access_logs'); + +const qr_codesRoutes = require('./routes/qr_codes'); + +const videosRoutes = require('./routes/videos'); + +const rolesRoutes = require('./routes/roles'); + +const permissionsRoutes = require('./routes/permissions'); + +const options = { + definition: { + openapi: '3.0.0', + info: { + version: '1.0.0', + title: 'sev', + description: + 'sev Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.', + }, + servers: [ + { + url: config.swaggerUrl, + description: 'Development server', + }, + ], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + responses: { + UnauthorizedError: { + description: 'Access token is missing or invalid', + }, + }, + }, + security: [ + { + bearerAuth: [], + }, + ], + }, + apis: ['./src/routes/*.js'], +}; + +const specs = swaggerJsDoc(options); +app.use( + '/api-docs', + function (req, res, next) { + swaggerUI.host = req.get('host'); + next(); + }, + swaggerUI.serve, + swaggerUI.setup(specs), +); + +app.use(cors({ origin: true })); +require('./auth/auth'); + +app.use(bodyParser.json()); + +app.use('/api/auth', authRoutes); +app.use('/api/file', fileRoutes); +app.use('/api/pexels', pexelsRoutes); +app.enable('trust proxy'); + +app.use( + '/api/users', + passport.authenticate('jwt', { session: false }), + usersRoutes, +); + +app.use( + '/api/access_logs', + passport.authenticate('jwt', { session: false }), + access_logsRoutes, +); + +app.use( + '/api/qr_codes', + passport.authenticate('jwt', { session: false }), + qr_codesRoutes, +); + +app.use( + '/api/videos', + passport.authenticate('jwt', { session: false }), + videosRoutes, +); + +app.use( + '/api/roles', + passport.authenticate('jwt', { session: false }), + rolesRoutes, +); + +app.use( + '/api/permissions', + passport.authenticate('jwt', { session: false }), + permissionsRoutes, +); + +app.use( + '/api/openai', + passport.authenticate('jwt', { session: false }), + openaiRoutes, +); + +app.use('/api/contact-form', contactFormRoutes); + +app.use( + '/api/search', + passport.authenticate('jwt', { session: false }), + searchRoutes, +); + +const publicDir = path.join(__dirname, '../public'); + +if (fs.existsSync(publicDir)) { + app.use('/', express.static(publicDir)); + + app.get('*', function (request, response) { + response.sendFile(path.resolve(publicDir, 'index.html')); + }); +} + +const PORT = process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080; + +db.sequelize.sync().then(function () { + app.listen(PORT, () => { + console.log(`Listening on port ${PORT}`); + }); +}); + +module.exports = app; diff --git a/backend/src/middlewares/check-permissions.js b/backend/src/middlewares/check-permissions.js new file mode 100644 index 0000000..2675691 --- /dev/null +++ b/backend/src/middlewares/check-permissions.js @@ -0,0 +1,64 @@ +const ValidationError = require('../services/notifications/errors/validation'); + +/** + * @param {string} permission + * @return {import("express").RequestHandler} + */ +function checkPermissions(permission) { + return (req, res, next) => { + const { currentUser } = req; + if (currentUser) { + if (currentUser.id === req.params.id || currentUser.id === req.body.id) { + next(); + return; + } + const userPermission = currentUser.custom_permissions.find( + (cp) => cp.name === permission, + ); + + if (userPermission) { + next(); + } else { + if (!currentUser.app_role) { + return next(new ValidationError('auth.forbidden')); + } + currentUser.app_role + .getPermissions() + .then((permissions) => { + if (permissions.find((p) => p.name === permission)) { + next(); + } else { + next(new ValidationError('auth.forbidden')); + } + }) + .catch((e) => next(e)); + } + } else { + next(new ValidationError('auth.unauthorized')); + } + }; +} + +const METHOD_MAP = { + POST: 'CREATE', + GET: 'READ', + PUT: 'UPDATE', + PATCH: 'UPDATE', + DELETE: 'DELETE', +}; + +/** + * @param {string} name + * @return {import("express").RequestHandler} + */ +function checkCrudPermissions(name) { + return (req, res, next) => { + const permissionName = `${METHOD_MAP[req.method]}_${name.toUpperCase()}`; + checkPermissions(permissionName)(req, res, next); + }; +} + +module.exports = { + checkPermissions, + checkCrudPermissions, +}; diff --git a/backend/src/middlewares/upload.js b/backend/src/middlewares/upload.js new file mode 100644 index 0000000..6d88c73 --- /dev/null +++ b/backend/src/middlewares/upload.js @@ -0,0 +1,11 @@ +const util = require('util'); +const Multer = require('multer'); +const maxSize = 10 * 1024 * 1024; + +let processFile = Multer({ + storage: Multer.memoryStorage(), + limits: { fileSize: maxSize }, +}).single('file'); + +let processFileMiddleware = util.promisify(processFile); +module.exports = processFileMiddleware; diff --git a/backend/src/routes/access_logs.js b/backend/src/routes/access_logs.js new file mode 100644 index 0000000..0cdc4f4 --- /dev/null +++ b/backend/src/routes/access_logs.js @@ -0,0 +1,438 @@ +const express = require('express'); + +const Access_logsService = require('../services/access_logs'); +const Access_logsDBApi = require('../db/api/access_logs'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('access_logs')); + +/** + * @swagger + * components: + * schemas: + * Access_logs: + * type: object + * properties: + + */ + +/** + * @swagger + * tags: + * name: Access_logs + * description: The Access_logs managing API + */ + +/** + * @swagger + * /api/access_logs: + * post: + * security: + * - bearerAuth: [] + * tags: [Access_logs] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Access_logs" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Access_logs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Access_logsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Access_logs] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Access_logs" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Access_logs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Access_logsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/access_logs/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Access_logs] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Access_logs" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Access_logs" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Access_logsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/access_logs/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Access_logs] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Access_logs" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Access_logsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/access_logs/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Access_logs] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Access_logs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Access_logsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/access_logs: + * get: + * security: + * - bearerAuth: [] + * tags: [Access_logs] + * summary: Get all access_logs + * description: Get all access_logs + * responses: + * 200: + * description: Access_logs list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Access_logs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const currentUser = req.currentUser; + const payload = await Access_logsDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'access_time']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/access_logs/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Access_logs] + * summary: Count all access_logs + * description: Count all access_logs + * responses: + * 200: + * description: Access_logs count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Access_logs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Access_logsDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/access_logs/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Access_logs] + * summary: Find all access_logs that match search criteria + * description: Find all access_logs that match search criteria + * responses: + * 200: + * description: Access_logs list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Access_logs" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Access_logsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/access_logs/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Access_logs] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Access_logs" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Access_logsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js new file mode 100644 index 0000000..b4edb29 --- /dev/null +++ b/backend/src/routes/auth.js @@ -0,0 +1,268 @@ +const express = require('express'); +const passport = require('passport'); + +const config = require('../config'); +const AuthService = require('../services/auth'); +const ForbiddenError = require('../services/notifications/errors/forbidden'); +const EmailSender = require('../services/email'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +/** + * @swagger + * components: + * schemas: + * Auth: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * default: admin@flatlogic.com + * description: User email + * password: + * type: string + * default: password + * description: User password + */ + +/** + * @swagger + * tags: + * name: Auth + * description: Authorization operations + */ + +/** + * @swagger + * /api/auth/signin/local: + * post: + * tags: [Auth] + * summary: Logs user into the system + * description: Logs user into the system + * requestBody: + * description: Set valid user email and password + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Auth" + * responses: + * 200: + * description: Successful login + * 400: + * description: Invalid username/password supplied + * x-codegen-request-body-name: body + */ + +router.post( + '/signin/local', + wrapAsync(async (req, res) => { + const payload = await AuthService.signin( + req.body.email, + req.body.password, + req, + ); + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/auth/me: + * get: + * security: + * - bearerAuth: [] + * tags: [Auth] + * summary: Get current authorized user info + * description: Get current authorized user info + * responses: + * 200: + * description: Successful retrieval of current authorized user data + * 400: + * description: Invalid username/password supplied + * x-codegen-request-body-name: body + */ + +router.get( + '/me', + passport.authenticate('jwt', { session: false }), + (req, res) => { + if (!req.currentUser || !req.currentUser.id) { + throw new ForbiddenError(); + } + + const payload = req.currentUser; + delete payload.password; + res.status(200).send(payload); + }, +); + +router.put( + '/password-reset', + wrapAsync(async (req, res) => { + const payload = await AuthService.passwordReset( + req.body.token, + req.body.password, + req, + ); + res.status(200).send(payload); + }), +); + +router.put( + '/password-update', + passport.authenticate('jwt', { session: false }), + wrapAsync(async (req, res) => { + const payload = await AuthService.passwordUpdate( + req.body.currentPassword, + req.body.newPassword, + req, + ); + res.status(200).send(payload); + }), +); + +router.post( + '/send-email-address-verification-email', + passport.authenticate('jwt', { session: false }), + wrapAsync(async (req, res) => { + if (!req.currentUser) { + throw new ForbiddenError(); + } + + await AuthService.sendEmailAddressVerificationEmail(req.currentUser.email); + const payload = true; + res.status(200).send(payload); + }), +); + +router.post( + '/send-password-reset-email', + wrapAsync(async (req, res) => { + const link = new URL(req.headers.referer); + await AuthService.sendPasswordResetEmail( + req.body.email, + 'register', + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/auth/signup: + * post: + * tags: [Auth] + * summary: Register new user into the system + * description: Register new user into the system + * requestBody: + * description: Set valid user email and password + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Auth" + * responses: + * 200: + * description: New user successfully signed up + * 400: + * description: Invalid username/password supplied + * 500: + * description: Some server error + * x-codegen-request-body-name: body + */ + +router.post( + '/signup', + wrapAsync(async (req, res) => { + const link = new URL(req.headers.referer); + const payload = await AuthService.signup( + req.body.email, + req.body.password, + + req, + link.host, + ); + res.status(200).send(payload); + }), +); + +router.put( + '/profile', + passport.authenticate('jwt', { session: false }), + wrapAsync(async (req, res) => { + if (!req.currentUser || !req.currentUser.id) { + throw new ForbiddenError(); + } + + await AuthService.updateProfile(req.body.profile, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +router.put( + '/verify-email', + wrapAsync(async (req, res) => { + const payload = await AuthService.verifyEmail( + req.body.token, + req, + req.headers.referer, + ); + res.status(200).send(payload); + }), +); + +router.get('/email-configured', (req, res) => { + const payload = EmailSender.isConfigured; + res.status(200).send(payload); +}); + +router.get('/signin/google', (req, res, next) => { + passport.authenticate('google', { + scope: ['profile', 'email'], + state: req.query.app, + })(req, res, next); +}); + +router.get( + '/signin/google/callback', + passport.authenticate('google', { + failureRedirect: '/login', + session: false, + }), + + function (req, res) { + socialRedirect(res, req.query.state, req.user.token, config); + }, +); + +router.get('/signin/microsoft', (req, res, next) => { + passport.authenticate('microsoft', { + scope: ['https://graph.microsoft.com/user.read openid'], + state: req.query.app, + })(req, res, next); +}); + +router.get( + '/signin/microsoft/callback', + passport.authenticate('microsoft', { + failureRedirect: '/login', + session: false, + }), + function (req, res) { + socialRedirect(res, req.query.state, req.user.token, config); + }, +); + +router.use('/', require('../helpers').commonErrorHandler); + +function socialRedirect(res, state, token, config) { + res.redirect(config.uiUrl + '/login?token=' + token); +} + +module.exports = router; diff --git a/backend/src/routes/contactForm.js b/backend/src/routes/contactForm.js new file mode 100644 index 0000000..3249987 --- /dev/null +++ b/backend/src/routes/contactForm.js @@ -0,0 +1,33 @@ +const express = require('express'); +const router = express.Router(); +const EmailSender = require('../services/email'); + +router.post('/send', async (req, res) => { + try { + const { email, subject, message } = req.body; + + if (!email || !subject || !message) { + return res.status(400).json({ error: 'All fields are required' }); + } + + const emailSender = new EmailSender({ + to: 'seatrend84@gmail.com', + subject: subject, + html: () => ` +

From: ${email}

+

Subject: ${subject}

+

Message:

+

${message}

+ `, + text: () => `From: ${email}\nSubject: ${subject}\nMessage:\n${message}`, + }); + + await emailSender.send(); + res.status(200).json({ message: 'Email sent successfully' }); + } catch (error) { + console.error('Error sending email:', error); + res.status(500).json({ error: 'Error sending email' }); + } +}); + +module.exports = router; diff --git a/backend/src/routes/file.js b/backend/src/routes/file.js new file mode 100644 index 0000000..e98d04c --- /dev/null +++ b/backend/src/routes/file.js @@ -0,0 +1,40 @@ +const express = require('express'); +const config = require('../config'); +const path = require('path'); +const passport = require('passport'); +const services = require('../services/file'); +const router = express.Router(); + +router.get('/download', (req, res) => { + if ( + process.env.NODE_ENV == 'production' || + process.env.NEXT_PUBLIC_BACK_API + ) { + services.downloadGCloud(req, res); + } else { + services.downloadLocal(req, res); + } +}); + +router.post( + '/upload/:table/:field', + passport.authenticate('jwt', { session: false }), + (req, res) => { + const fileName = `${req.params.table}/${req.params.field}`; + + if ( + process.env.NODE_ENV == 'production' || + process.env.NEXT_PUBLIC_BACK_API + ) { + services.uploadGCloud(fileName, req, res); + } else { + services.uploadLocal(fileName, { + entity: null, + maxFileSize: 10 * 1024 * 1024, + folderIncludesAuthenticationUid: false, + })(req, res); + } + }, +); + +module.exports = router; diff --git a/backend/src/routes/openai.js b/backend/src/routes/openai.js new file mode 100644 index 0000000..341fe0a --- /dev/null +++ b/backend/src/routes/openai.js @@ -0,0 +1,180 @@ +const express = require('express'); +const db = require('../db/models'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); +const sjs = require('sequelize-json-schema'); +const { getWidget } = require('../services/openai'); +const RolesService = require('../services/roles'); +const RolesDBApi = require('../db/api/roles'); + +/** + * @swagger + * /api/roles/roles-info/{infoId}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Remove role information by ID + * description: Remove specific role information by ID + * parameters: + * - in: path + * name: infoId + * description: ID of role information to remove + * required: true + * schema: + * type: string + * - in: query + * name: userId + * description: ID of the user + * required: true + * schema: + * type: string + * - in: query + * name: key + * description: Key of the role information to remove + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Role information successfully removed + * content: + * application/json: + * schema: + * type: object + * properties: + * user: + * type: string + * description: The user information + * 400: + * description: Invalid ID or key supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Role not found + * 500: + * description: Some server error + */ + +router.delete( + '/roles-info/:infoId', + wrapAsync(async (req, res) => { + const role = await RolesService.removeRoleInfoById( + req.query.infoId, + req.query.roleId, + req.query.key, + req.currentUser, + ); + + res.status(200).send(role); + }), +); + +/** + * @swagger + * /api/roles/role-info/{roleId}: + * get: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Get role information by key + * description: Get specific role information by key + * parameters: + * - in: path + * name: roleId + * description: ID of role to get information for + * required: true + * schema: + * type: string + * - in: query + * name: key + * description: Key of the role information to retrieve + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Role information successfully received + * content: + * application/json: + * schema: + * type: object + * properties: + * info: + * type: string + * description: The role information + * 400: + * description: Invalid ID or key supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Role not found + * 500: + * description: Some server error + */ + +router.get( + '/info-by-key', + wrapAsync(async (req, res) => { + const roleId = req.query.roleId; + const key = req.query.key; + const currentUser = req.currentUser; + let info = await RolesService.getRoleInfoByKey(key, roleId, currentUser); + const role = await RolesDBApi.findBy({ id: roleId }); + if (!role?.role_customization) { + await Promise.all( + ['pie', 'bar'].map(async (e) => { + const schema = await sjs.getSequelizeSchema(db.sequelize, {}); + const payload = { + description: `Create some cool ${e} chart`, + modelDefinition: schema.definitions, + }; + const widgetId = await getWidget(payload, currentUser?.id, roleId); + if (widgetId) { + await RolesService.addRoleInfo( + roleId, + currentUser?.id, + 'widgets', + widgetId, + req.currentUser, + ); + } + }), + ); + info = await RolesService.getRoleInfoByKey(key, roleId, currentUser); + } + res.status(200).send(info); + }), +); + +router.post( + '/create_widget', + wrapAsync(async (req, res) => { + const { description, userId, roleId } = req.body; + + const currentUser = req.currentUser; + const schema = await sjs.getSequelizeSchema(db.sequelize, {}); + const payload = { + description, + modelDefinition: schema.definitions, + }; + + const widgetId = await getWidget(payload, userId, roleId); + + if (widgetId) { + await RolesService.addRoleInfo( + roleId, + userId, + 'widgets', + widgetId, + currentUser, + ); + + return res.status(200).send(widgetId); + } else { + return res.status(400).send(widgetId); + } + }), +); + +module.exports = router; diff --git a/backend/src/routes/organizationLogin.js b/backend/src/routes/organizationLogin.js new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/routes/permissions.js b/backend/src/routes/permissions.js new file mode 100644 index 0000000..c3f1685 --- /dev/null +++ b/backend/src/routes/permissions.js @@ -0,0 +1,442 @@ +const express = require('express'); + +const PermissionsService = require('../services/permissions'); +const PermissionsDBApi = require('../db/api/permissions'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('permissions')); + +/** + * @swagger + * components: + * schemas: + * Permissions: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: Permissions + * description: The Permissions managing API + */ + +/** + * @swagger + * /api/permissions: + * post: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Permissions" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Permissions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await PermissionsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Permissions" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Permissions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await PermissionsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/permissions/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Permissions" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Permissions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await PermissionsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/permissions/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Permissions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await PermissionsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/permissions/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Permissions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await PermissionsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/permissions: + * get: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Get all permissions + * description: Get all permissions + * responses: + * 200: + * description: Permissions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Permissions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const currentUser = req.currentUser; + const payload = await PermissionsDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/permissions/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Count all permissions + * description: Count all permissions + * responses: + * 200: + * description: Permissions count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Permissions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await PermissionsDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/permissions/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Find all permissions that match search criteria + * description: Find all permissions that match search criteria + * responses: + * 200: + * description: Permissions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Permissions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await PermissionsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/permissions/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Permissions] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Permissions" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await PermissionsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/pexels.js b/backend/src/routes/pexels.js new file mode 100644 index 0000000..ab2100f --- /dev/null +++ b/backend/src/routes/pexels.js @@ -0,0 +1,106 @@ +const express = require('express'); +const router = express.Router(); +const { pexelsKey, pexelsQuery } = require('../config'); +const fetch = require('node-fetch'); + +const KEY = pexelsKey; + +router.get('/image', async (req, res) => { + const headers = { + Authorization: `${KEY}`, + }; + const query = pexelsQuery || 'nature'; + const orientation = 'portrait'; + const perPage = 1; + const url = `https://api.pexels.com/v1/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`; + + try { + const response = await fetch(url, { headers }); + const data = await response.json(); + res.status(200).json(data.photos[0]); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch image' }); + } +}); + +router.get('/video', async (req, res) => { + const headers = { + Authorization: `${KEY}`, + }; + const query = pexelsQuery || 'nature'; + const orientation = 'portrait'; + const perPage = 1; + const url = `https://api.pexels.com/videos/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`; + + try { + const response = await fetch(url, { headers }); + const data = await response.json(); + res.status(200).json(data.videos[0]); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch video' }); + } +}); + +router.get('/multiple-images', async (req, res) => { + const headers = { + Authorization: `${KEY}`, + }; + + const queries = req.query.queries + ? req.query.queries.split(',') + : ['home', 'apple', 'pizza', 'mountains', 'cat']; + const orientation = 'square'; + const perPage = 1; + + const fallbackImage = { + src: 'https://images.pexels.com/photos/8199252/pexels-photo-8199252.jpeg', + photographer: 'Yan Krukau', + photographer_url: 'https://www.pexels.com/@yankrukov', + }; + const fetchFallbackImage = async () => { + try { + const response = await fetch('https://picsum.photos/600'); + return { + src: response.url, + photographer: 'Random Picsum', + photographer_url: 'https://picsum.photos/', + }; + } catch (error) { + return fallbackImage; + } + }; + const fetchImage = async (query) => { + const url = `https://api.pexels.com/v1/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`; + const response = await fetch(url, { headers }); + const data = await response.json(); + return data.photos[0] || null; + }; + + const imagePromises = queries.map((query) => fetchImage(query)); + const imagesResults = await Promise.allSettled(imagePromises); + + const formattedImages = await Promise.all( + imagesResults.map(async (result) => { + if (result.status === 'fulfilled' && result.value) { + const image = result.value; + return { + src: image.src?.original || fallbackImage.src, + photographer: image.photographer || fallbackImage.photographer, + photographer_url: + image.photographer_url || fallbackImage.photographer_url, + }; + } else { + const fallback = await fetchFallbackImage(); + return { + src: fallback.src || '', + photographer: fallback.photographer || 'Unknown', + photographer_url: fallback.photographer_url || '', + }; + } + }), + ); + + res.json(formattedImages); +}); + +module.exports = router; diff --git a/backend/src/routes/qr_codes.js b/backend/src/routes/qr_codes.js new file mode 100644 index 0000000..809edb7 --- /dev/null +++ b/backend/src/routes/qr_codes.js @@ -0,0 +1,438 @@ +const express = require('express'); + +const Qr_codesService = require('../services/qr_codes'); +const Qr_codesDBApi = require('../db/api/qr_codes'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('qr_codes')); + +/** + * @swagger + * components: + * schemas: + * Qr_codes: + * type: object + * properties: + + * code: + * type: string + * default: code + + */ + +/** + * @swagger + * tags: + * name: Qr_codes + * description: The Qr_codes managing API + */ + +/** + * @swagger + * /api/qr_codes: + * post: + * security: + * - bearerAuth: [] + * tags: [Qr_codes] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Qr_codes" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Qr_codes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Qr_codesService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Qr_codes] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Qr_codes" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Qr_codes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Qr_codesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/qr_codes/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Qr_codes] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Qr_codes" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Qr_codes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await Qr_codesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/qr_codes/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Qr_codes] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Qr_codes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Qr_codesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/qr_codes/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Qr_codes] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Qr_codes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Qr_codesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/qr_codes: + * get: + * security: + * - bearerAuth: [] + * tags: [Qr_codes] + * summary: Get all qr_codes + * description: Get all qr_codes + * responses: + * 200: + * description: Qr_codes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Qr_codes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const currentUser = req.currentUser; + const payload = await Qr_codesDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'code', 'valid_from', 'valid_until']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/qr_codes/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Qr_codes] + * summary: Count all qr_codes + * description: Count all qr_codes + * responses: + * 200: + * description: Qr_codes count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Qr_codes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Qr_codesDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/qr_codes/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Qr_codes] + * summary: Find all qr_codes that match search criteria + * description: Find all qr_codes that match search criteria + * responses: + * 200: + * description: Qr_codes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Qr_codes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Qr_codesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/qr_codes/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Qr_codes] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Qr_codes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await Qr_codesDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/roles.js b/backend/src/routes/roles.js new file mode 100644 index 0000000..5f9cbf9 --- /dev/null +++ b/backend/src/routes/roles.js @@ -0,0 +1,433 @@ +const express = require('express'); + +const RolesService = require('../services/roles'); +const RolesDBApi = require('../db/api/roles'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('roles')); + +/** + * @swagger + * components: + * schemas: + * Roles: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: Roles + * description: The Roles managing API + */ + +/** + * @swagger + * /api/roles: + * post: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Roles" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Roles" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await RolesService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Roles" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Roles" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await RolesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/roles/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Roles" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Roles" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await RolesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/roles/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Roles" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await RolesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/roles/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Roles" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await RolesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/roles: + * get: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Get all roles + * description: Get all roles + * responses: + * 200: + * description: Roles list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Roles" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const currentUser = req.currentUser; + const payload = await RolesDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/roles/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Count all roles + * description: Count all roles + * responses: + * 200: + * description: Roles count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Roles" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await RolesDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/roles/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Find all roles that match search criteria + * description: Find all roles that match search criteria + * responses: + * 200: + * description: Roles list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Roles" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await RolesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/roles/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Roles] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Roles" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await RolesDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/search.js b/backend/src/routes/search.js new file mode 100644 index 0000000..b848adb --- /dev/null +++ b/backend/src/routes/search.js @@ -0,0 +1,54 @@ +const express = require('express'); +const SearchService = require('../services/search'); + +const router = express.Router(); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); +router.use(checkCrudPermissions('search')); + +/** + * @swagger + * path: + * /api/search: + * post: + * summary: Search + * description: Search results across multiple tables + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * searchQuery: + * type: string + * required: + * - searchQuery + * responses: + * 200: + * description: Successful request + * 400: + * description: Invalid request + * 500: + * description: Internal server error + */ + +router.post('/', async (req, res) => { + const { searchQuery } = req.body; + + if (!searchQuery) { + return res.status(400).json({ error: 'Please enter a search query' }); + } + + try { + const foundMatches = await SearchService.search( + searchQuery, + req.currentUser, + ); + res.json(foundMatches); + } catch (error) { + console.error('Internal Server Error', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +module.exports = router; diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js new file mode 100644 index 0000000..a6d214f --- /dev/null +++ b/backend/src/routes/users.js @@ -0,0 +1,444 @@ +const express = require('express'); + +const UsersService = require('../services/users'); +const UsersDBApi = require('../db/api/users'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('users')); + +/** + * @swagger + * components: + * schemas: + * Users: + * type: object + * properties: + + * firstName: + * type: string + * default: firstName + * lastName: + * type: string + * default: lastName + * phoneNumber: + * type: string + * default: phoneNumber + * email: + * type: string + * default: email + + */ + +/** + * @swagger + * tags: + * name: Users + * description: The Users managing API + */ + +/** + * @swagger + * /api/users: + * post: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Users" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await UsersService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Users" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await UsersService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/users/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Users" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await UsersService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/users/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await UsersService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/users/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await UsersService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/users: + * get: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Get all users + * description: Get all users + * responses: + * 200: + * description: Users list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const currentUser = req.currentUser; + const payload = await UsersDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'firstName', 'lastName', 'phoneNumber', 'email']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/users/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Count all users + * description: Count all users + * responses: + * 200: + * description: Users count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await UsersDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/users/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Find all users that match search criteria + * description: Find all users that match search criteria + * responses: + * 200: + * description: Users list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await UsersDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/users/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await UsersDBApi.findBy({ id: req.params.id }); + + delete payload.password; + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/videos.js b/backend/src/routes/videos.js new file mode 100644 index 0000000..8c27d24 --- /dev/null +++ b/backend/src/routes/videos.js @@ -0,0 +1,437 @@ +const express = require('express'); + +const VideosService = require('../services/videos'); +const VideosDBApi = require('../db/api/videos'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('videos')); + +/** + * @swagger + * components: + * schemas: + * Videos: + * type: object + * properties: + + * title: + * type: string + * default: title + * url: + * type: string + * default: url + + * + */ + +/** + * @swagger + * tags: + * name: Videos + * description: The Videos managing API + */ + +/** + * @swagger + * /api/videos: + * post: + * security: + * - bearerAuth: [] + * tags: [Videos] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Videos" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Videos" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await VideosService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Videos] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Videos" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Videos" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await VideosService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/videos/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Videos] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Videos" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Videos" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await VideosService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/videos/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Videos] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Videos" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await VideosService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/videos/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Videos] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Videos" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await VideosService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/videos: + * get: + * security: + * - bearerAuth: [] + * tags: [Videos] + * summary: Get all videos + * description: Get all videos + * responses: + * 200: + * description: Videos list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Videos" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const currentUser = req.currentUser; + const payload = await VideosDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'title', 'url']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/videos/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Videos] + * summary: Count all videos + * description: Count all videos + * responses: + * 200: + * description: Videos count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Videos" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await VideosDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/videos/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Videos] + * summary: Find all videos that match search criteria + * description: Find all videos that match search criteria + * responses: + * 200: + * description: Videos list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Videos" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await VideosDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/videos/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Videos] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Videos" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await VideosDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/access_logs.js b/backend/src/services/access_logs.js new file mode 100644 index 0000000..6b49dae --- /dev/null +++ b/backend/src/services/access_logs.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const Access_logsDBApi = require('../db/api/access_logs'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Access_logsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Access_logsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Access_logsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let access_logs = await Access_logsDBApi.findBy({ id }, { transaction }); + + if (!access_logs) { + throw new ValidationError('access_logsNotFound'); + } + + const updatedAccess_logs = await Access_logsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedAccess_logs; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Access_logsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Access_logsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/auth.js b/backend/src/services/auth.js new file mode 100644 index 0000000..2a5ccc2 --- /dev/null +++ b/backend/src/services/auth.js @@ -0,0 +1,226 @@ +const UsersDBApi = require('../db/api/users'); +const ValidationError = require('./notifications/errors/validation'); +const ForbiddenError = require('./notifications/errors/forbidden'); +const bcrypt = require('bcrypt'); +const EmailAddressVerificationEmail = require('./email/list/addressVerification'); +const InvitationEmail = require('./email/list/invitation'); +const PasswordResetEmail = require('./email/list/passwordReset'); +const EmailSender = require('./email'); +const config = require('../config'); +const helpers = require('../helpers'); + +class Auth { + static async signup(email, password, options = {}, host) { + const user = await UsersDBApi.findBy({ email }); + + const hashedPassword = await bcrypt.hash( + password, + config.bcrypt.saltRounds, + ); + + if (user) { + if (user.authenticationUid) { + throw new ValidationError('auth.emailAlreadyInUse'); + } + + if (user.disabled) { + throw new ValidationError('auth.userDisabled'); + } + + await UsersDBApi.updatePassword(user.id, hashedPassword, options); + + if (EmailSender.isConfigured) { + await this.sendEmailAddressVerificationEmail(user.email, host); + } + + const data = { + user: { + id: user.id, + email: user.email, + }, + }; + + return helpers.jwtSign(data); + } + + const newUser = await UsersDBApi.createFromAuth( + { + firstName: email.split('@')[0], + password: hashedPassword, + email: email, + }, + options, + ); + + if (EmailSender.isConfigured) { + await this.sendEmailAddressVerificationEmail(newUser.email, host); + } + + const data = { + user: { + id: newUser.id, + email: newUser.email, + }, + }; + + return helpers.jwtSign(data); + } + + static async signin(email, password, options = {}) { + const user = await UsersDBApi.findBy({ email }); + + if (!user) { + throw new ValidationError('auth.userNotFound'); + } + + if (user.disabled) { + throw new ValidationError('auth.userDisabled'); + } + + if (!user.password) { + throw new ValidationError('auth.wrongPassword'); + } + + if (!EmailSender.isConfigured) { + user.emailVerified = true; + } + + if (!user.emailVerified) { + throw new ValidationError('auth.userNotVerified'); + } + + const passwordsMatch = await bcrypt.compare(password, user.password); + + if (!passwordsMatch) { + throw new ValidationError('auth.wrongPassword'); + } + + const data = { + user: { + id: user.id, + email: user.email, + }, + }; + + return helpers.jwtSign(data); + } + + static async sendEmailAddressVerificationEmail(email, host) { + let link; + try { + const token = await UsersDBApi.generateEmailVerificationToken(email); + link = `${host}/verify-email?token=${token}`; + } catch (error) { + console.error(error); + throw new ValidationError('auth.emailAddressVerificationEmail.error'); + } + + const emailAddressVerificationEmail = new EmailAddressVerificationEmail( + email, + link, + ); + + return new EmailSender(emailAddressVerificationEmail).send(); + } + + static async sendPasswordResetEmail(email, type = 'register', host) { + let link; + + try { + const token = await UsersDBApi.generatePasswordResetToken(email); + link = `${host}/password-reset?token=${token}`; + } catch (error) { + console.error(error); + throw new ValidationError('auth.passwordReset.error'); + } + + let passwordResetEmail; + if (type === 'register') { + passwordResetEmail = new PasswordResetEmail(email, link); + } + if (type === 'invitation') { + passwordResetEmail = new InvitationEmail(email, link); + } + + return new EmailSender(passwordResetEmail).send(); + } + + static async verifyEmail(token, options = {}) { + const user = await UsersDBApi.findByEmailVerificationToken(token, options); + + if (!user) { + throw new ValidationError( + 'auth.emailAddressVerificationEmail.invalidToken', + ); + } + + return UsersDBApi.markEmailVerified(user.id, options); + } + + static async passwordUpdate(currentPassword, newPassword, options) { + const currentUser = options.currentUser || null; + if (!currentUser) { + throw new ForbiddenError(); + } + + const currentPasswordMatch = await bcrypt.compare( + currentPassword, + currentUser.password, + ); + + if (!currentPasswordMatch) { + throw new ValidationError('auth.wrongPassword'); + } + + const newPasswordMatch = await bcrypt.compare( + newPassword, + currentUser.password, + ); + + if (newPasswordMatch) { + throw new ValidationError('auth.passwordUpdate.samePassword'); + } + + const hashedPassword = await bcrypt.hash( + newPassword, + config.bcrypt.saltRounds, + ); + + return UsersDBApi.updatePassword(currentUser.id, hashedPassword, options); + } + + static async passwordReset(token, password, options = {}) { + const user = await UsersDBApi.findByPasswordResetToken(token, options); + + if (!user) { + throw new ValidationError('auth.passwordReset.invalidToken'); + } + + const hashedPassword = await bcrypt.hash( + password, + config.bcrypt.saltRounds, + ); + + return UsersDBApi.updatePassword(user.id, hashedPassword, options); + } + + static async updateProfile(data, currentUser) { + let transaction = await db.sequelize.transaction(); + + try { + await UsersDBApi.findBy({ id: currentUser.id }, { transaction }); + + await UsersDBApi.update(currentUser.id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +} + +module.exports = Auth; diff --git a/backend/src/services/email/htmlTemplates/addressVerification/emailAddressVerification.html b/backend/src/services/email/htmlTemplates/addressVerification/emailAddressVerification.html new file mode 100644 index 0000000..95d8b3f --- /dev/null +++ b/backend/src/services/email/htmlTemplates/addressVerification/emailAddressVerification.html @@ -0,0 +1,52 @@ + + + + + + + + + diff --git a/backend/src/services/email/htmlTemplates/invitation/invitationTemplate.html b/backend/src/services/email/htmlTemplates/invitation/invitationTemplate.html new file mode 100644 index 0000000..e685483 --- /dev/null +++ b/backend/src/services/email/htmlTemplates/invitation/invitationTemplate.html @@ -0,0 +1,56 @@ + + + + + + + + + diff --git a/backend/src/services/email/htmlTemplates/passwordReset/passwordResetEmail.html b/backend/src/services/email/htmlTemplates/passwordReset/passwordResetEmail.html new file mode 100644 index 0000000..c77f215 --- /dev/null +++ b/backend/src/services/email/htmlTemplates/passwordReset/passwordResetEmail.html @@ -0,0 +1,55 @@ + + + + + + + + + diff --git a/backend/src/services/email/index.js b/backend/src/services/email/index.js new file mode 100644 index 0000000..fa7f3c7 --- /dev/null +++ b/backend/src/services/email/index.js @@ -0,0 +1,41 @@ +const config = require('../../config'); +const assert = require('assert'); +const nodemailer = require('nodemailer'); + +module.exports = class EmailSender { + constructor(email) { + this.email = email; + } + + async send() { + assert(this.email, 'email is required'); + assert(this.email.to, 'email.to is required'); + assert(this.email.subject, 'email.subject is required'); + assert(this.email.html, 'email.html is required'); + + const htmlContent = await this.email.html(); + + const transporter = nodemailer.createTransport(this.transportConfig); + + const mailOptions = { + from: this.from, + to: this.email.to, + subject: this.email.subject, + html: htmlContent, + }; + + return transporter.sendMail(mailOptions); + } + + static get isConfigured() { + return !!config.email?.auth?.pass && !!config.email?.auth?.user; + } + + get transportConfig() { + return config.email; + } + + get from() { + return config.email.from; + } +}; diff --git a/backend/src/services/email/list/addressVerification.js b/backend/src/services/email/list/addressVerification.js new file mode 100644 index 0000000..89be6d3 --- /dev/null +++ b/backend/src/services/email/list/addressVerification.js @@ -0,0 +1,41 @@ +const { getNotification } = require('../../notifications/helpers'); +const fs = require('fs').promises; +const path = require('path'); + +module.exports = class EmailAddressVerificationEmail { + constructor(to, link) { + this.to = to; + this.link = link; + } + + get subject() { + return getNotification( + 'emails.emailAddressVerification.subject', + getNotification('app.title'), + ); + } + + async html() { + try { + const templatePath = path.join( + __dirname, + '../../email/htmlTemplates/addressVerification/emailAddressVerification.html', + ); + + const template = await fs.readFile(templatePath, 'utf8'); + + const appTitle = getNotification('app.title'); + const signupUrl = this.link; + + let html = template + .replace(/{appTitle}/g, appTitle) + .replace(/{signupUrl}/g, signupUrl) + .replace(/{to}/g, this.to); + + return html; + } catch (error) { + console.error('Error generating invitation email HTML:', error); + throw error; + } + } +}; diff --git a/backend/src/services/email/list/invitation.js b/backend/src/services/email/list/invitation.js new file mode 100644 index 0000000..d2afc1e --- /dev/null +++ b/backend/src/services/email/list/invitation.js @@ -0,0 +1,41 @@ +const fs = require('fs').promises; +const path = require('path'); +const { getNotification } = require('../../notifications/helpers'); + +module.exports = class InvitationEmail { + constructor(to, host) { + this.to = to; + this.host = host; + } + + get subject() { + return getNotification( + 'emails.invitation.subject', + getNotification('app.title'), + ); + } + + async html() { + try { + const templatePath = path.join( + __dirname, + '../../email/htmlTemplates/invitation/invitationTemplate.html', + ); + + const template = await fs.readFile(templatePath, 'utf8'); + + const appTitle = getNotification('app.title'); + const signupUrl = `${this.host}&invitation=true`; + + let html = template + .replace(/{appTitle}/g, appTitle) + .replace(/{signupUrl}/g, signupUrl) + .replace(/{to}/g, this.to); + + return html; + } catch (error) { + console.error('Error generating invitation email HTML:', error); + throw error; + } + } +}; diff --git a/backend/src/services/email/list/passwordReset.js b/backend/src/services/email/list/passwordReset.js new file mode 100644 index 0000000..68ba353 --- /dev/null +++ b/backend/src/services/email/list/passwordReset.js @@ -0,0 +1,42 @@ +const { getNotification } = require('../../notifications/helpers'); +const path = require('path'); +const { promises: fs } = require('fs'); + +module.exports = class PasswordResetEmail { + constructor(to, link) { + this.to = to; + this.link = link; + } + + get subject() { + return getNotification( + 'emails.passwordReset.subject', + getNotification('app.title'), + ); + } + + async html() { + try { + const templatePath = path.join( + __dirname, + '../../email/htmlTemplates/passwordReset/passwordResetEmail.html', + ); + + const template = await fs.readFile(templatePath, 'utf8'); + + const appTitle = getNotification('app.title'); + const resetUrl = this.link; + const accountName = this.to; + + let html = template + .replace(/{appTitle}/g, appTitle) + .replace(/{resetUrl}/g, resetUrl) + .replace(/{accountName}/g, accountName); + + return html; + } catch (error) { + console.error('Error generating invitation email HTML:', error); + throw error; + } + } +}; diff --git a/backend/src/services/file.js b/backend/src/services/file.js new file mode 100644 index 0000000..cb08164 --- /dev/null +++ b/backend/src/services/file.js @@ -0,0 +1,202 @@ +const formidable = require('formidable'); +const fs = require('fs'); +const config = require('../config'); +const path = require('path'); +const { format } = require('util'); + +const ensureDirectoryExistence = (filePath) => { + const dirname = path.dirname(filePath); + + if (fs.existsSync(dirname)) { + return true; + } + + ensureDirectoryExistence(dirname); + fs.mkdirSync(dirname); +}; + +const uploadLocal = ( + folder, + validations = { + entity: null, + maxFileSize: null, + folderIncludesAuthenticationUid: false, + }, +) => { + return (req, res) => { + if (!req.currentUser) { + res.sendStatus(403); + return; + } + + if (validations.entity) { + res.sendStatus(403); + return; + } + + if (validations.folderIncludesAuthenticationUid) { + folder = folder.replace(':userId', req.currentUser.authenticationUid); + if ( + !req.currentUser.authenticationUid || + !folder.includes(req.currentUser.authenticationUid) + ) { + res.sendStatus(403); + return; + } + } + + const form = new formidable.IncomingForm(); + form.uploadDir = config.uploadDir; + + if (validations && validations.maxFileSize) { + form.maxFileSize = validations.maxFileSize; + } + + form.parse(req, function (err, fields, files) { + const filename = String(fields.filename); + const fileTempUrl = files.file.path; + + if (!filename) { + fs.unlinkSync(fileTempUrl); + res.sendStatus(500); + return; + } + + const privateUrl = path.join(form.uploadDir, folder, filename); + ensureDirectoryExistence(privateUrl); + fs.renameSync(fileTempUrl, privateUrl); + res.sendStatus(200); + }); + + form.on('error', function (err) { + res.status(500).send(err); + }); + }; +}; + +const downloadLocal = async (req, res) => { + const privateUrl = req.query.privateUrl; + if (!privateUrl) { + return res.sendStatus(404); + } + res.download(path.join(config.uploadDir, privateUrl)); +}; + +const initGCloud = () => { + const processFile = require('../middlewares/upload'); + const { Storage } = require('@google-cloud/storage'); + + const crypto = require('crypto'); + const hash = config.gcloud.hash; + + const privateKey = process.env.GC_PRIVATE_KEY.replace(/\\\n/g, '\n'); + + const storage = new Storage({ + projectId: process.env.GC_PROJECT_ID, + credentials: { + client_email: process.env.GC_CLIENT_EMAIL, + private_key: privateKey, + }, + }); + + const bucket = storage.bucket(config.gcloud.bucket); + return { hash, bucket, processFile }; +}; + +const uploadGCloud = async (folder, req, res) => { + try { + const { hash, bucket, processFile } = initGCloud(); + await processFile(req, res); + let buffer = await req.file.buffer; + let filename = await req.body.filename; + + if (!req.file) { + return res.status(400).send({ message: 'Please upload a file!' }); + } + + let path = `${hash}/${folder}/${filename}`; + let blob = bucket.file(path); + + console.log(path); + + const blobStream = blob.createWriteStream({ + resumable: false, + }); + + blobStream.on('error', (err) => { + console.log('Upload error'); + console.log(err.message); + res.status(500).send({ message: err.message }); + }); + + console.log(`https://storage.googleapis.com/${bucket.name}/${blob.name}`); + + blobStream.on('finish', async (data) => { + const publicUrl = format( + `https://storage.googleapis.com/${bucket.name}/${blob.name}`, + ); + + res.status(200).send({ + message: 'Uploaded the file successfully: ' + path, + url: publicUrl, + }); + }); + + blobStream.end(buffer); + } catch (err) { + console.log(err); + + res.status(500).send({ + message: `Could not upload the file. ${err}`, + }); + } +}; + +const downloadGCloud = async (req, res) => { + try { + const { hash, bucket, processFile } = initGCloud(); + + const privateUrl = await req.query.privateUrl; + const filePath = `${hash}/${privateUrl}`; + const file = bucket.file(filePath); + const fileExists = await file.exists(); + + if (fileExists[0]) { + const stream = file.createReadStream(); + stream.pipe(res); + } else { + res.status(404).send({ + message: 'Could not download the file. ' + err, + }); + } + } catch (err) { + res.status(404).send({ + message: 'Could not download the file. ' + err, + }); + } +}; + +const deleteGCloud = async (privateUrl) => { + try { + const { hash, bucket, processFile } = initGCloud(); + const filePath = `${hash}/${privateUrl}`; + + const file = bucket.file(filePath); + const fileExists = await file.exists(); + + if (fileExists[0]) { + file.delete(); + } + } catch (err) { + console.log(`Cannot find the file ${privateUrl}`); + } +}; + +module.exports = { + initGCloud, + uploadLocal, + downloadLocal, + deleteGCloud, + uploadGCloud, + downloadGCloud, +}; diff --git a/backend/src/services/notifications/errors/forbidden.js b/backend/src/services/notifications/errors/forbidden.js new file mode 100644 index 0000000..192fa10 --- /dev/null +++ b/backend/src/services/notifications/errors/forbidden.js @@ -0,0 +1,16 @@ +const { getNotification, isNotification } = require('../helpers'); + +module.exports = class ForbiddenError extends Error { + constructor(messageCode) { + let message; + + if (messageCode && isNotification(messageCode)) { + message = getNotification(messageCode); + } + + message = message || getNotification('errors.forbidden.message'); + + super(message); + this.code = 403; + } +}; diff --git a/backend/src/services/notifications/errors/validation.js b/backend/src/services/notifications/errors/validation.js new file mode 100644 index 0000000..464550c --- /dev/null +++ b/backend/src/services/notifications/errors/validation.js @@ -0,0 +1,16 @@ +const { getNotification, isNotification } = require('../helpers'); + +module.exports = class ValidationError extends Error { + constructor(messageCode) { + let message; + + if (messageCode && isNotification(messageCode)) { + message = getNotification(messageCode); + } + + message = message || getNotification('errors.validation.message'); + + super(message); + this.code = 400; + } +}; diff --git a/backend/src/services/notifications/helpers.js b/backend/src/services/notifications/helpers.js new file mode 100644 index 0000000..1c3a60f --- /dev/null +++ b/backend/src/services/notifications/helpers.js @@ -0,0 +1,30 @@ +const _get = require('lodash/get'); +const errors = require('./list'); + +function format(message, args) { + if (!message) { + return null; + } + + return message.replace(/{(\d+)}/g, function (match, number) { + return typeof args[number] != 'undefined' ? args[number] : match; + }); +} + +const isNotification = (key) => { + const message = _get(errors, key); + return !!message; +}; + +const getNotification = (key, ...args) => { + const message = _get(errors, key); + + if (!message) { + return key; + } + + return format(message, args); +}; + +exports.getNotification = getNotification; +exports.isNotification = isNotification; diff --git a/backend/src/services/notifications/list.js b/backend/src/services/notifications/list.js new file mode 100644 index 0000000..3cd26f1 --- /dev/null +++ b/backend/src/services/notifications/list.js @@ -0,0 +1,100 @@ +const errors = { + app: { + title: 'sev', + }, + + auth: { + userDisabled: 'Your account is disabled', + forbidden: 'Forbidden', + unauthorized: 'Unauthorized', + userNotFound: `Sorry, we don't recognize your credentials`, + wrongPassword: `Sorry, we don't recognize your credentials`, + weakPassword: 'This password is too weak', + emailAlreadyInUse: 'Email is already in use', + invalidEmail: 'Please provide a valid email', + passwordReset: { + invalidToken: 'Password reset link is invalid or has expired', + error: `Email not recognized`, + }, + passwordUpdate: { + samePassword: `You can't use the same password. Please create new password`, + }, + userNotVerified: `Sorry, your email has not been verified yet`, + emailAddressVerificationEmail: { + invalidToken: 'Email verification link is invalid or has expired', + error: `Email not recognized`, + }, + }, + + iam: { + errors: { + userAlreadyExists: 'User with this email already exists', + userNotFound: 'User not found', + disablingHimself: `You can't disable yourself`, + revokingOwnPermission: `You can't revoke your own owner permission`, + deletingHimself: `You can't delete yourself`, + emailRequired: 'Email is required', + }, + }, + + importer: { + errors: { + invalidFileEmpty: 'The file is empty', + invalidFileExcel: 'Only excel (.xlsx) files are allowed', + invalidFileUpload: + 'Invalid file. Make sure you are using the last version of the template.', + importHashRequired: 'Import hash is required', + importHashExistent: 'Data has already been imported', + userEmailMissing: 'Some items in the CSV do not have an email', + }, + }, + + errors: { + forbidden: { + message: 'Forbidden', + }, + validation: { + message: 'An error occurred', + }, + searchQueryRequired: { + message: 'Search query is required', + }, + }, + + emails: { + invitation: { + subject: `You've been invited to {0}`, + body: ` +

Hello,

+

You've been invited to {0} set password for your {1} account.

+

{2}

+

Thanks,

+

Your {0} team

+ `, + }, + emailAddressVerification: { + subject: `Verify your email for {0}`, + body: ` +

Hello,

+

Follow this link to verify your email address.

+

{0}

+

If you didn't ask to verify this address, you can ignore this email.

+

Thanks,

+

Your {1} team

+ `, + }, + passwordReset: { + subject: `Reset your password for {0}`, + body: ` +

Hello,

+

Follow this link to reset your {0} password for your {1} account.

+

{2}

+

If you didn't ask to reset your password, you can ignore this email.

+

Thanks,

+

Your {0} team

+ `, + }, + }, +}; + +module.exports = errors; diff --git a/backend/src/services/openai.js b/backend/src/services/openai.js new file mode 100644 index 0000000..947a3d1 --- /dev/null +++ b/backend/src/services/openai.js @@ -0,0 +1,22 @@ +const axios = require('axios'); +const { v4: uuid } = require('uuid'); +const RoleService = require('./roles'); +const config = require('../config'); + +module.exports = class OpenAiService { + static async getWidget(payload, userId, roleId) { + const response = await axios.post( + `${config.flHost}/${config.project_uuid}/project_customization_widgets.json`, + payload, + ); + + if (response.status >= 200 && response.status < 300) { + const { widget_id } = await response.data; + await RoleService.addRoleInfo(roleId, userId, 'widgets', widget_id); + return widget_id; + } else { + console.error('=======error=======', response.data); + return { value: null, error: response.data }; + } + } +}; diff --git a/backend/src/services/permissions.js b/backend/src/services/permissions.js new file mode 100644 index 0000000..ad78c26 --- /dev/null +++ b/backend/src/services/permissions.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const PermissionsDBApi = require('../db/api/permissions'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class PermissionsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await PermissionsDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await PermissionsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let permissions = await PermissionsDBApi.findBy({ id }, { transaction }); + + if (!permissions) { + throw new ValidationError('permissionsNotFound'); + } + + const updatedPermissions = await PermissionsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedPermissions; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await PermissionsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await PermissionsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/qr_codes.js b/backend/src/services/qr_codes.js new file mode 100644 index 0000000..59a69d9 --- /dev/null +++ b/backend/src/services/qr_codes.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const Qr_codesDBApi = require('../db/api/qr_codes'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Qr_codesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Qr_codesDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await Qr_codesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let qr_codes = await Qr_codesDBApi.findBy({ id }, { transaction }); + + if (!qr_codes) { + throw new ValidationError('qr_codesNotFound'); + } + + const updatedQr_codes = await Qr_codesDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedQr_codes; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Qr_codesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Qr_codesDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/roles.js b/backend/src/services/roles.js new file mode 100644 index 0000000..05b6fd2 --- /dev/null +++ b/backend/src/services/roles.js @@ -0,0 +1,430 @@ +const db = require('../db/models'); +const RolesDBApi = require('../db/api/roles'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +function buildWidgetResult(widget, queryResult, queryString) { + if (queryResult[0] && queryResult[0].length) { + const key = Object.keys(queryResult[0][0])[0]; + const value = + widget.widget_type === 'scalar' ? queryResult[0][0][key] : queryResult[0]; + const widgetData = JSON.parse(widget.data); + return { ...widget, ...widgetData, value, query: queryString }; + } else { + return { ...widget, value: [], query: queryString }; + } +} + +async function executeQuery(queryString, currentUser) { + try { + return await db.sequelize.query(queryString, { + replacements: { organizationId: currentUser.organizationId }, + }); + } catch (e) { + console.log(e); + return []; + } +} + +function insertWhereConditions(queryString, whereConditions) { + if (!whereConditions) return queryString; + + const whereIndex = queryString.toLowerCase().indexOf('where'); + const groupByIndex = queryString.toLowerCase().indexOf('group by'); + const insertIndex = + whereIndex === -1 + ? groupByIndex !== -1 + ? groupByIndex + : queryString.length + : whereIndex + 5; + + const prefix = queryString.substring(0, insertIndex); + const suffix = queryString.substring(insertIndex); + const conditionString = + whereIndex === -1 + ? ` WHERE ${whereConditions} ` + : ` ${whereConditions} AND `; + + return `${prefix}${conditionString}${suffix}`; +} + +function constructWhereConditions(mainTable, currentUser, replacements) { + const { + organizationId, + app_role: { globalAccess }, + } = currentUser; + const tablesWithoutOrgId = ['permissions', 'roles']; + let whereConditions = ''; + + if (!globalAccess && !tablesWithoutOrgId.includes(mainTable)) { + whereConditions += `"${mainTable}"."organizationId" = :organizationId`; + replacements.organizationId = organizationId; + } + + if (mainTable !== 'users') { + whereConditions += whereConditions ? ' AND ' : ''; + whereConditions += `"${mainTable}"."deletedAt" IS NULL`; + } + + return whereConditions; +} + +function extractTableName(queryString) { + const tableNameRegex = /FROM\s+("?)([^"\s]+)\1\s*/i; + const match = tableNameRegex.exec(queryString); + return match ? match[2] : null; +} + +function buildQueryString(widget, currentUser) { + let queryString = widget?.query || ''; + const tableName = extractTableName(queryString); + const mainTable = JSON.parse(widget?.data)?.main_table || tableName; + const replacements = {}; + const whereConditions = constructWhereConditions( + mainTable, + currentUser, + replacements, + ); + queryString = insertWhereConditions(queryString, whereConditions); + console.log(queryString, 'queryString'); + return queryString; +} + +async function constructWidgetsResults(widgets, currentUser) { + const widgetsResults = []; + for (const widget of widgets) { + if (!widget) continue; + const queryString = buildQueryString(widget, currentUser); + const queryResult = await executeQuery(queryString, currentUser); + widgetsResults.push(buildWidgetResult(widget, queryResult, queryString)); + } + return widgetsResults; +} + +async function fetchWidgetsData(widgets) { + const widgetPromises = (widgets || []).map((widgetId) => + axios.get( + `${config.flHost}/${config.project_uuid}/project_customization_widgets/${widgetId}.json`, + ), + ); + const widgetResults = widgetPromises + ? await Promise.allSettled(widgetPromises) + : []; + return widgetResults + .filter((result) => result.status === 'fulfilled') + .map((result) => result.value.data); +} + +async function processWidgets(widgets, currentUser) { + const widgetData = await fetchWidgetsData(widgets); + return constructWidgetsResults(widgetData, currentUser); +} + +function parseCustomization(role) { + try { + return JSON.parse(role.role_customization || '{}'); + } catch (e) { + console.log(e); + return {}; + } +} + +async function findRole(roleId, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + const role = roleId + ? await RolesDBApi.findBy({ id: roleId }, { transaction }) + : await RolesDBApi.findBy({ name: 'User' }, { transaction }); + await transaction.commit(); + return role; + } catch (error) { + await transaction.rollback(); + throw error; + } +} + +module.exports = class RolesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await RolesDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await RolesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let roles = await RolesDBApi.findBy({ id }, { transaction }); + + if (!roles) { + throw new ValidationError('rolesNotFound'); + } + + const updatedRoles = await RolesDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedRoles; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await RolesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await RolesDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async addRoleInfo(roleId, userId, key, widgetId, currentUser) { + const regexExpForUuid = + /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi; + const widgetIdIsUUID = regexExpForUuid.test(widgetId); + + const transaction = await db.sequelize.transaction(); + let role; + if (roleId) { + role = await RolesDBApi.findBy({ id: roleId }, { transaction }); + } else { + role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); + } + + if (!role) { + throw new ValidationError('rolesNotFound'); + } + + try { + let customization = {}; + try { + customization = JSON.parse(role.role_customization || '{}'); + } catch (e) { + console.log(e); + } + + if (widgetIdIsUUID && Array.isArray(customization[key])) { + const el = customization[key].find((e) => e === widgetId); + !el ? customization[key].unshift(widgetId) : null; + } + + if (widgetIdIsUUID && !customization[key]) { + customization[key] = [widgetId]; + } + + const newRole = await RolesDBApi.update( + role.id, + { + role_customization: JSON.stringify(customization), + name: role.name, + permissions: role.permissions, + }, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + + return newRole; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async removeRoleInfoById(infoId, roleId, key, currentUser) { + const transaction = await db.sequelize.transaction(); + + let role; + if (roleId) { + role = await RolesDBApi.findBy({ id: roleId }, { transaction }); + } else { + role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); + } + if (!role) { + await transaction.rollback(); + throw new ValidationError('rolesNotFound'); + } + + let customization = {}; + try { + customization = JSON.parse(role.role_customization || '{}'); + } catch (e) { + console.log(e); + } + + customization[key] = customization[key].filter((item) => item !== infoId); + + const response = await axios.delete( + `${config.flHost}/${config.project_uuid}/project_customization_widgets/${infoId}.json`, + ); + const { status } = await response; + try { + const result = await RolesDBApi.update( + role.id, + { + role_customization: JSON.stringify(customization), + name: role.name, + permissions: role.permissions, + }, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return result; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async getRoleInfoByKey(key, roleId, currentUser) { + const transaction = await db.sequelize.transaction(); + + const organizationId = currentUser.organizationId; + let globalAccess = currentUser.app_role?.globalAccess; + let queryString = ''; + + let role; + try { + if (roleId) { + role = await RolesDBApi.findBy({ id: roleId }, { transaction }); + } else { + role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); + } + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + + let customization = '{}'; + + try { + customization = JSON.parse(role.role_customization || '{}'); + } catch (e) { + console.log(e); + } + + if (key === 'widgets') { + const widgets = customization[key] || []; + const widgetArray = widgets.map((widget) => { + return axios.get( + `${config.flHost}/${config.project_uuid}/project_customization_widgets/${widget}.json`, + ); + }); + const widgetResults = await Promise.allSettled(widgetArray); + + const fulfilledWidgets = widgetResults.map((result) => { + if (result.status === 'fulfilled') { + return result.value.data; + } + }); + + const widgetsResults = []; + + if (Array.isArray(fulfilledWidgets)) { + for (const widget of fulfilledWidgets) { + let result = []; + try { + result = await db.sequelize.query(widget.query); + } catch (e) { + console.log(e); + } + + if (result[0] && result[0].length) { + const key = Object.keys(result[0][0])[0]; + const value = + widget.widget_type === 'scalar' ? result[0][0][key] : result[0]; + const widgetData = JSON.parse(widget.data); + widgetsResults.push({ ...widget, ...widgetData, value }); + } else { + widgetsResults.push({ ...widget, value: null }); + } + } + } + return widgetsResults; + } + return customization[key]; + } +}; diff --git a/backend/src/services/search.js b/backend/src/services/search.js new file mode 100644 index 0000000..29f93f9 --- /dev/null +++ b/backend/src/services/search.js @@ -0,0 +1,133 @@ +const db = require('../db/models'); +const ValidationError = require('./notifications/errors/validation'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +/** + * @param {string} permission + * @param {object} currentUser + */ +async function checkPermissions(permission, currentUser) { + if (!currentUser) { + throw new ValidationError('auth.unauthorized'); + } + + const userPermission = currentUser.custom_permissions.find( + (cp) => cp.name === permission, + ); + + if (userPermission) { + return true; + } + + try { + if (!currentUser.app_role) { + throw new ValidationError('auth.forbidden'); + } + + const permissions = await currentUser.app_role.getPermissions(); + + return !!permissions.find((p) => p.name === permission); + } catch (e) { + throw e; + } +} + +module.exports = class SearchService { + static async search(searchQuery, currentUser) { + try { + if (!searchQuery) { + throw new ValidationError('iam.errors.searchQueryRequired'); + } + const tableColumns = { + users: ['firstName', 'lastName', 'phoneNumber', 'email'], + + qr_codes: ['code'], + + videos: ['title', 'url'], + }; + const columnsInt = {}; + + let allFoundRecords = []; + + for (const tableName in tableColumns) { + if (tableColumns.hasOwnProperty(tableName)) { + const attributesToSearch = tableColumns[tableName]; + const attributesIntToSearch = columnsInt[tableName] || []; + const whereCondition = { + [Op.or]: [ + ...attributesToSearch.map((attribute) => ({ + [attribute]: { + [Op.iLike]: `%${searchQuery}%`, + }, + })), + ...attributesIntToSearch.map((attribute) => + Sequelize.where( + Sequelize.cast( + Sequelize.col(`${tableName}.${attribute}`), + 'varchar', + ), + { [Op.iLike]: `%${searchQuery}%` }, + ), + ), + ], + }; + + const hasPermission = await checkPermissions( + `READ_${tableName.toUpperCase()}`, + currentUser, + ); + if (!hasPermission) { + continue; + } + + const foundRecords = await db[tableName].findAll({ + where: whereCondition, + attributes: [ + ...tableColumns[tableName], + 'id', + ...attributesIntToSearch, + ], + }); + + const modifiedRecords = foundRecords.map((record) => { + const matchAttribute = []; + + for (const attribute of attributesToSearch) { + if ( + record[attribute] + ?.toLowerCase() + ?.includes(searchQuery.toLowerCase()) + ) { + matchAttribute.push(attribute); + } + } + + for (const attribute of attributesIntToSearch) { + const castedValue = String(record[attribute]); + if ( + castedValue && + castedValue.toLowerCase().includes(searchQuery.toLowerCase()) + ) { + matchAttribute.push(attribute); + } + } + + return { + ...record.get(), + matchAttribute, + tableName, + }; + }); + + allFoundRecords = allFoundRecords.concat(modifiedRecords); + } + } + + return allFoundRecords; + } catch (error) { + throw error; + } + } +}; diff --git a/backend/src/services/users.js b/backend/src/services/users.js new file mode 100644 index 0000000..b6de7a8 --- /dev/null +++ b/backend/src/services/users.js @@ -0,0 +1,152 @@ +const db = require('../db/models'); +const UsersDBApi = require('../db/api/users'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +const InvitationEmail = require('./email/list/invitation'); +const EmailSender = require('./email'); +const AuthService = require('./auth'); + +module.exports = class UsersService { + static async create(data, currentUser, sendInvitationEmails = true, host) { + let transaction = await db.sequelize.transaction(); + + let email = data.email; + let emailsToInvite = []; + try { + if (email) { + let user = await UsersDBApi.findBy({ email }, { transaction }); + if (user) { + throw new ValidationError('iam.errors.userAlreadyExists'); + } else { + await UsersDBApi.create( + { data }, + + { + currentUser, + transaction, + }, + ); + emailsToInvite.push(email); + } + } else { + throw new ValidationError('iam.errors.emailRequired'); + } + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + if (emailsToInvite && emailsToInvite.length) { + if (!sendInvitationEmails) return; + + AuthService.sendPasswordResetEmail(email, 'invitation', host); + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + let emailsToInvite = []; + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', () => { + console.log('results csv', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + const hasAllEmails = results.every((result) => result.email); + + if (!hasAllEmails) { + throw new ValidationError('importer.errors.userEmailMissing'); + } + + await UsersDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + emailsToInvite = results.map((result) => result.email); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + + if (emailsToInvite && emailsToInvite.length && !sendInvitationEmails) { + emailsToInvite.forEach((email) => { + AuthService.sendPasswordResetEmail(email, 'invitation', host); + }); + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + let users = await UsersDBApi.findBy({ id }, { transaction }); + + if (!users) { + throw new ValidationError('iam.errors.userNotFound'); + } + + const updatedUser = await UsersDBApi.update( + id, + data, + + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedUser; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + if (currentUser.id === id) { + throw new ValidationError('iam.errors.deletingHimself'); + } + + if (currentUser.app_role?.name !== config.roles.admin) { + throw new ValidationError('errors.forbidden.message'); + } + + await UsersDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/videos.js b/backend/src/services/videos.js new file mode 100644 index 0000000..17c7bde --- /dev/null +++ b/backend/src/services/videos.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const VideosDBApi = require('../db/api/videos'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class VideosService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await VideosDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await VideosDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let videos = await VideosDBApi.findBy({ id }, { transaction }); + + if (!videos) { + throw new ValidationError('videosNotFound'); + } + + const updatedVideos = await VideosDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedVideos; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await VideosDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await VideosDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/yarn.lock b/backend/yarn.lock new file mode 100644 index 0000000..222a4f9 --- /dev/null +++ b/backend/yarn.lock @@ -0,0 +1,4470 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@apidevtools/json-schema-ref-parser@^9.0.6": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz#8ff5386b365d4c9faa7c8b566ff16a46a577d9b8" + integrity sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.6" + call-me-maybe "^1.0.1" + js-yaml "^4.1.0" + +"@apidevtools/openapi-schemas@^2.0.4": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17" + integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== + +"@apidevtools/swagger-methods@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267" + integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== + +"@apidevtools/swagger-parser@10.0.3": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz#32057ae99487872c4dd96b314a1ab4b95d89eaf5" + integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g== + dependencies: + "@apidevtools/json-schema-ref-parser" "^9.0.6" + "@apidevtools/openapi-schemas" "^2.0.4" + "@apidevtools/swagger-methods" "^3.0.2" + "@jsdevtools/ono" "^7.1.3" + call-me-maybe "^1.0.1" + z-schema "^5.0.1" + +"@azure/abort-controller@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.1.0.tgz#788ee78457a55af8a1ad342acb182383d2119249" + integrity sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw== + dependencies: + tslib "^2.2.0" + +"@azure/abort-controller@^2.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz#42fe0ccab23841d9905812c58f1082d27784566d" + integrity sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA== + dependencies: + tslib "^2.6.2" + +"@azure/core-auth@^1.3.0", "@azure/core-auth@^1.4.0", "@azure/core-auth@^1.5.0": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.7.2.tgz#558b7cb7dd12b00beec07ae5df5907d74df1ebd9" + integrity sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-util" "^1.1.0" + tslib "^2.6.2" + +"@azure/core-client@^1.3.0", "@azure/core-client@^1.5.0", "@azure/core-client@^1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@azure/core-client/-/core-client-1.9.2.tgz#6fc69cee2816883ab6c5cdd653ee4f2ff9774f74" + integrity sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-auth" "^1.4.0" + "@azure/core-rest-pipeline" "^1.9.1" + "@azure/core-tracing" "^1.0.0" + "@azure/core-util" "^1.6.1" + "@azure/logger" "^1.0.0" + tslib "^2.6.2" + +"@azure/core-http-compat@^2.0.1": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz#d1585ada24ba750dc161d816169b33b35f762f0d" + integrity sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-client" "^1.3.0" + "@azure/core-rest-pipeline" "^1.3.0" + +"@azure/core-lro@^2.2.0": + version "2.7.2" + resolved "https://registry.yarnpkg.com/@azure/core-lro/-/core-lro-2.7.2.tgz#787105027a20e45c77651a98b01a4d3b01b75a08" + integrity sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-util" "^1.2.0" + "@azure/logger" "^1.0.0" + tslib "^2.6.2" + +"@azure/core-paging@^1.1.1": + version "1.6.2" + resolved "https://registry.yarnpkg.com/@azure/core-paging/-/core-paging-1.6.2.tgz#40d3860dc2df7f291d66350b2cfd9171526433e7" + integrity sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA== + dependencies: + tslib "^2.6.2" + +"@azure/core-rest-pipeline@^1.1.0", "@azure/core-rest-pipeline@^1.3.0", "@azure/core-rest-pipeline@^1.8.1", "@azure/core-rest-pipeline@^1.9.1": + version "1.16.2" + resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.2.tgz#3f71b09e45a65926cc598478b4f1bcd0fe67bf4b" + integrity sha512-Hnhm/PG9/SQ07JJyLDv3l9Qr8V3xgAe1hFoBYzt6LaalMxfL/ZqFaZf/bz5VN3pMcleCPwl8ivlS2Fjxq/iC8Q== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-auth" "^1.4.0" + "@azure/core-tracing" "^1.0.1" + "@azure/core-util" "^1.9.0" + "@azure/logger" "^1.0.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.0" + tslib "^2.6.2" + +"@azure/core-tracing@^1.0.0", "@azure/core-tracing@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.1.2.tgz#065dab4e093fb61899988a1cdbc827d9ad90b4ee" + integrity sha512-dawW9ifvWAWmUm9/h+/UQ2jrdvjCJ7VJEuCJ6XVNudzcOwm53BFZH4Q845vjfgoUAM8ZxokvVNxNxAITc502YA== + dependencies: + tslib "^2.6.2" + +"@azure/core-util@^1.0.0", "@azure/core-util@^1.1.0", "@azure/core-util@^1.2.0", "@azure/core-util@^1.3.0", "@azure/core-util@^1.6.1", "@azure/core-util@^1.9.0": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.9.1.tgz#05ea9505c5cdf29c55ccf99a648c66ddd678590b" + integrity sha512-OLsq0etbHO1MA7j6FouXFghuHrAFGk+5C1imcpQ2e+0oZhYF07WLA+NW2Vqs70R7d+zOAWiWM3tbE1sXcDN66g== + dependencies: + "@azure/abort-controller" "^2.0.0" + tslib "^2.6.2" + +"@azure/identity@^4.2.1": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-4.4.0.tgz#f2743e63d346000a70b0eed5a3b397dedd3984a7" + integrity sha512-oG6oFNMxUuoivYg/ElyZWVSZfw42JQyHbrp+lR7VJ1BYWsGzt34NwyDw3miPp1QI7Qm5+4KAd76wGsbHQmkpkg== + dependencies: + "@azure/abort-controller" "^1.0.0" + "@azure/core-auth" "^1.5.0" + "@azure/core-client" "^1.9.2" + "@azure/core-rest-pipeline" "^1.1.0" + "@azure/core-tracing" "^1.0.0" + "@azure/core-util" "^1.3.0" + "@azure/logger" "^1.0.0" + "@azure/msal-browser" "^3.14.0" + "@azure/msal-node" "^2.9.2" + events "^3.0.0" + jws "^4.0.0" + open "^8.0.0" + stoppable "^1.1.0" + tslib "^2.2.0" + +"@azure/keyvault-keys@^4.4.0": + version "4.8.0" + resolved "https://registry.yarnpkg.com/@azure/keyvault-keys/-/keyvault-keys-4.8.0.tgz#1513b3a187bb3a9a372b5980c593962fb793b2ad" + integrity sha512-jkuYxgkw0aaRfk40OQhFqDIupqblIOIlYESWB6DKCVDxQet1pyv86Tfk9M+5uFM0+mCs6+MUHU+Hxh3joiUn4Q== + dependencies: + "@azure/abort-controller" "^1.0.0" + "@azure/core-auth" "^1.3.0" + "@azure/core-client" "^1.5.0" + "@azure/core-http-compat" "^2.0.1" + "@azure/core-lro" "^2.2.0" + "@azure/core-paging" "^1.1.1" + "@azure/core-rest-pipeline" "^1.8.1" + "@azure/core-tracing" "^1.0.0" + "@azure/core-util" "^1.0.0" + "@azure/logger" "^1.0.0" + tslib "^2.2.0" + +"@azure/logger@^1.0.0": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@azure/logger/-/logger-1.1.3.tgz#09a8fd4850b9112865756e92d5e8b728ee457345" + integrity sha512-J8/cIKNQB1Fc9fuYqBVnrppiUtW+5WWJPCj/tAokC5LdSTwkWWttN+jsRgw9BLYD7JDBx7PceiqOBxJJ1tQz3Q== + dependencies: + tslib "^2.6.2" + +"@azure/msal-browser@^3.14.0": + version "3.19.1" + resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.19.1.tgz#c5e5a7996f95cadc11920bffa2bf6321e3a24555" + integrity sha512-pqYP2gK0GCEa4OxtOqlS+EdFQqhXV6ZuESgSTYWq2ABXyxBVVdd5KNuqgR5SU0OwI2V1YWdFVvLDe1487dyQ0g== + dependencies: + "@azure/msal-common" "14.13.1" + +"@azure/msal-common@14.13.1": + version "14.13.1" + resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.13.1.tgz#e296cf8cc556082af9c35d803496424e8a95d8b7" + integrity sha512-iUp3BYrsRZ4X3EiaZ2fDjNFjmtYMv9rEQd6c1op6ULn0HWk4ACvDmosL6NaBgWOhl1BAblIbd9vmB5/ilF8d4A== + +"@azure/msal-node@^2.9.2": + version "2.11.1" + resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.11.1.tgz#7fea67a1c6904301eb8853fae7df86c34306a9cc" + integrity sha512-8ECtug4RL+zsgh20VL8KYHjrRO3MJOeAKEPRXT2lwtiu5U3BdyIdBb50+QZthEkIi60K6pc/pdOx/k5Jp4sLng== + dependencies: + "@azure/msal-common" "14.13.1" + jsonwebtoken "^9.0.0" + uuid "^8.3.0" + +"@google-cloud/paginator@^3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-3.0.7.tgz#fb6f8e24ec841f99defaebf62c75c2e744dd419b" + integrity sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ== + dependencies: + arrify "^2.0.0" + extend "^3.0.2" + +"@google-cloud/projectify@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-2.1.1.tgz#ae6af4fee02d78d044ae434699a630f8df0084ef" + integrity sha512-+rssMZHnlh0twl122gXY4/aCrk0G1acBqkHFfYddtsqpYXGxA29nj9V5V9SfC+GyOG00l650f6lG9KL+EpFEWQ== + +"@google-cloud/promisify@^2.0.0": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-2.0.4.tgz#9d8705ecb2baa41b6b2673f3a8e9b7b7e1abc52a" + integrity sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA== + +"@google-cloud/storage@^5.18.2": + version "5.20.5" + resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-5.20.5.tgz#1de71fc88d37934a886bc815722c134b162d335d" + integrity sha512-lOs/dCyveVF8TkVFnFSF7IGd0CJrTm91qiK6JLu+Z8qiT+7Ag0RyVhxZIWkhiACqwABo7kSHDm8FdH8p2wxSSw== + dependencies: + "@google-cloud/paginator" "^3.0.7" + "@google-cloud/projectify" "^2.0.0" + "@google-cloud/promisify" "^2.0.0" + abort-controller "^3.0.0" + arrify "^2.0.0" + async-retry "^1.3.3" + compressible "^2.0.12" + configstore "^5.0.0" + duplexify "^4.0.0" + ent "^2.2.0" + extend "^3.0.2" + gaxios "^4.0.0" + google-auth-library "^7.14.1" + hash-stream-validation "^0.2.2" + mime "^3.0.0" + mime-types "^2.0.8" + p-limit "^3.0.1" + pumpify "^2.0.0" + retry-request "^4.2.2" + stream-events "^1.0.4" + teeny-request "^7.1.3" + uuid "^8.0.0" + xdg-basedir "^4.0.0" + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + 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" + +"@js-joda/core@^5.6.1": + version "5.6.3" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-5.6.3.tgz#41ae1c07de1ebe0f6dde1abcbc9700a09b9c6056" + integrity sha512-T1rRxzdqkEXcou0ZprN1q9yDRlvzCPLqmlNt5IIsGBzoEVgLCCYrKEwc84+TvsXuAc95VAZwtWD2zVsKPY4bcA== + +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + +"@mapbox/node-pre-gyp@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" + integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + +"@one-ini/wasm@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323" + integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw== + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + +"@types/debug@^4.1.8": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + +"@types/json-schema@^7.0.6": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/ms@*": + version "0.7.34" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== + +"@types/node@*", "@types/node@>=18": + version "20.14.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.11.tgz#09b300423343460455043ddd4d0ded6ac579b74b" + integrity sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA== + dependencies: + undici-types "~5.26.4" + +"@types/readable-stream@^4.0.0": + version "4.0.15" + resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-4.0.15.tgz#e6ec26fe5b02f578c60baf1fa9452e90957d2bfb" + integrity sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw== + dependencies: + "@types/node" "*" + safe-buffer "~5.1.1" + +"@types/validator@^13.7.17": + version "13.12.0" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.12.0.tgz#1fe4c3ae9de5cf5193ce64717c99ef2fa7d8756f" + integrity sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +abbrev@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf" + integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@^1.3.7, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agent-base@^7.0.2, agent-base@^7.1.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" + integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== + +ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +anymatch@~3.1.1, anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array.prototype.map@^1.0.1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/array.prototype.map/-/array.prototype.map-1.0.7.tgz#82fa4d6027272d1fca28a63bbda424d0185d78a7" + integrity sha512-XpcFfLoBEAhezrrNw1V+yLXkE7M6uR7xJEsxbG6c/V9v043qurwVJB9r9UTnoSioFDoz1i1VOydpWGmJpfVZbg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-array-method-boxes-properly "^1.0.0" + es-object-atoms "^1.0.0" + is-string "^1.0.7" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + 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" + +arrify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +async-retry@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== + dependencies: + retry "0.13.1" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +axios@^1.6.7: + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.0, base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +base64url@3.x.x: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + +bcrypt@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.1.1.tgz#0f732c6dcb4e12e5b70a25e326a72965879ba6e2" + integrity sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.11" + node-addon-api "^5.0.0" + +bignumber.js@^9.0.0: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bl@^6.0.11: + version "6.0.14" + resolved "https://registry.yarnpkg.com/bl/-/bl-6.0.14.tgz#b9ae9862118a3d2ebec999c5318466012314f96c" + integrity sha512-TJfbvGdL7KFGxTsEbsED7avqpFdY56q9IW0/aiytyheJzxST/+Io6cx/4Qx0K2/u0BPRDs65mjaQzYvMZeNocQ== + dependencies: + "@types/readable-stream" "^4.0.0" + buffer "^6.0.3" + inherits "^2.0.4" + readable-stream "^4.2.0" + +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + 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.1" + type-is "~1.6.18" + unpipe "1.0.0" + +boxen@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" + integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^5.3.1" + chalk "^3.0.0" + cli-boxes "^2.2.0" + string-width "^4.1.0" + term-size "^2.1.0" + type-fest "^0.8.1" + widest-line "^3.1.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-writer@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" + integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +busboy@^0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + integrity sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg== + dependencies: + dicer "0.2.5" + readable-stream "1.1.x" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + 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" + +call-me-maybe@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" + integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + +chokidar@^3.2.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + 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" + optionalDependencies: + fsevents "~2.3.2" + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +cli-boxes@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + +cli-color@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.4.tgz#d658080290968816b322248b7306fad2346fb2c8" + integrity sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA== + dependencies: + d "^1.0.1" + es5-ext "^0.10.64" + es6-iterator "^2.0.3" + memoizee "^0.4.15" + timers-ext "^0.1.7" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-response@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" + integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== + dependencies: + mimic-response "^1.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75" + integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + +compressible@^2.0.12: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +config-chain@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +configstore@^5.0.0, configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + +console-control-strings@^1.0.0, console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-env@7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +csv-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/csv-parser/-/csv-parser-3.0.0.tgz#b88a6256d79e090a97a1b56451f9327b01d710e7" + integrity sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ== + dependencies: + minimist "^1.2.0" + +d@1, d@^1.0.1, d@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de" + integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== + dependencies: + es5-ext "^0.10.64" + type "^2.7.2" + +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.1.1, debug@^4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +debug@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +debug@^3.2.6: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + integrity sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA== + dependencies: + mimic-response "^1.0.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +defer-to-connect@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +define-properties@^1.1.2, define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + +denque@^1.4.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" + integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-libc@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +dicer@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + integrity sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg== + dependencies: + readable-stream "1.1.x" + streamsearch "0.1.2" + +diff@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +doctrine@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dot-prop@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +dottie@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/dottie/-/dottie-2.0.6.tgz#34564ebfc6ec5e5772272d466424ad5b696484d4" + integrity sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA== + +duplexer3@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" + integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== + +duplexify@^4.0.0, duplexify@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.3.tgz#a07e1c0d0a2c001158563d32592ba58bddb0236f" + integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.2" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +editorconfig@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-1.0.4.tgz#040c9a8e9a6c5288388b87c2db07028aa89f53a3" + integrity sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q== + dependencies: + "@one-ini/wasm" "0.1.1" + commander "^10.0.0" + minimatch "9.0.1" + semver "^7.5.3" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +ent@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.1.tgz#68dc99a002f115792c26239baedaaea9e70c0ca2" + integrity sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A== + dependencies: + punycode "^1.4.1" + +es-abstract@^1.17.0-next.1, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + 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" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-get-iterator@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14, es5-ext@~0.10.2: + version "0.10.64" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + esniff "^2.0.1" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" + integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg== + dependencies: + d "^1.0.2" + ext "^1.7.0" + +es6-weak-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" + integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== + dependencies: + d "1" + es5-ext "^0.10.46" + es6-iterator "^2.0.3" + es6-symbol "^3.1.1" + +escalade@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-goat@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" + integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@^3.0.0, events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +express@4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.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" + +ext@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + +extend@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-text-encoding@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" + integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + 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" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +flat@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.1.tgz#a392059cc382881ff98642f5da4dde0a959f309b" + integrity sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA== + dependencies: + is-buffer "~2.0.3" + +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +foreground-child@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" + integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formidable@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" + integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2, fresh@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.1, function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + +gaxios@^4.0.0: + version "4.3.3" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.3.3.tgz#d44bdefe52d34b6435cc41214fdb160b64abfc22" + integrity sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA== + dependencies: + abort-controller "^3.0.0" + extend "^3.0.2" + https-proxy-agent "^5.0.0" + is-stream "^2.0.0" + node-fetch "^2.6.7" + +gcp-metadata@^4.2.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.3.1.tgz#fb205fe6a90fef2fd9c85e6ba06e5559ee1eefa9" + integrity sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A== + dependencies: + gaxios "^4.0.0" + json-bigint "^1.0.0" + +generate-function@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + +get-caller-file@^2.0.1, get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + 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" + +get-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + +glob-parent@~5.1.0, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^10.3.3: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + 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" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + 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" + +global-dirs@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.1.0.tgz#e9046a49c806ff04d6c1825e196c8f0091e8df4d" + integrity sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ== + dependencies: + ini "1.3.7" + +globalthis@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + +google-auth-library@^7.14.1: + version "7.14.1" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-7.14.1.tgz#e3483034162f24cc71b95c8a55a210008826213c" + integrity sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + fast-text-encoding "^1.0.0" + gaxios "^4.0.0" + gcp-metadata "^4.2.0" + gtoken "^5.0.4" + jws "^4.0.0" + lru-cache "^6.0.0" + +google-p12-pem@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.1.4.tgz#123f7b40da204de4ed1fbf2fd5be12c047fc8b3b" + integrity sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg== + dependencies: + node-forge "^1.3.1" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +gtoken@^5.0.4: + version "5.3.2" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.3.2.tgz#deb7dc876abe002178e0515e383382ea9446d58f" + integrity sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ== + dependencies: + gaxios "^4.0.0" + google-p12-pem "^3.1.3" + jws "^4.0.0" + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.0, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + +has-yarn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" + integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== + +hash-stream-validation@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512" + integrity sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ== + +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +helmet@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.1.1.tgz#751f0e273d809ace9c172073e0003bed27d27a4a" + integrity sha512-Avg4XxSBrehD94mkRwEljnO+6RZx7AGfk8Wa6K1nxaU+hbXlFOhlOIMgPfFqOYQB/dBCsTpootTGuiOG+CHiQA== + +http-cache-semantics@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + +http-proxy-agent@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +https-proxy-agent@^7.0.0: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.2, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + integrity sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflection@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.13.4.tgz#65aa696c4e2da6225b148d7a154c449366633a32" + integrity sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" + integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== + +ini@^1.3.4, ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +internal-slot@^1.0.4, internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arguments@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-buffer@~2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-core-module@^2.13.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" + integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== + dependencies: + hasown "^2.0.2" + +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" + integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== + dependencies: + global-dirs "^2.0.1" + is-path-inside "^3.0.1" + +is-map@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-npm@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" + integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-inside@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== + +is-promise@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + +is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-set@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +is-yarn-global@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" + integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +iterate-iterator@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.2.tgz#551b804c9eaa15b847ea6a7cdc2f5bf1ec150f91" + integrity sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw== + +iterate-value@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57" + integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ== + dependencies: + es-get-iterator "^1.0.2" + iterate-iterator "^1.0.1" + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +js-beautify@^1.14.5: + version "1.15.1" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.15.1.tgz#4695afb508c324e1084ee0b952a102023fc65b64" + integrity sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA== + dependencies: + config-chain "^1.1.13" + editorconfig "^1.0.4" + glob "^10.3.3" + js-cookie "^3.0.5" + nopt "^7.2.0" + +js-cookie@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" + integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== + +js-md4@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/js-md4/-/js-md4-0.3.2.tgz#cd3b3dc045b0c404556c81ddb5756c23e59d7cf5" + integrity sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA== + +js-yaml@3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ== + +json2csv@^5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/json2csv/-/json2csv-5.0.7.tgz#f3a583c25abd9804be873e495d1e65ad8d1b54ae" + integrity sha512-YRZbUnyaJZLZUJSRi2G/MqahCyRv9n/ds+4oIetjDF3jWQA7AG7iSeKTiZiCNqtMZM7HDyt0e/W6lEnoGEmMGA== + dependencies: + commander "^6.1.0" + jsonparse "^1.3.1" + lodash.get "^4.4.2" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonparse@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== + +jsonwebtoken@8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + 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 "^5.6.0" + +jsonwebtoken@^9.0.0: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + 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" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + +latest-version@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" + integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== + dependencies: + package-json "^6.3.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.mergewith@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash@4.17.21, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + dependencies: + chalk "^4.0.0" + +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +lru-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" + integrity sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ== + dependencies: + es5-ext "~0.10.2" + +make-dir@^3.0.0, make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memoizee@^0.4.15: + version "0.4.17" + resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.17.tgz#942a5f8acee281fa6fb9c620bddc57e3b7382949" + integrity sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA== + dependencies: + d "^1.0.2" + es5-ext "^0.10.64" + es6-weak-map "^2.0.3" + event-emitter "^0.3.5" + is-promise "^2.2.2" + lru-queue "^0.1.0" + next-tick "^1.1.0" + timers-ext "^0.1.7" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-descriptors@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +methods@^1.1.2, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== + +mime-types@^2.0.8, mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0, mime@^1.3.4: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + +mimic-response@^1.0.0, mimic-response@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +minimatch@3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimatch@9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253" + integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mocha@8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.1.3.tgz#5e93f873e35dfdd69617ea75f9c68c2ca61c2ac5" + integrity sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.4.2" + debug "4.1.1" + diff "4.0.2" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.1.6" + growl "1.10.5" + he "1.2.0" + js-yaml "3.14.0" + log-symbols "4.0.0" + minimatch "3.0.4" + ms "2.1.2" + object.assign "4.1.0" + promise.allsettled "1.0.2" + serialize-javascript "4.0.0" + strip-json-comments "3.0.1" + supports-color "7.1.0" + which "2.0.2" + wide-align "1.1.3" + workerpool "6.0.0" + yargs "13.3.2" + yargs-parser "13.1.2" + yargs-unparser "1.6.1" + +moment-timezone@^0.5.43: + version "0.5.45" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.45.tgz#cb685acd56bac10e69d93c536366eb65aa6bcf5c" + integrity sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ== + dependencies: + moment "^2.29.4" + +moment@2.30.1, moment@^2.29.4: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multer@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4.tgz#e2bc6cac0df57a8832b858d7418ccaa8ebaf7d8c" + integrity sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw== + dependencies: + append-field "^1.0.0" + busboy "^0.2.11" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + on-finished "^2.3.0" + type-is "^1.6.4" + xtend "^4.0.0" + +mysql2@2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-2.2.5.tgz#72624ffb4816f80f96b9c97fedd8c00935f9f340" + integrity sha512-XRqPNxcZTpmFdXbJqb+/CtYVLCx14x1RTeNMD4954L331APu75IC74GDqnZMEt1kwaXy6TySo55rF2F3YJS78g== + dependencies: + denque "^1.4.1" + generate-function "^2.3.1" + iconv-lite "^0.6.2" + long "^4.0.0" + lru-cache "^6.0.0" + named-placeholders "^1.1.2" + seq-queue "^0.0.5" + sqlstring "^2.3.2" + +named-placeholders@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351" + integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w== + dependencies: + lru-cache "^7.14.1" + +native-duplexpair@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/native-duplexpair/-/native-duplexpair-1.0.0.tgz#7899078e64bf3c8a3d732601b3d40ff05db58fa0" + integrity sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + +node-addon-api@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" + integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== + +node-fetch@^2.6.1, node-fetch@^2.6.7: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-mocks-http@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/node-mocks-http/-/node-mocks-http-1.9.0.tgz#6000c570fc4b809603782309be81c73a71d85b71" + integrity sha512-ILf7Ws8xyX9Rl2fLZ7xhZBovrRwgaP84M13esndP6V17M/8j25TpwNzb7Im8U9XCo6fRhdwqiQajWXpsas/E6w== + dependencies: + accepts "^1.3.7" + depd "^1.1.0" + fresh "^0.5.2" + merge-descriptors "^1.0.1" + methods "^1.1.2" + mime "^1.3.4" + parseurl "^1.3.3" + range-parser "^1.2.0" + type-is "^1.6.18" + +nodemailer@6.9.9: + version "6.9.9" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.9.tgz#4549bfbf710cc6addec5064dd0f19874d24248d9" + integrity sha512-dexTll8zqQoVJEZPwQAKzxxtFn0qTnjdQTchoU6Re9BUUGBJiOy3YMn/0ShTW6J5M0dfQ1NeDeRTTl4oIWgQMA== + +nodemon@2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.5.tgz#df67fe1fd1312ddb0c1e393ae2cf55aacdcec2f3" + integrity sha512-6/jqtZvJdk092pVnD2AIH19KQ9GQZAKOZVy/yT1ueL6aoV+Ix7a1lVZStXzvEh0fP4zE41DDWlkVoHjR6WlozA== + dependencies: + chokidar "^3.2.2" + debug "^3.2.6" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.7" + semver "^5.7.1" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.3" + update-notifier "^4.1.0" + +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + +nopt@^7.2.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-7.2.1.tgz#1cac0eab9b8e97c9093338446eddd40b2c8ca1e7" + integrity sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w== + dependencies: + abbrev "^2.0.0" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-url@^4.1.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" + integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== + +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + +oauth@0.10.x: + version "0.10.0" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.10.0.tgz#3551c4c9b95c53ea437e1e21e46b649482339c58" + integrity sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q== + +oauth@0.9.x: + version "0.9.15" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" + integrity sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA== + +object-assign@^4, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +object-keys@^1.0.11, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +on-finished@2.4.1, on-finished@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +open@^8.0.0: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + +p-limit@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.1, p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== + +package-json@^6.3.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" + integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== + dependencies: + got "^9.6.0" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^6.2.0" + +packet-reader@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" + integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== + +parseurl@^1.3.3, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +passport-google-oauth2@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/passport-google-oauth2/-/passport-google-oauth2-0.2.0.tgz#fc9ea59e7091f02e24fd16d6be9257ea982ebbc3" + integrity sha512-62EdPtbfVdc55nIXi0p1WOa/fFMM8v/M8uQGnbcXA4OexZWCnfsEi3wo2buag+Is5oqpuHzOtI64JpHk0Xi5RQ== + dependencies: + passport-oauth2 "^1.1.2" + +passport-jwt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.1.tgz#c443795eff322c38d173faa0a3c481479646ec3d" + integrity sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ== + dependencies: + jsonwebtoken "^9.0.0" + passport-strategy "^1.0.0" + +passport-microsoft@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/passport-microsoft/-/passport-microsoft-0.1.0.tgz#dc72c1a38b294d74f4dc55fe93f52e25cb9aa5b4" + integrity sha512-0giBDgE1fnR5X84zJZkQ11hnKVrzEgViwRO6RGsormK9zTxFQmN/UHMTDbIpvhk989VqALewB6Pk1R5vNr3GHw== + dependencies: + passport-oauth2 "1.2.0" + pkginfo "0.2.x" + +passport-oauth2@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.2.0.tgz#49613a3eca85c7a1e65bf1019e2b6b80a10c8ac2" + integrity sha512-6128N+n/MOrJdXxdC2q/PVKXtqgihGFIeup+9bsPybAvMPOUKqdGhh9ZIzZF8rFKJOlxUP9fgP3H0JQe18n0rg== + dependencies: + oauth "0.9.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + +passport-oauth2@^1.1.2: + version "1.8.0" + resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.8.0.tgz#55725771d160f09bbb191828d5e3d559eee079c8" + integrity sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA== + dependencies: + base64url "3.x.x" + oauth "0.10.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + utils-merge "1.x.x" + +passport-strategy@1.x.x, passport-strategy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== + +passport@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.7.0.tgz#3688415a59a48cf8068417a8a8092d4492ca3a05" + integrity sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + utils-merge "^1.0.1" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== + +pg-connection-string@^2.4.0, pg-connection-string@^2.6.1: + version "2.6.4" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.4.tgz#f543862adfa49fa4e14bc8a8892d2a84d754246d" + integrity sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA== + +pg-hstore@2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/pg-hstore/-/pg-hstore-2.3.4.tgz#4425e3e2a3e15d2a334c35581186c27cf2e9b8dd" + integrity sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA== + dependencies: + underscore "^1.13.1" + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-pool@^3.2.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.2.tgz#3a592370b8ae3f02a7c8130d245bc02fa2c5f3f2" + integrity sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg== + +pg-protocol@^1.3.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3" + integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg== + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg@8.4.1: + version "8.4.1" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.4.1.tgz#06cfb6208ae787a869b2f0022da11b90d13d933e" + integrity sha512-NRsH0aGMXmX1z8Dd0iaPCxWUw4ffu+lIAmGm+sTCwuDDWkpEgRCAHZYDwqaNhC5hG5DRMOjSUFasMWhvcmLN1A== + dependencies: + buffer-writer "2.0.0" + packet-reader "1.0.0" + pg-connection-string "^2.4.0" + pg-pool "^3.2.1" + pg-protocol "^1.3.0" + pg-types "^2.1.0" + pgpass "1.x" + +pgpass@1.x: + version "1.0.5" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== + dependencies: + split2 "^4.1.0" + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkginfo@0.2.x: + version "0.2.3" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.2.3.tgz#7239c42a5ef6c30b8f328439d9b9ff71042490f8" + integrity sha512-7W7wTrE/NsY8xv/DTGjwNIyNah81EQH0MWcTzrHL6pOpMocOGZc0Mbdz9aXxSrp+U0mSmkU8jrNCDCfUs3sOBg== + +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +promise.allsettled@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.2.tgz#d66f78fbb600e83e863d893e98b3d4376a9c47c9" + integrity sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg== + dependencies: + array.prototype.map "^1.0.1" + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + iterate-value "^1.0.0" + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +pstree.remy@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-2.0.1.tgz#abfc7b5a621307c728b551decbbefb51f0e4aa1e" + integrity sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw== + dependencies: + duplexify "^4.1.1" + inherits "^2.0.3" + pump "^3.0.0" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +pupa@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" + integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== + dependencies: + escape-goat "^2.0.0" + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.0, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@1.2.8, rc@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@1.1.x: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.2.2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1, readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^4.2.0: + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" + +registry-auth-token@^4.0.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.2.tgz#f02d49c3668884612ca031419491a13539e21fac" + integrity sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg== + dependencies: + rc "1.2.8" + +registry-url@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" + integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== + dependencies: + rc "^1.2.8" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +resolve@^1.22.1: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +responselike@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ== + dependencies: + lowercase-keys "^1.0.0" + +retry-as-promised@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-7.0.4.tgz#9df73adaeea08cb2948b9d34990549dc13d800a2" + integrity sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA== + +retry-request@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.2.2.tgz#b7d82210b6d2651ed249ba3497f07ea602f1a903" + integrity sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg== + dependencies: + debug "^4.1.1" + extend "^3.0.2" + +retry@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver-diff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" + integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== + dependencies: + semver "^6.3.0" + +semver@^5.6.0, semver@^5.7.1: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.5, semver@^7.5.3, semver@^7.5.4: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + 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" + +seq-queue@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" + integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q== + +sequelize-cli@6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/sequelize-cli/-/sequelize-cli-6.6.2.tgz#8d838b25c988cf136914cdc3843e19d88c3dcb67" + integrity sha512-V8Oh+XMz2+uquLZltZES6MVAD+yEnmMfwfn+gpXcDiwE3jyQygLt4xoI0zG8gKt6cRcs84hsKnXAKDQjG/JAgg== + dependencies: + cli-color "^2.0.3" + fs-extra "^9.1.0" + js-beautify "^1.14.5" + lodash "^4.17.21" + resolve "^1.22.1" + umzug "^2.3.0" + yargs "^16.2.0" + +sequelize-json-schema@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/sequelize-json-schema/-/sequelize-json-schema-2.1.1.tgz#a82d3813925e81485d76ce291f4ff5c8cb2ae492" + integrity sha512-yCGaHnmQQeL6MQ/fOxhkR5C2aOGZyTD6OrgjP4yw1rbuujuIUVdzWN3AsC6r6AvlGZ3EUBBbCJHKl8OIFFES4Q== + +sequelize-pool@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-7.1.0.tgz#210b391af4002762f823188fd6ecfc7413020768" + integrity sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg== + +sequelize@6.35.2: + version "6.35.2" + resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.35.2.tgz#9276d24055a9a07bd6812c89ab402659f5853e70" + integrity sha512-EdzLaw2kK4/aOnWQ7ed/qh3B6/g+1DvmeXr66RwbcqSm/+QRS9X0LDI5INBibsy4eNJHWIRPo3+QK0zL+IPBHg== + dependencies: + "@types/debug" "^4.1.8" + "@types/validator" "^13.7.17" + debug "^4.3.4" + dottie "^2.0.6" + inflection "^1.13.4" + lodash "^4.17.21" + moment "^2.29.4" + moment-timezone "^0.5.43" + pg-connection-string "^2.6.1" + retry-as-promised "^7.0.4" + semver "^7.5.4" + sequelize-pool "^7.1.0" + toposort-class "^1.0.1" + uuid "^8.3.2" + validator "^13.9.0" + wkx "^0.5.0" + +serialize-javascript@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + 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" + +set-function-name@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +split2@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +sqlite@4.0.15: + version "4.0.15" + resolved "https://registry.yarnpkg.com/sqlite/-/sqlite-4.0.15.tgz#071e0577afb327fbd74a75354ea15964378392e3" + integrity sha512-irPPTrbVoDvwzRGpe0v8vxpNwMl+q0tXQzffQTcCUnaJzQFO0hfLLvFwGDKxd6vYBuvEr3uvPkObVoGOvVsmzA== + +sqlstring@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c" + integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" + +stoppable@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b" + integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw== + +stream-events@^1.0.4, stream-events@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" + integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== + dependencies: + stubs "^3.0.0" + +stream-shift@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" + integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== + +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string_decoder@^1.1.1, string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow== + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-json-comments@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" + integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +stubs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" + integrity sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw== + +supports-color@7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + +supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +swagger-jsdoc@^6.2.8: + version "6.2.8" + resolved "https://registry.yarnpkg.com/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz#6d33d9fb07ff4a7c1564379c52c08989ec7d0256" + integrity sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ== + dependencies: + commander "6.2.0" + doctrine "3.0.0" + glob "7.1.6" + lodash.mergewith "^4.6.2" + swagger-parser "^10.0.3" + yaml "2.0.0-1" + +swagger-parser@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/swagger-parser/-/swagger-parser-10.0.3.tgz#04cb01c18c3ac192b41161c77f81e79309135d03" + integrity sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg== + dependencies: + "@apidevtools/swagger-parser" "10.0.3" + +swagger-ui-dist@>=5.0.0: + version "5.17.14" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz#e2c222e5bf9e15ccf80ec4bc08b4aaac09792fd6" + integrity sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw== + +swagger-ui-express@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz#fb8c1b781d2793a6bd2f8a205a3f4bd6fa020dd8" + integrity sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA== + dependencies: + swagger-ui-dist ">=5.0.0" + +tar@^6.1.11: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tedious@^18.2.4: + version "18.2.4" + resolved "https://registry.yarnpkg.com/tedious/-/tedious-18.2.4.tgz#c33986f2561b4fde92bb9df70f44ae1a14f71b46" + integrity sha512-+6Nzn/aURTQ+8OxLAJ8fKK5Fbb84HRTI3bHiAC3ZzBKrBg9BHtcHxjmlIni5Zn46hzKiZ5WrDMSwDH8oIYjV8w== + dependencies: + "@azure/identity" "^4.2.1" + "@azure/keyvault-keys" "^4.4.0" + "@js-joda/core" "^5.6.1" + "@types/node" ">=18" + bl "^6.0.11" + iconv-lite "^0.6.3" + js-md4 "^0.3.2" + native-duplexpair "^1.0.0" + sprintf-js "^1.1.3" + +teeny-request@^7.1.3: + version "7.2.0" + resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-7.2.0.tgz#41347ece068f08d741e7b86df38a4498208b2633" + integrity sha512-SyY0pek1zWsi0LRVAALem+avzMLc33MKW/JLLakdP4s9+D7+jHcy5x6P+h94g2QNZsAqQNfX5lsbd3WSeJXrrw== + dependencies: + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.1" + stream-events "^1.0.5" + uuid "^8.0.0" + +term-size@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" + integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== + +timers-ext@^0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.8.tgz#b4e442f10b7624a29dd2aa42c295e257150cf16c" + integrity sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww== + dependencies: + es5-ext "^0.10.64" + next-tick "^1.1.0" + +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +toposort-class@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988" + integrity sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg== + +touch@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694" + integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +tslib@^2.2.0, tslib@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@^1.6.18, type-is@^1.6.4, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +type@^2.7.2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486" + integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ== + +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + 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" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + 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" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + 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" + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +uid2@0.0.x: + version "0.0.4" + resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.4.tgz#033f3b1d5d32505f5ce5f888b9f3b667123c0a44" + integrity sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA== + +umzug@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/umzug/-/umzug-2.3.0.tgz#0ef42b62df54e216b05dcaf627830a6a8b84a184" + integrity sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw== + dependencies: + bluebird "^3.7.2" + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +undefsafe@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +underscore@^1.13.1: + version "1.13.6" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" + integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-notifier@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3" + integrity sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A== + dependencies: + boxen "^4.2.0" + chalk "^3.0.0" + configstore "^5.0.1" + has-yarn "^2.1.0" + import-lazy "^2.1.0" + is-ci "^2.0.0" + is-installed-globally "^0.3.1" + is-npm "^4.0.0" + is-yarn-global "^0.3.0" + latest-version "^5.0.0" + pupa "^2.0.1" + semver-diff "^3.1.1" + xdg-basedir "^4.0.0" + +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + integrity sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ== + dependencies: + prepend-http "^2.0.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1, utils-merge@1.x.x, utils-merge@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +validator@^13.7.0, validator@^13.9.0: + version "13.12.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.12.0.tgz#7d78e76ba85504da3fee4fd1922b385914d4b35f" + integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + 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" + +which-module@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== + +which-typed-array@^1.1.14, which-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + 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" + +which@2.0.2, which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + +wkx@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.5.0.tgz#c6c37019acf40e517cc6b94657a25a3d4aa33e8c" + integrity sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg== + dependencies: + "@types/node" "*" + +workerpool@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.0.0.tgz#85aad67fa1a2c8ef9386a1b43539900f61d03d58" + integrity sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@2.0.0-1: + version "2.0.0-1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.0-1.tgz#8c3029b3ee2028306d5bcf396980623115ff8d18" + integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== + +yargs-parser@13.1.2, yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^15.0.1: + version "15.0.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.3.tgz#316e263d5febe8b38eef61ac092b33dfcc9b1115" + integrity sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.1.tgz#bd4b0ee05b4c94d058929c32cb09e3fce71d3c5f" + integrity sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA== + dependencies: + camelcase "^5.3.1" + decamelize "^1.2.0" + flat "^4.1.0" + is-plain-obj "^1.1.0" + yargs "^14.2.3" + +yargs@13.3.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" + +yargs@^14.2.3: + version "14.2.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414" + integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg== + dependencies: + cliui "^5.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^15.0.1" + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + 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" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +z-schema@^5.0.1: + version "5.0.6" + resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-5.0.6.tgz#46d6a687b15e4a4369e18d6cb1c7b8618fc256c5" + integrity sha512-+XR1GhnWklYdfr8YaZv/iu+vY+ux7V5DS5zH1DQf6bO5ufrt/5cgNhVO5qyhsjFXvsqQb/f08DWE9b6uPscyAg== + dependencies: + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + validator "^13.7.0" + optionalDependencies: + commander "^10.0.0" diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000..f456727 --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,11 @@ +module.exports = { + extends: [ + 'next/core-web-vitals', + 'eslint-config-prettier', + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + ], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + root: true, +}; diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..cedf9c7 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,10 @@ +{ + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "quoteProps": "as-needed", + "jsxSingleQuote": true, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always" +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..56e10d0 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,19 @@ +FROM node:20.15.1-alpine + +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN yarn install +# If you are building your code for production +# RUN npm ci --only=production + +# Bundle app source +COPY . . + +EXPOSE 3000 +CMD [ "yarn", "dev" ] \ No newline at end of file diff --git a/frontend/LICENSE-justboil b/frontend/LICENSE-justboil new file mode 100644 index 0000000..798238d --- /dev/null +++ b/frontend/LICENSE-justboil @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019-current JustBoil.me (https://justboil.me) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..22d593b --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,91 @@ +# sev + +## This project was generated by Flatlogic Platform. + +## Install + +`cd` to project's dir and run `npm install` + +### Builds + +Build are handled by Next.js CLI — [Info](https://nextjs.org/docs/api-reference/cli) + +### Hot-reloads for development + +``` +npm run dev +``` + +### Builds and minifies for production + +``` +npm run build +``` + +### Exports build for static hosts + +``` +npm run export +``` + +### Lint + +``` +npm run lint +``` + +### Format with prettier + +``` +npm run format +``` + +## Support + +For any additional information please refer to [Flatlogic homepage](https://flatlogic.com). + +## To start the project with Docker: + +### Description: + +The project contains the **docker folder** and the `Dockerfile`. + +The `Dockerfile` is used to Deploy the project to Google Cloud. + +The **docker folder** contains a couple of helper scripts: + +- `docker-compose.yml` (all our services: web, backend, db are described here) +- `start-backend.sh` (starts backend, but only after the database) +- `wait-for-it.sh` (imported from https://github.com/vishnubob/wait-for-it) + + > To avoid breaking the application, we recommend you don't edit the following files: everything that includes the **docker folder** and `Dokerfile`. + +### Run services: + +1. Install docker compose (https://docs.docker.com/compose/install/) + +2. Move to `docker` folder. All next steps should be done from this folder. + + `cd docker` + +3. Make executables from `wait-for-it.sh` and `start-backend.sh`: + + `chmod +x start-backend.sh && chmod +x wait-for-it.sh` + +4. Download dependend projects for services. + +5. Review the docker-compose.yml file. Make sure that all services have Dockerfiles. Only db service doesn't require a Dockerfile. + +6. Make sure you have needed ports (see them in `ports`) available on your local machine. + +7. Start services: + + 7.1. With an empty database `rm -rf data && docker-compose up` + + 7.2. With a stored (from previus runs) database data `docker-compose up` + +8. Check http://localhost:3000 + +9. Stop services: + + 9.1. Just press `Ctr+C` diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts new file mode 100644 index 0000000..a4a7b3f --- /dev/null +++ b/frontend/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs new file mode 100644 index 0000000..d8c575e --- /dev/null +++ b/frontend/next.config.mjs @@ -0,0 +1,46 @@ +/** + * @type {import('next').NextConfig} + */ +const output = process.env.NODE_ENV === 'production' ? 'export' : 'standalone'; +const nextConfig = { + trailingSlash: true, + distDir: 'build', + output, + basePath: '', + swcMinify: false, + images: { + unoptimized: true, + remotePatterns: [ + { + protocol: 'https', + hostname: '**', + }, + ], + }, + + async rewrites() { + return [ + { + source: '/home', + destination: '/web_pages/home', + }, + + { + source: '/about', + destination: '/web_pages/about', + }, + + { + source: '/contact', + destination: '/web_pages/contact', + }, + + { + source: '/faq', + destination: '/web_pages/faq', + }, + ]; + }, +}; + +export default nextConfig; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..84dd863 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,71 @@ +{ + "private": true, + "scripts": { + "dev": "cross-env PORT=${FRONT_PORT:-3000} next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "format": "prettier '{components,pages,src,interfaces,hooks}/**/*.{tsx,ts,js}' --write" + }, + "dependencies": { + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "@mdi/js": "^7.4.47", + "@mui/material": "^6.3.0", + "@mui/x-data-grid": "^6.19.2", + "@reduxjs/toolkit": "^2.1.0", + "@tailwindcss/typography": "^0.5.13", + "@tinymce/tinymce-react": "^4.3.2", + "apexcharts": "^3.45.2", + "axios": "^1.6.7", + "chart.js": "^4.4.1", + "chroma-js": "^2.4.2", + "dayjs": "^1.11.10", + "file-saver": "^2.0.5", + "formik": "^2.4.5", + "intro.js": "^7.2.0", + "intro.js-react": "^1.0.0", + "jsonwebtoken": "^9.0.2", + "jwt-decode": "^3.1.2", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "next": "^14.1.0", + "numeral": "^2.0.6", + "query-string": "^8.1.0", + "react": "^19.0.0", + "react-apexcharts": "^1.4.1", + "react-big-calendar": "^1.10.3", + "react-chartjs-2": "^4.3.1", + "react-datepicker": "^4.10.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", + "react-dom": "^19.0.0", + "react-toastify": "^11.0.2", + "react-redux": "^8.0.2", + "react-select": "^5.7.0", + "react-select-async-paginate": "^0.7.9", + "react-switch": "^7.0.0", + "swr": "^1.3.0", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@tailwindcss/forms": "^0.5.7", + "@tailwindcss/line-clamp": "^0.4.4", + "@types/node": "18.7.16", + "@types/numeral": "^2.0.2", + "@types/react-big-calendar": "^1.8.8", + "@types/react-redux": "^7.1.24", + "@typescript-eslint/eslint-plugin": "^5.37.0", + "@typescript-eslint/parser": "^5.37.0", + "autoprefixer": "^10.4.0", + "cross-env": "^7.0.3", + "eslint": "^8.23.1", + "eslint-config-next": "^13.0.4", + "eslint-config-prettier": "^8.5.0", + "postcss": "^8.4.4", + "postcss-import": "^14.1.0", + "prettier": "^3.2.4", + "tailwindcss": "^3.4.1", + "typescript": "^4.8.3" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..5bee7ce --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,9 @@ +/* eslint-env node */ +module.exports = { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/prettier.config.js b/frontend/prettier.config.js new file mode 100644 index 0000000..d0ee549 --- /dev/null +++ b/frontend/prettier.config.js @@ -0,0 +1,13 @@ +module.exports = { + semi: false, + singleQuote: true, + printWidth: 100, + trailingComma: 'es5', + arrowParens: 'always', + tabWidth: 2, + useTabs: false, + quoteProps: 'as-needed', + jsxSingleQuote: false, + bracketSpacing: true, + bracketSameLine: false, +}; diff --git a/frontend/public/data-sources/clients.json b/frontend/public/data-sources/clients.json new file mode 100644 index 0000000..c97621a --- /dev/null +++ b/frontend/public/data-sources/clients.json @@ -0,0 +1,224 @@ +{ + "data": [ + { + "id": 19, + "avatar": "https://avatars.dicebear.com/v2/gridy/Howell-Hand.svg", + "login": "percy64", + "name": "Howell Hand", + "company": "Kiehn-Green", + "city": "Emelyside", + "progress": 70, + "created": "Mar 3, 2022", + "created_mm_dd_yyyy": "03-03-2022" + }, + { + "id": 11, + "avatar": "https://avatars.dicebear.com/v2/gridy/Hope-Howe.svg", + "login": "dare.concepcion", + "name": "Hope Howe", + "company": "Nolan Inc", + "city": "Paristown", + "progress": 68, + "created": "Dec 1, 2022", + "created_mm_dd_yyyy": "12-01-2022" + }, + { + "id": 32, + "avatar": "https://avatars.dicebear.com/v2/gridy/Nelson-Jerde.svg", + "login": "geovanni.kessler", + "name": "Nelson Jerde", + "company": "Nitzsche LLC", + "city": "Jailynbury", + "progress": 49, + "created": "May 18, 2022", + "created_mm_dd_yyyy": "05-18-2022" + }, + { + "id": 22, + "avatar": "https://avatars.dicebear.com/v2/gridy/Kim-Weimann.svg", + "login": "macejkovic.dashawn", + "name": "Kim Weimann", + "company": "Brown-Lueilwitz", + "city": "New Emie", + "progress": 38, + "created": "May 4, 2022", + "created_mm_dd_yyyy": "05-04-2022" + }, + { + "id": 34, + "avatar": "https://avatars.dicebear.com/v2/gridy/Justice-OReilly.svg", + "login": "hilpert.leora", + "name": "Justice O'Reilly", + "company": "Lakin-Muller", + "city": "New Kacie", + "progress": 38, + "created": "Mar 27, 2022", + "created_mm_dd_yyyy": "03-27-2022" + }, + { + "id": 48, + "avatar": "https://avatars.dicebear.com/v2/gridy/Adrienne-Mayer-III.svg", + "login": "ferry.sophia", + "name": "Adrienne Mayer III", + "company": "Kozey, McLaughlin and Kuhn", + "city": "Howardbury", + "progress": 39, + "created": "Mar 29, 2022", + "created_mm_dd_yyyy": "03-29-2022" + }, + { + "id": 20, + "avatar": "https://avatars.dicebear.com/v2/gridy/Mr.-Julien-Ebert.svg", + "login": "gokuneva", + "name": "Mr. Julien Ebert", + "company": "Cormier LLC", + "city": "South Serenaburgh", + "progress": 29, + "created": "Jun 25, 2022", + "created_mm_dd_yyyy": "06-25-2022" + }, + { + "id": 47, + "avatar": "https://avatars.dicebear.com/v2/gridy/Lenna-Smitham.svg", + "login": "paolo.walter", + "name": "Lenna Smitham", + "company": "King Inc", + "city": "McCulloughfort", + "progress": 59, + "created": "Oct 8, 2022", + "created_mm_dd_yyyy": "10-08-2022" + }, + { + "id": 24, + "avatar": "https://avatars.dicebear.com/v2/gridy/Travis-Davis.svg", + "login": "lkessler", + "name": "Travis Davis", + "company": "Leannon and Sons", + "city": "West Frankton", + "progress": 52, + "created": "Oct 20, 2022", + "created_mm_dd_yyyy": "10-20-2022" + }, + { + "id": 49, + "avatar": "https://avatars.dicebear.com/v2/gridy/Prof.-Esteban-Steuber.svg", + "login": "shana.lang", + "name": "Prof. Esteban Steuber", + "company": "Langosh-Ernser", + "city": "East Sedrick", + "progress": 34, + "created": "May 16, 2022", + "created_mm_dd_yyyy": "05-16-2022" + }, + { + "id": 36, + "avatar": "https://avatars.dicebear.com/v2/gridy/Russell-Goodwin-V.svg", + "login": "jewel07", + "name": "Russell Goodwin V", + "company": "Nolan-Stracke", + "city": "Williamsonmouth", + "progress": 55, + "created": "Apr 22, 2022", + "created_mm_dd_yyyy": "04-22-2022" + }, + { + "id": 33, + "avatar": "https://avatars.dicebear.com/v2/gridy/Ms.-Cassidy-Wiegand-DVM.svg", + "login": "burnice.okuneva", + "name": "Ms. Cassidy Wiegand DVM", + "company": "Kuhlman-Hahn", + "city": "New Ruthiehaven", + "progress": 76, + "created": "Sep 16, 2022", + "created_mm_dd_yyyy": "09-16-2022" + }, + { + "id": 44, + "avatar": "https://avatars.dicebear.com/v2/gridy/Mr.-Watson-Brakus-PhD.svg", + "login": "oconnell.juanita", + "name": "Mr. Watson Brakus PhD", + "company": "Osinski, Bins and Kuhn", + "city": "Lake Gloria", + "progress": 58, + "created": "Jun 22, 2022", + "created_mm_dd_yyyy": "06-22-2022" + }, + { + "id": 46, + "avatar": "https://avatars.dicebear.com/v2/gridy/Mr.-Garrison-Friesen-V.svg", + "login": "vgutmann", + "name": "Mr. Garrison Friesen V", + "company": "VonRueden, Rippin and Pfeffer", + "city": "Port Cieloport", + "progress": 39, + "created": "Oct 19, 2022", + "created_mm_dd_yyyy": "10-19-2022" + }, + { + "id": 14, + "avatar": "https://avatars.dicebear.com/v2/gridy/Ms.-Sister-Morar.svg", + "login": "veum.lucio", + "name": "Ms. Sister Morar", + "company": "Gusikowski, Altenwerth and Abbott", + "city": "Lake Macville", + "progress": 34, + "created": "Jun 11, 2022", + "created_mm_dd_yyyy": "06-11-2022" + }, + { + "id": 40, + "avatar": "https://avatars.dicebear.com/v2/gridy/Ms.-Laisha-Reinger.svg", + "login": "edietrich", + "name": "Ms. Laisha Reinger", + "company": "Boehm PLC", + "city": "West Alexiemouth", + "progress": 73, + "created": "Nov 2, 2022", + "created_mm_dd_yyyy": "11-02-2022" + }, + { + "id": 5, + "avatar": "https://avatars.dicebear.com/v2/gridy/Cameron-Lind.svg", + "login": "mose44", + "name": "Cameron Lind", + "company": "Tremblay, Padberg and Pouros", + "city": "Naderview", + "progress": 59, + "created": "Sep 14, 2022", + "created_mm_dd_yyyy": "09-14-2022" + }, + { + "id": 43, + "avatar": "https://avatars.dicebear.com/v2/gridy/Sarai-Little.svg", + "login": "rau.abelardo", + "name": "Sarai Little", + "company": "Deckow LLC", + "city": "Jeanieborough", + "progress": 49, + "created": "Jun 13, 2022", + "created_mm_dd_yyyy": "06-13-2022" + }, + { + "id": 2, + "avatar": "https://avatars.dicebear.com/v2/gridy/Shyann-Kautzer.svg", + "login": "imurazik", + "name": "Shyann Kautzer", + "company": "Osinski, Boehm and Kihn", + "city": "New Alvera", + "progress": 41, + "created": "Feb 15, 2022", + "created_mm_dd_yyyy": "02-15-2022" + }, + { + "id": 15, + "avatar": "https://avatars.dicebear.com/v2/gridy/Lorna-Christiansen.svg", + "login": "annalise97", + "name": "Lorna Christiansen", + "company": "Altenwerth-Friesen", + "city": "Port Elbertland", + "progress": 36, + "created": "Mar 9, 2022", + "created_mm_dd_yyyy": "03-09-2022" + } + ] +} diff --git a/frontend/public/data-sources/history.json b/frontend/public/data-sources/history.json new file mode 100644 index 0000000..12e81b6 --- /dev/null +++ b/frontend/public/data-sources/history.json @@ -0,0 +1,40 @@ +{ + "data": [ + { + "id": 1, + "amount": 375.53, + "account": "45721474", + "name": "Home Loan Account", + "date": "3 days ago", + "type": "deposit", + "business": "Turcotte" + }, + { + "id": 2, + "amount": 470.26, + "account": "94486537", + "name": "Savings Account", + "date": "3 days ago", + "type": "payment", + "business": "Murazik - Graham" + }, + { + "id": 3, + "amount": 971.34, + "account": "63189893", + "name": "Checking Account", + "date": "5 days ago", + "type": "invoice", + "business": "Fahey - Keebler" + }, + { + "id": 4, + "amount": 374.63, + "account": "74828780", + "name": "Auto Loan Account", + "date": "7 days ago", + "type": "withdraw", + "business": "Collier - Hintz" + } + ] +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..c8c4e3e --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/colors.ts b/frontend/src/colors.ts new file mode 100644 index 0000000..0fea7b8 --- /dev/null +++ b/frontend/src/colors.ts @@ -0,0 +1,145 @@ +import type { ColorButtonKey } from './interfaces'; + +export const gradientBgBase = 'bg-gradient-to-tr'; +export const colorBgBase = 'bg-violet-50/50'; +export const gradientBgPurplePink = `${gradientBgBase} from-purple-400 via-pink-500 to-red-500`; +export const gradientBgViolet = `${gradientBgBase} ${colorBgBase}`; +export const gradientBgDark = `${gradientBgBase} from-dark-700 via-dark-900 to-dark-800`; +export const gradientBgPinkRed = `${gradientBgBase} from-pink-400 via-red-500 to-yellow-500`; + +export const colorsBgLight = { + white: 'bg-white text-black', + light: ' bg-white text-black text-black dark:bg-dark-900 dark:text-white', + contrast: 'bg-gray-800 text-white dark:bg-white dark:text-black', + success: + 'bg-emerald-500 border-emerald-500 dark:bg-pavitra-blue dark:border-pavitra-blue text-white', + danger: 'bg-red-500 border-red-500 text-white', + warning: 'bg-yellow-500 border-yellow-500 text-white', + info: 'bg-blue-500 border-blue-500 dark:bg-pavitra-blue dark:border-pavitra-blue text-white', +}; + +export const colorsText = { + white: 'text-black dark:text-slate-100', + light: 'text-gray-700 dark:text-slate-400', + contrast: 'dark:text-white', + success: 'text-emerald-500', + danger: 'text-red-500', + warning: 'text-yellow-500', + info: 'text-blue-500', +}; + +export const colorsOutline = { + white: [colorsText.white, 'border-gray-100'].join(' '), + light: [colorsText.light, 'border-gray-100'].join(' '), + contrast: [colorsText.contrast, 'border-gray-900 dark:border-slate-100'].join( + ' ', + ), + success: [colorsText.success, 'border-emerald-500'].join(' '), + danger: [colorsText.danger, 'border-red-500'].join(' '), + warning: [colorsText.warning, 'border-yellow-500'].join(' '), + info: [colorsText.info, 'border-blue-500'].join(' '), +}; + +export const getButtonColor = ( + color: ColorButtonKey, + isOutlined: boolean, + hasHover: boolean, + isActive = false, +) => { + if (color === 'void') { + return ''; + } + + const colors = { + ring: { + white: 'ring-gray-200 dark:ring-gray-500', + whiteDark: 'ring-gray-200 dark:ring-dark-500', + lightDark: 'ring-gray-200 dark:ring-gray-500', + contrast: 'ring-gray-300 dark:ring-gray-400', + success: 'ring-emerald-300 dark:ring-pavitra-blue', + danger: 'ring-red-300 dark:ring-red-700', + warning: 'ring-yellow-300 dark:ring-yellow-700', + info: 'ring-blue-300 dark:ring-pavitra-blue', + }, + active: { + white: 'bg-gray-100', + whiteDark: 'bg-gray-100 dark:bg-dark-800', + lightDark: 'bg-gray-200 dark:bg-slate-700', + contrast: 'bg-gray-700 dark:bg-slate-100', + success: 'bg-emerald-700 dark:bg-pavitra-blue', + danger: 'bg-red-700 dark:bg-red-600', + warning: 'bg-yellow-700 dark:bg-yellow-600', + info: 'bg-blue-700 dark:bg-pavitra-blue', + }, + bg: { + white: 'bg-white text-black', + whiteDark: 'bg-white text-black dark:bg-dark-900 dark:text-white', + lightDark: 'bg-gray-100 text-black dark:bg-slate-800 dark:text-white', + contrast: 'bg-gray-800 text-white dark:bg-white dark:text-black', + success: 'bg-emerald-600 dark:bg-pavitra-blue text-white', + danger: 'bg-red-600 text-white dark:bg-red-500 ', + warning: 'bg-yellow-600 dark:bg-yellow-500 text-white', + info: ' bg-blue-600 dark:bg-pavitra-blue text-white ', + }, + bgHover: { + white: 'hover:bg-gray-100', + whiteDark: 'hover:bg-gray-100 hover:dark:bg-dark-800', + lightDark: 'hover:bg-gray-200 hover:dark:bg-slate-700', + contrast: 'hover:bg-gray-700 hover:dark:bg-slate-100', + success: + 'hover:bg-emerald-700 hover:border-emerald-700 hover:dark:bg-pavitra-blue hover:dark:border-pavitra-blue', + danger: + 'hover:bg-red-700 hover:border-red-700 hover:dark:bg-red-600 hover:dark:border-red-600', + warning: + 'hover:bg-yellow-700 hover:border-yellow-700 hover:dark:bg-yellow-600 hover:dark:border-yellow-600', + info: 'hover:bg-blue-700 hover:border-blue-700 hover:dark:bg-pavitra-blue/80 hover:dark:border-pavitra-blue/80', + }, + borders: { + white: 'border-white', + whiteDark: 'border-white dark:border-dark-900', + lightDark: 'border-gray-100 dark:border-slate-800', + contrast: 'border-gray-800 dark:border-white', + success: 'border-emerald-600 dark:border-pavitra-blue', + danger: 'border-red-600 dark:border-red-500', + warning: 'border-yellow-600 dark:border-yellow-500', + info: 'border-blue-600 border-blue-600 dark:border-pavitra-blue', + }, + text: { + contrast: 'dark:text-slate-100', + success: 'text-emerald-600 dark:text-pavitra-blue', + danger: 'text-red-600 dark:text-red-500', + warning: 'text-yellow-600 dark:text-yellow-500', + info: 'text-blue-600 dark:text-pavitra-blue', + }, + outlineHover: { + contrast: + 'hover:bg-gray-800 hover:text-gray-100 hover:dark:bg-slate-100 hover:dark:text-black', + success: + 'hover:bg-emerald-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-pavitra-blue', + danger: + 'hover:bg-red-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-red-600', + warning: + 'hover:bg-yellow-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-yellow-600', + info: 'hover:bg-blue-600 hover:bg-blue-600 hover:text-white hover:dark:text-white hover:dark:border-pavitra-blue', + }, + }; + + const isOutlinedProcessed = + isOutlined && ['white', 'whiteDark', 'lightDark'].indexOf(color) < 0; + + const base = [colors.borders[color], colors.ring[color]]; + + if (isActive) { + base.push(colors.active[color]); + } else { + base.push(isOutlinedProcessed ? colors.text[color] : colors.bg[color]); + } + + if (hasHover) { + base.push( + isOutlinedProcessed ? colors.outlineHover[color] : colors.bgHover[color], + ); + } + + return base.join(' '); +}; diff --git a/frontend/src/components/Access_logs/CardAccess_logs.tsx b/frontend/src/components/Access_logs/CardAccess_logs.tsx new file mode 100644 index 0000000..bf3e645 --- /dev/null +++ b/frontend/src/components/Access_logs/CardAccess_logs.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + access_logs: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardAccess_logs = ({ + access_logs, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ACCESS_LOGS'); + + return ( +
+ {loading && } +
    + {!loading && + access_logs.map((item, index) => ( +
  • +
    + + {item.access_time} + + +
    + +
    +
    +
    +
    +
    User
    +
    +
    + {dataFormatter.usersOneListFormatter(item.user)} +
    +
    +
    + +
    +
    + QRCode +
    +
    +
    + {dataFormatter.qr_codesOneListFormatter(item.qr_code)} +
    +
    +
    + +
    +
    + AccessTime +
    +
    +
    + {dataFormatter.dateTimeFormatter(item.access_time)} +
    +
    +
    +
    +
  • + ))} + {!loading && access_logs.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardAccess_logs; diff --git a/frontend/src/components/Access_logs/ListAccess_logs.tsx b/frontend/src/components/Access_logs/ListAccess_logs.tsx new file mode 100644 index 0000000..4440ddc --- /dev/null +++ b/frontend/src/components/Access_logs/ListAccess_logs.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + access_logs: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListAccess_logs = ({ + access_logs, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ACCESS_LOGS'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + access_logs.map((item) => ( + +
+ dark:divide-dark-700 overflow-x-auto' + } + > +
+

User

+

+ {dataFormatter.usersOneListFormatter(item.user)} +

+
+ +
+

QRCode

+

+ {dataFormatter.qr_codesOneListFormatter(item.qr_code)} +

+
+ +
+

AccessTime

+

+ {dataFormatter.dateTimeFormatter(item.access_time)} +

+
+ + +
+
+ ))} + {!loading && access_logs.length === 0 && ( +
+

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListAccess_logs; diff --git a/frontend/src/components/Access_logs/TableAccess_logs.tsx b/frontend/src/components/Access_logs/TableAccess_logs.tsx new file mode 100644 index 0000000..64f5bf6 --- /dev/null +++ b/frontend/src/components/Access_logs/TableAccess_logs.tsx @@ -0,0 +1,497 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/access_logs/access_logsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureAccess_logsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import ListAccess_logs from './ListAccess_logs'; + +const perPage = 10; + +const TableSampleAccess_logs = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + access_logs, + loading, + count, + notify: access_logsNotify, + refetch, + } = useAppSelector((state) => state.access_logs); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (access_logsNotify.showNotification) { + notify( + access_logsNotify.typeNotification, + access_logsNotify.textNotification, + ); + } + }, [access_logsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `access_logs`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
+ `datagrid--row`} + rows={access_logs ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
+ ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
+ <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
+
+
+ Filter +
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
+
Value
+ + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : ( +
+
+ Contains +
+ +
+ )} +
+
+ Action +
+ { + deleteFilter(filterItem.id); + }} + /> +
+
+ ); + })} +
+ + +
+ +
+
+
+ ) : null} + +

Are you sure you want to delete this item?

+
+ + {access_logs && Array.isArray(access_logs) && !showGrid && ( + + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleAccess_logs; diff --git a/frontend/src/components/Access_logs/configureAccess_logsCols.tsx b/frontend/src/components/Access_logs/configureAccess_logsCols.tsx new file mode 100644 index 0000000..3fd4845 --- /dev/null +++ b/frontend/src/components/Access_logs/configureAccess_logsCols.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_ACCESS_LOGS'); + + return [ + { + field: 'user', + headerName: 'User', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'qr_code', + headerName: 'QRCode', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('qr_codes'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'access_time', + headerName: 'AccessTime', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.access_time), + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ + , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/AsideMenu.tsx b/frontend/src/components/AsideMenu.tsx new file mode 100644 index 0000000..0a1c120 --- /dev/null +++ b/frontend/src/components/AsideMenu.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { MenuAsideItem } from '../interfaces'; +import AsideMenuLayer from './AsideMenuLayer'; +import OverlayLayer from './OverlayLayer'; + +type Props = { + menu: MenuAsideItem[]; + isAsideMobileExpanded: boolean; + isAsideLgActive: boolean; + onAsideLgClose: () => void; +}; + +export default function AsideMenu({ + isAsideMobileExpanded = false, + isAsideLgActive = false, + ...props +}: Props) { + return ( + <> + + {isAsideLgActive && ( + + )} + + ); +} diff --git a/frontend/src/components/AsideMenuItem.tsx b/frontend/src/components/AsideMenuItem.tsx new file mode 100644 index 0000000..c5a8b8a --- /dev/null +++ b/frontend/src/components/AsideMenuItem.tsx @@ -0,0 +1,116 @@ +import React, { useEffect, useState } from 'react'; +import { mdiMinus, mdiPlus } from '@mdi/js'; +import BaseIcon from './BaseIcon'; +import Link from 'next/link'; +import { getButtonColor } from '../colors'; +import AsideMenuList from './AsideMenuList'; +import { MenuAsideItem } from '../interfaces'; +import { useAppSelector } from '../stores/hooks'; +import { useRouter } from 'next/router'; + +type Props = { + item: MenuAsideItem; + isDropdownList?: boolean; +}; + +const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { + const [isLinkActive, setIsLinkActive] = useState(false); + const [isDropdownActive, setIsDropdownActive] = useState(false); + + const asideMenuItemStyle = useAppSelector( + (state) => state.style.asideMenuItemStyle, + ); + const asideMenuDropdownStyle = useAppSelector( + (state) => state.style.asideMenuDropdownStyle, + ); + const asideMenuItemActiveStyle = useAppSelector( + (state) => state.style.asideMenuItemActiveStyle, + ); + const borders = useAppSelector((state) => state.style.borders); + const activeLinkColor = useAppSelector( + (state) => state.style.activeLinkColor, + ); + const activeClassAddon = + !item.color && isLinkActive ? asideMenuItemActiveStyle : ''; + + const { asPath, isReady } = useRouter(); + + useEffect(() => { + if (item.href && isReady) { + const linkPathName = new URL(item.href, location.href).pathname + '/'; + const activePathname = new URL(asPath, location.href).pathname; + + const activeView = activePathname.split('/')[1]; + const linkPathNameView = linkPathName.split('/')[1]; + + setIsLinkActive(linkPathNameView === activeView); + } + }, [item.href, isReady, asPath]); + + const asideMenuItemInnerContents = ( + <> + {item.icon && ( + + )} + + {item.label} + + {item.menu && ( + + )} + + ); + + const componentClass = [ + 'flex cursor-pointer py-1.5 ', + isDropdownList ? 'px-6 text-sm' : '', + item.color + ? getButtonColor(item.color, false, true) + : `${asideMenuItemStyle}`, + isLinkActive + ? `text-black ${activeLinkColor} dark:text-white dark:bg-dark-800` + : '', + ].join(' '); + + return ( +
  • + {item.withDevider &&
    } + {item.href && ( + + {asideMenuItemInnerContents} + + )} + {!item.href && ( +
    setIsDropdownActive(!isDropdownActive)} + > + {asideMenuItemInnerContents} +
    + )} + {item.menu && ( + + )} +
  • + ); +}; + +export default AsideMenuItem; diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx new file mode 100644 index 0000000..c299237 --- /dev/null +++ b/frontend/src/components/AsideMenuLayer.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { mdiLogout, mdiClose } from '@mdi/js'; +import BaseIcon from './BaseIcon'; +import AsideMenuList from './AsideMenuList'; +import { MenuAsideItem } from '../interfaces'; +import { useAppSelector } from '../stores/hooks'; +import Link from 'next/link'; + +type Props = { + menu: MenuAsideItem[]; + className?: string; + onAsideLgCloseClick: () => void; +}; + +export default function AsideMenuLayer({ + menu, + className = '', + ...props +}: Props) { + const asideStyle = useAppSelector((state) => state.style.asideStyle); + const asideBrandStyle = useAppSelector( + (state) => state.style.asideBrandStyle, + ); + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const darkMode = useAppSelector((state) => state.style.darkMode); + + const handleAsideLgCloseClick = (e: React.MouseEvent) => { + e.preventDefault(); + props.onAsideLgCloseClick(); + }; + + return ( + + ); +} diff --git a/frontend/src/components/AsideMenuList.tsx b/frontend/src/components/AsideMenuList.tsx new file mode 100644 index 0000000..1220c79 --- /dev/null +++ b/frontend/src/components/AsideMenuList.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { MenuAsideItem } from '../interfaces'; +import AsideMenuItem from './AsideMenuItem'; +import { useAppSelector } from '../stores/hooks'; +import { hasPermission } from '../helpers/userPermissions'; + +type Props = { + menu: MenuAsideItem[]; + isDropdownList?: boolean; + className?: string; +}; + +export default function AsideMenuList({ + menu, + isDropdownList = false, + className = '', +}: Props) { + const { currentUser } = useAppSelector((state) => state.auth); + + if (!currentUser) return null; + + return ( +
      + {menu.map((item, index) => { + if (!hasPermission(currentUser, item.permissions)) return null; + + return ( + + ); + })} +
    + ); +} diff --git a/frontend/src/components/BaseButton.tsx b/frontend/src/components/BaseButton.tsx new file mode 100644 index 0000000..cb87f90 --- /dev/null +++ b/frontend/src/components/BaseButton.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import Link from 'next/link'; +import { getButtonColor } from '../colors'; +import BaseIcon from './BaseIcon'; +import type { ColorButtonKey } from '../interfaces'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + label?: string; + icon?: string; + iconSize?: string | number; + href?: string; + target?: string; + type?: string; + color?: ColorButtonKey; + className?: string; + iconClassName?: string; + asAnchor?: boolean; + small?: boolean; + outline?: boolean; + active?: boolean; + disabled?: boolean; + roundedFull?: boolean; + onClick?: (e: React.MouseEvent) => void; +}; + +export default function BaseButton({ + label, + icon, + iconSize, + href, + target, + type, + color = 'white', + className = '', + iconClassName = '', + asAnchor = false, + small = false, + outline = false, + active = false, + disabled = false, + roundedFull = false, + onClick, +}: Props) { + const corners = useAppSelector((state) => state.style.corners); + const componentClass = [ + 'inline-flex', + 'justify-center', + 'items-center', + 'whitespace-nowrap', + 'focus:outline-none', + 'transition-colors', + 'focus:ring', + 'duration-150', + 'border', + disabled ? 'cursor-not-allowed' : 'cursor-pointer', + roundedFull ? 'rounded-full' : `${corners}`, + getButtonColor(color, outline, !disabled, active), + className, + ]; + + if (!label && icon) { + componentClass.push('p-1'); + } else if (small) { + componentClass.push('text-sm', roundedFull ? 'px-3 py-1' : 'p-1'); + } else { + componentClass.push('py-2', roundedFull ? 'px-6' : 'px-3'); + } + + if (disabled) { + componentClass.push(outline ? 'opacity-50' : 'opacity-70'); + } + + const componentClassString = componentClass.join(' '); + + const componentChildren = ( + <> + {icon && ( + + )} + {label && ( + {label} + )} + + ); + + if (href && !disabled) { + return ( + + {componentChildren} + + ); + } + + return React.createElement( + asAnchor ? 'a' : 'button', + { + className: componentClassString, + type: type ?? 'button', + target, + disabled, + onClick, + }, + componentChildren, + ); +} diff --git a/frontend/src/components/BaseButtons.tsx b/frontend/src/components/BaseButtons.tsx new file mode 100644 index 0000000..86eb456 --- /dev/null +++ b/frontend/src/components/BaseButtons.tsx @@ -0,0 +1,38 @@ +import { Children, cloneElement, ReactElement } from 'react'; +import type { ReactNode } from 'react'; + +type Props = { + type?: string; + mb?: string; + noWrap?: boolean; + classAddon?: string; + children: ReactNode; + className?: string; +}; + +const BaseButtons = ({ + type = 'justify-end', + mb = '-mb-3', + classAddon = 'mr-3 last:mr-0 mb-3', + noWrap = false, + children, + className, +}: Props) => { + return ( +
    + {Children.map(children, (child: ReactElement) => + child + ? cloneElement(child, { + className: `${classAddon} ${child.props.className}`, + }) + : null, + )} +
    + ); +}; + +export default BaseButtons; diff --git a/frontend/src/components/BaseDivider.tsx b/frontend/src/components/BaseDivider.tsx new file mode 100644 index 0000000..52e7f29 --- /dev/null +++ b/frontend/src/components/BaseDivider.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { useAppSelector } from '../stores/hooks'; +type Props = { + navBar?: boolean; +}; + +export default function BaseDivider({ navBar = false }: Props) { + const borders = useAppSelector((state) => state.style.borders); + const classAddon = navBar + ? 'hidden lg:block lg:my-0.5 dark:border-dark-700' + : 'my-6 -mx-6 dark:border-dark-800'; + + return
    ; +} diff --git a/frontend/src/components/BaseIcon.tsx b/frontend/src/components/BaseIcon.tsx new file mode 100644 index 0000000..d26fe1c --- /dev/null +++ b/frontend/src/components/BaseIcon.tsx @@ -0,0 +1,39 @@ +import React, { ReactNode } from 'react'; + +type Props = { + path: string; + w?: string; + h?: string; + fill?: string; + size?: string | number | null; + className?: string; + children?: ReactNode; +}; + +export default function BaseIcon({ + path, + fill, + w = 'w-6', + h = 'h-6', + size = null, + className = '', + children, +}: Props) { + const iconSize = size ?? 16; + + return ( + + + + + {children} + + ); +} diff --git a/frontend/src/components/BigCalendar.tsx b/frontend/src/components/BigCalendar.tsx new file mode 100644 index 0000000..5d3c01c --- /dev/null +++ b/frontend/src/components/BigCalendar.tsx @@ -0,0 +1,171 @@ +import React, { useEffect, useMemo, useState, useRef } from 'react'; +import { + Calendar, + Views, + momentLocalizer, + SlotInfo, + EventProps, +} from 'react-big-calendar'; +import moment from 'moment'; +import 'react-big-calendar/lib/css/react-big-calendar.css'; +import ListActionsPopover from './ListActionsPopover'; +import Link from 'next/link'; + +import { useAppSelector } from '../stores/hooks'; +import { hasPermission } from '../helpers/userPermissions'; + +const localizer = momentLocalizer(moment); + +type TEvent = { + id: string; + title: string; + start: Date; + end: Date; +}; + +type Props = { + events: any[]; + handleDeleteAction: (id: string) => void; + handleCreateEventAction: (slotInfo: SlotInfo) => void; + onDateRangeChange: (range: { start: string; end: string }) => void; + entityName: string; + showField: string; + pathEdit?: string; + pathView?: string; + 'start-data-key': string; + 'end-data-key': string; +}; + +const BigCalendar = ({ + events, + handleDeleteAction, + handleCreateEventAction, + onDateRangeChange, + entityName, + showField, + pathEdit, + pathView, + 'start-data-key': startDataKey, + 'end-data-key': endDataKey, +}: Props) => { + const [myEvents, setMyEvents] = useState([]); + const prevRange = useRef<{ start: string; end: string }>(); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = + currentUser && + hasPermission(currentUser, `UPDATE_${entityName.toUpperCase()}`); + const hasCreatePermission = + currentUser && + hasPermission(currentUser, `CREATE_${entityName.toUpperCase()}`); + + const { defaultDate, scrollToTime } = useMemo( + () => ({ + defaultDate: new Date(), + scrollToTime: new Date(1970, 1, 1, 6), + }), + [], + ); + + useEffect(() => { + if (!events || !Array.isArray(events) || !events?.length) return; + + const formattedEvents = events.map((event) => ({ + ...event, + start: new Date(event[startDataKey]), + end: new Date(event[endDataKey]), + title: event[showField], + })); + + setMyEvents(formattedEvents); + }, [endDataKey, events, startDataKey, showField]); + + const onRangeChange = (range: Date[] | { start: Date; end: Date }) => { + const newRange = { start: '', end: '' }; + const format = 'YYYY-MM-DDTHH:mm'; + + if (Array.isArray(range)) { + newRange.start = moment(range[0]).format(format); + newRange.end = moment(range[range.length - 1]).format(format); + } else { + newRange.start = moment(range.start).format(format); + newRange.end = moment(range.end).format(format); + } + + if (newRange.start === newRange.end) { + newRange.end = moment(newRange.end).add(1, 'days').format(format); + } + + // check if the range fits in the previous range + if ( + prevRange.current && + prevRange.current.start <= newRange.start && + prevRange.current.end >= newRange.end + ) { + return; + } + + prevRange.current = { start: newRange.start, end: newRange.end }; + onDateRangeChange(newRange); + }; + + return ( +
    + ( + + ), + }} + /> +
    + ); +}; + +const MyCustomEvent = ( + props: { + onDelete: (id: string) => void; + hasUpdatePermission: boolean; + pathEdit?: string; + pathView?: string; + } & EventProps, +) => { + const { onDelete, hasUpdatePermission, title, event, pathEdit, pathView } = + props; + + return ( +
    + + {title} + + +
    + ); +}; + +export default BigCalendar; diff --git a/frontend/src/components/CardBox.tsx b/frontend/src/components/CardBox.tsx new file mode 100644 index 0000000..09b11aa --- /dev/null +++ b/frontend/src/components/CardBox.tsx @@ -0,0 +1,70 @@ +import React, { ReactNode } from 'react'; +import CardBoxComponentBody from './CardBoxComponentBody'; +import CardBoxComponentFooter from './CardBoxComponentFooter'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + rounded?: string; + flex?: string; + className?: string; + hasComponentLayout?: boolean; + cardBoxClassName?: string; + hasTable?: boolean; + isHoverable?: boolean; + isModal?: boolean; + children: ReactNode; + footer?: ReactNode; + isList?: boolean; + id?: string; + onClick?: (e: React.MouseEvent) => void; +}; + +export default function CardBox({ + rounded = 'rounded', + flex = 'flex-col', + className = '', + hasComponentLayout = false, + cardBoxClassName = '', + hasTable = false, + isHoverable = false, + isList = false, + isModal = false, + children, + footer, + id = '', + onClick, +}: Props) { + const corners = useAppSelector((state) => state.style.corners); + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + const componentClass = [ + `flex dark:border-dark-700 dark:bg-dark-900`, + className, + corners !== 'rounded-full' ? corners : 'rounded-3xl', + flex, + isList ? '' : `${cardsStyle}`, + hasTable ? '' : `border-dark-700 dark:border-dark-700`, + ]; + + if (isHoverable) { + componentClass.push('hover:shadow-lg transition-shadow duration-500'); + } + + return React.createElement( + 'div', + { className: componentClass.join(' '), onClick }, + hasComponentLayout ? ( + children + ) : ( + <> + + {children} + + {footer && {footer}} + + ), + ); +} diff --git a/frontend/src/components/CardBoxComponentBody.tsx b/frontend/src/components/CardBoxComponentBody.tsx new file mode 100644 index 0000000..12448d8 --- /dev/null +++ b/frontend/src/components/CardBoxComponentBody.tsx @@ -0,0 +1,21 @@ +import React, { ReactNode } from 'react'; + +type Props = { + noPadding?: boolean; + className?: string; + children?: ReactNode; + id?: string; +}; + +export default function CardBoxComponentBody({ + noPadding = false, + className, + children, + id, +}: Props) { + return ( +
    + {children} +
    + ); +} diff --git a/frontend/src/components/CardBoxComponentEmpty.tsx b/frontend/src/components/CardBoxComponentEmpty.tsx new file mode 100644 index 0000000..c9072bb --- /dev/null +++ b/frontend/src/components/CardBoxComponentEmpty.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const CardBoxComponentEmpty = () => { + return ( +
    +

    Nothing's here…

    +
    + ); +}; + +export default CardBoxComponentEmpty; diff --git a/frontend/src/components/CardBoxComponentFooter.tsx b/frontend/src/components/CardBoxComponentFooter.tsx new file mode 100644 index 0000000..184a058 --- /dev/null +++ b/frontend/src/components/CardBoxComponentFooter.tsx @@ -0,0 +1,10 @@ +import React, { ReactNode } from 'react'; + +type Props = { + className?: string; + children?: ReactNode; +}; + +export default function CardBoxComponentFooter({ className, children }: Props) { + return
    {children}
    ; +} diff --git a/frontend/src/components/CardBoxComponentTitle.tsx b/frontend/src/components/CardBoxComponentTitle.tsx new file mode 100644 index 0000000..20990e6 --- /dev/null +++ b/frontend/src/components/CardBoxComponentTitle.tsx @@ -0,0 +1,17 @@ +import React, { ReactNode } from 'react'; + +type Props = { + title: string; + children?: ReactNode; +}; + +const CardBoxComponentTitle = ({ title, children }: Props) => { + return ( +
    +

    {title}

    + {children} +
    + ); +}; + +export default CardBoxComponentTitle; diff --git a/frontend/src/components/CardBoxModal.tsx b/frontend/src/components/CardBoxModal.tsx new file mode 100644 index 0000000..c87c0c5 --- /dev/null +++ b/frontend/src/components/CardBoxModal.tsx @@ -0,0 +1,75 @@ +import { mdiClose } from '@mdi/js'; +import { ReactNode } from 'react'; +import type { ColorButtonKey } from '../interfaces'; +import BaseButton from './BaseButton'; +import BaseButtons from './BaseButtons'; +import CardBox from './CardBox'; +import CardBoxComponentTitle from './CardBoxComponentTitle'; +import OverlayLayer from './OverlayLayer'; + +type Props = { + title: string; + buttonColor: ColorButtonKey; + buttonLabel: string; + isActive: boolean; + children: ReactNode; + onConfirm: () => void; + onCancel?: () => void; +}; + +const CardBoxModal = ({ + title, + buttonColor, + buttonLabel, + isActive, + children, + onConfirm, + onCancel, +}: Props) => { + if (!isActive) { + return null; + } + + const footer = ( + + + {!!onCancel && ( + + )} + + ); + + return ( + + + + {!!onCancel && ( + + )} + + +
    {children}
    +
    +
    + ); +}; + +export default CardBoxModal; diff --git a/frontend/src/components/ChartLineSample/config.ts b/frontend/src/components/ChartLineSample/config.ts new file mode 100644 index 0000000..c29cbdd --- /dev/null +++ b/frontend/src/components/ChartLineSample/config.ts @@ -0,0 +1,54 @@ +export const chartColors = { + default: { + primary: '#00D1B2', + info: '#209CEE', + danger: '#FF3860', + }, +}; + +const randomChartData = (n: number) => { + const data = []; + + for (let i = 0; i < n; i++) { + data.push(Math.round(Math.random() * 200)); + } + + return data; +}; + +const datasetObject = (color: string, points: number) => { + return { + fill: false, + borderColor: chartColors.default[color], + borderWidth: 2, + borderDash: [], + borderDashOffset: 0.0, + pointBackgroundColor: chartColors.default[color], + pointBorderColor: 'rgba(255,255,255,0)', + pointHoverBackgroundColor: chartColors.default[color], + pointBorderWidth: 20, + pointHoverRadius: 4, + pointHoverBorderWidth: 15, + pointRadius: 4, + data: randomChartData(points), + tension: 0.5, + cubicInterpolationMode: 'default', + }; +}; + +export const sampleChartData = (points = 9) => { + const labels = []; + + for (let i = 1; i <= points; i++) { + labels.push(`0${i}`); + } + + return { + labels, + datasets: [ + datasetObject('primary', points), + datasetObject('info', points), + datasetObject('danger', points), + ], + }; +}; diff --git a/frontend/src/components/ChartLineSample/index.tsx b/frontend/src/components/ChartLineSample/index.tsx new file mode 100644 index 0000000..0761549 --- /dev/null +++ b/frontend/src/components/ChartLineSample/index.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { + Chart, + LineElement, + PointElement, + LineController, + LinearScale, + CategoryScale, + Tooltip, +} from 'chart.js'; +import { Line } from 'react-chartjs-2'; + +Chart.register( + LineElement, + PointElement, + LineController, + LinearScale, + CategoryScale, + Tooltip, +); + +const options = { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + display: false, + }, + x: { + display: true, + }, + }, + plugins: { + legend: { + display: false, + }, + }, +}; + +const ChartLineSample = ({ data }) => { + return ; +}; + +export default ChartLineSample; diff --git a/frontend/src/components/ClickOutside.tsx b/frontend/src/components/ClickOutside.tsx new file mode 100644 index 0000000..d878647 --- /dev/null +++ b/frontend/src/components/ClickOutside.tsx @@ -0,0 +1,29 @@ +import React, { useCallback, useEffect, useRef } from 'react'; + +const ClickOutside = ({ children, onClickOutside, excludedElements }) => { + const wrapperRef = useRef(null); + + const handleClickOutside = useCallback( + (event) => { + if ( + wrapperRef.current && + !wrapperRef.current.contains(event.target) && + !excludedElements.some((el) => el.current.contains(event.target)) + ) { + onClickOutside(); + } + }, + [wrapperRef, onClickOutside, ...excludedElements], + ); + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [handleClickOutside]); + + return
    {children}
    ; +}; + +export default ClickOutside; diff --git a/frontend/src/components/DataGridMultiSelect.tsx b/frontend/src/components/DataGridMultiSelect.tsx new file mode 100644 index 0000000..bb82434 --- /dev/null +++ b/frontend/src/components/DataGridMultiSelect.tsx @@ -0,0 +1,55 @@ +import { GridRenderEditCellParams, useGridApiContext } from '@mui/x-data-grid'; +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import { MenuItem, Select } from '@mui/material'; + +interface Props { + entityName: string; +} + +const DataGridMultiSelect = (props: GridRenderEditCellParams & Props) => { + const { id, value, field, entityName } = props; + const apiRef = useGridApiContext(); + const [options, setOptions] = useState([]); + + async function callApi(entityName: string) { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } + + useEffect(() => { + callApi(entityName).then((data) => { + setOptions(data); + }); + }, []); + + const handleChange = (event) => { + const eventValue = event.target.value; // The new value entered by the user + + const newValue = + typeof eventValue === 'string' ? value.split(',') : eventValue; + + apiRef.current.setEditCellValue({ + id, + field, + value: newValue.filter((x) => x !== ''), + }); + }; + + return ( + + ); +}; + +export default DataGridMultiSelect; diff --git a/frontend/src/components/DragDropFilePicker.tsx b/frontend/src/components/DragDropFilePicker.tsx new file mode 100644 index 0000000..821570d --- /dev/null +++ b/frontend/src/components/DragDropFilePicker.tsx @@ -0,0 +1,124 @@ +import React, { ChangeEvent, useEffect, useState } from 'react'; +import BaseIcon from './BaseIcon'; +import { mdiFileUploadOutline } from '@mdi/js'; + +type Props = { + file: File | null; + setFile: (file: File) => void; + formats?: string; +}; + +const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => { + const [highlight, setHighlight] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + const fileInput = React.createRef(); + + useEffect(() => { + if (!file && fileInput) fileInput.current.value = ''; + }, [file, fileInput]); + + function onFilesAdded(files: FileList | null) { + if (files && files[0]) { + const newFile = files[0]; + const fileExtension = newFile.name.split('.').pop().toLowerCase(); + + if (formats.includes(fileExtension) || !formats) { + setFile(newFile); + setErrorMessage(''); + } else { + setErrorMessage(`Allowed formats: ${formats}`); + } + } + } + + function onDragOver(e) { + e.preventDefault(); + setHighlight(true); + } + + function onDragLeave() { + setHighlight(false); + } + + function onDrop(e) { + e.preventDefault(); + + const files = e.dataTransfer.files; + + onFilesAdded(files); + setHighlight(false); + } + + const onClear = () => { + setFile(null); + setErrorMessage(''); + }; + + return ( +
    + +
    + ); +}; + +export default DragDropFilePicker; diff --git a/frontend/src/components/FooterBar.tsx b/frontend/src/components/FooterBar.tsx new file mode 100644 index 0000000..0f6b2b5 --- /dev/null +++ b/frontend/src/components/FooterBar.tsx @@ -0,0 +1,34 @@ +import React, { ReactNode } from 'react'; +import { containerMaxW } from '../config'; +import Logo from './Logo'; + +type Props = { + children?: ReactNode; +}; + +export default function FooterBar({ children }: Props) { + const year = new Date().getFullYear(); + + return ( + + ); +} diff --git a/frontend/src/components/FormCheckRadio.tsx b/frontend/src/components/FormCheckRadio.tsx new file mode 100644 index 0000000..17c00ea --- /dev/null +++ b/frontend/src/components/FormCheckRadio.tsx @@ -0,0 +1,20 @@ +import { ReactNode } from 'react'; + +type Props = { + children: ReactNode; + type: 'checkbox' | 'radio' | 'switch'; + label?: string; + className?: string; +}; + +const FormCheckRadio = (props: Props) => { + return ( + + ); +}; + +export default FormCheckRadio; diff --git a/frontend/src/components/FormCheckRadioGroup.tsx b/frontend/src/components/FormCheckRadioGroup.tsx new file mode 100644 index 0000000..04fa072 --- /dev/null +++ b/frontend/src/components/FormCheckRadioGroup.tsx @@ -0,0 +1,24 @@ +import { Children, cloneElement, ReactElement, ReactNode } from 'react'; + +type Props = { + isColumn?: boolean; + children: ReactNode; +}; + +const FormCheckRadioGroup = (props: Props) => { + return ( +
    + {Children.map(props.children, (child: ReactElement) => + cloneElement(child, { + className: `mr-6 mb-3 last:mr-0 ${child.props.className}`, + }), + )} +
    + ); +}; + +export default FormCheckRadioGroup; diff --git a/frontend/src/components/FormField.tsx b/frontend/src/components/FormField.tsx new file mode 100644 index 0000000..405908f --- /dev/null +++ b/frontend/src/components/FormField.tsx @@ -0,0 +1,92 @@ +import { Children, cloneElement, ReactElement, ReactNode } from 'react'; +import BaseIcon from './BaseIcon'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + label?: string; + labelFor?: string; + help?: string; + icons?: string[] | null[]; + isBorderless?: boolean; + isTransparent?: boolean; + hasTextareaHeight?: boolean; + children: ReactNode; + disabled?: boolean; + borderButtom?: boolean; + diversity?: boolean; + websiteBg?: boolean; +}; + +const FormField = ({ icons = [], ...props }: Props) => { + const childrenCount = Children.count(props.children); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const corners = useAppSelector((state) => state.style.corners); + const bgWebsiteColor = useAppSelector((state) => state.style.bgLayoutColor); + let elementWrapperClass = ''; + + switch (childrenCount) { + case 2: + elementWrapperClass = 'grid grid-cols-1 gap-3 md:grid-cols-2'; + break; + case 3: + elementWrapperClass = 'grid grid-cols-1 gap-3 md:grid-cols-3'; + } + + const controlClassName = [ + `px-3 py-2 max-w-full border-gray-300 dark:border-dark-700 ${corners} w-full dark:placeholder-gray-400`, + `${focusRing}`, + props.hasTextareaHeight ? 'h-24' : 'h-12', + props.isBorderless ? 'border-0' : 'border', + props.isTransparent + ? 'bg-transparent' + : `${props.websiteBg ? ` bg-white` : bgColor} dark:bg-dark-800`, + props.disabled ? 'bg-gray-200 text-gray-100 dark:bg-dark-900 disabled' : '', + props.borderButtom + ? `border-0 border-b ${ + props.diversity + ? 'border-gray-400' + : ' placeholder-white border-gray-300/10 border-white ' + } rounded-none focus:ring-0` + : '', + ].join(' '); + + return ( +
    + {props.label && ( + + )} +
    + {Children.map(props.children, (child: ReactElement, index) => ( +
    + {cloneElement(child, { + className: `${controlClassName} ${icons[index] ? 'pl-10' : ''}`, + })} + {icons[index] && ( + + )} +
    + ))} +
    + {props.help && ( +
    + {props.help} +
    + )} +
    + ); +}; + +export default FormField; diff --git a/frontend/src/components/FormFilePicker.tsx b/frontend/src/components/FormFilePicker.tsx new file mode 100644 index 0000000..7be6c35 --- /dev/null +++ b/frontend/src/components/FormFilePicker.tsx @@ -0,0 +1,101 @@ +import { useEffect, useState } from 'react'; +import { ColorButtonKey } from '../interfaces'; +import BaseButton from './BaseButton'; +import FileUploader from './Uploaders/UploadService'; +import { mdiReload } from '@mdi/js'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + label?: string; + icon?: string; + accept?: string; + color: ColorButtonKey; + isRoundIcon?: boolean; + path: string; + schema: object; + field: any; + form: any; +}; + +const FormFilePicker = ({ + label, + icon, + accept, + color, + isRoundIcon, + path, + schema, + form, + field, +}: Props) => { + const [file, setFile] = useState(null); + const [loading, setLoading] = useState(false); + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + let cornersRight; + if (corners === 'rounded') { + cornersRight = 'rounded-r'; + } else if (corners === 'rounded-lg') { + cornersRight = 'rounded-r-lg'; + } else if (corners === 'rounded-full') { + cornersRight = 'rounded-r-3xl'; + } else { + cornersRight = ''; + } + + useEffect(() => { + if (field.value) { + setFile(field.value[0]); + } + }, [field.value]); + const handleFileChange = async (event) => { + const file = event.currentTarget.files[0]; + setFile(file); + + FileUploader.validate(file, schema); + setLoading(true); + const remoteFile = await FileUploader.upload(path, file, schema); + + form.setFieldValue(field.name, [{ ...remoteFile }]); + setLoading(false); + }; + + const showFilename = !isRoundIcon && file; + + return ( +
    + + {showFilename && !loading && ( +
    + + {file.name} + +
    + )} +
    + ); +}; + +export default FormFilePicker; diff --git a/frontend/src/components/FormImagePicker.tsx b/frontend/src/components/FormImagePicker.tsx new file mode 100644 index 0000000..e8a8dac --- /dev/null +++ b/frontend/src/components/FormImagePicker.tsx @@ -0,0 +1,102 @@ +import { useState, useEffect } from 'react'; +import { ColorButtonKey } from '../interfaces'; +import BaseButton from './BaseButton'; +import ImagesUploader from './Uploaders/ImagesUploader'; +import FileUploader from './Uploaders/UploadService'; +import { mdiReload } from '@mdi/js'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + label?: string; + icon?: string; + accept?: string; + color: ColorButtonKey; + isRoundIcon?: boolean; + path: string; + schema: object; + field: any; + form: any; +}; + +const FormImagePicker = ({ + label, + icon, + accept, + color, + isRoundIcon, + path, + schema, + form, + field, +}: Props) => { + const [file, setFile] = useState(null); + const [loading, setLoading] = useState(false); + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + let cornersRight; + if (corners === 'rounded') { + cornersRight = 'rounded-r'; + } else if (corners === 'rounded-lg') { + cornersRight = 'rounded-r-lg'; + } else if (corners === 'rounded-full') { + cornersRight = 'rounded-r-3xl'; + } else { + cornersRight = ''; + } + + useEffect(() => { + if (field.value) { + setFile(field.value[0]); + } + }, [field.value]); + const handleFileChange = async (event) => { + const file = event.currentTarget.files[0]; + setFile(file); + + FileUploader.validate(file, schema); + setLoading(true); + const remoteFile = await FileUploader.upload(path, file, schema); + + form.setFieldValue(field.name, [{ ...remoteFile }]); + setLoading(false); + }; + + const showFilename = !isRoundIcon && file; + + return ( +
    + + {showFilename && !loading && ( +
    + + {file.name} + +
    + )} +
    + ); +}; + +export default FormImagePicker; diff --git a/frontend/src/components/IconRounded.tsx b/frontend/src/components/IconRounded.tsx new file mode 100644 index 0000000..7ec5864 --- /dev/null +++ b/frontend/src/components/IconRounded.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { ColorKey } from '../interfaces'; +import { colorsBgLight, colorsText } from '../colors'; +import BaseIcon from './BaseIcon'; + +type Props = { + icon: string; + color: ColorKey; + w?: string; + h?: string; + bg?: boolean; + className?: string; +}; + +export default function IconRounded({ + icon, + color, + w = 'w-12', + h = 'h-12', + bg = false, + className = '', +}: Props) { + const classAddon = bg + ? colorsBgLight[color] + : `${colorsText[color]} bg-gray-50 dark:bg-slate-800`; + + return ( + + ); +} diff --git a/frontend/src/components/ImageField.tsx b/frontend/src/components/ImageField.tsx new file mode 100644 index 0000000..2bf42ae --- /dev/null +++ b/frontend/src/components/ImageField.tsx @@ -0,0 +1,51 @@ +/* eslint-disable @next/next/no-img-element */ +// Why disabled: +// avatars.dicebear.com provides svg avatars +// next/image needs dangerouslyAllowSVG option for that + +import React, { ReactNode } from 'react'; +import { mdiImageOutline } from '@mdi/js'; +import BaseIcon from './BaseIcon'; + +type Props = { + name: string; + image?: object | null; + api?: string; + className?: string; + imageClassName?: string; + children?: ReactNode; +}; + +export default function ImageField({ + name, + image, + className = '', + imageClassName = '', + children, +}: Props) { + const imageSrc = image && image[0] ? `${image[0].publicUrl}` : ''; + + return ( +
    + {imageSrc ? ( + {name} + ) : ( +
    + +
    + )} + + {children} +
    + ); +} diff --git a/frontend/src/components/IntroGuide.tsx b/frontend/src/components/IntroGuide.tsx new file mode 100644 index 0000000..7271c83 --- /dev/null +++ b/frontend/src/components/IntroGuide.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Steps, Hints } from 'intro.js-react'; +import { useRouter } from 'next/router'; +interface IntroGuideProps { + steps: Array<{ + element: string; + intro: string; + position?: string; + }>; + disableInteraction?: boolean; + stepsEnabled: boolean; + stepsName: string; + onExit: () => void; +} + +const IntroGuide: React.FC = ({ + steps, + stepsEnabled, + onExit, + stepsName, +}) => { + const router = useRouter(); + const handleStepChange = (stepIndex: number) => { + if (stepIndex === 7 && stepsName === 'appSteps') { + onExit(); + router.push('/users/users-list/'); + } else if (stepIndex === 2 && stepsName === 'usersSteps') { + onExit(); + router.push('/roles/roles-list/'); + } + }; + + const handleExit = () => { + localStorage.setItem(`completed_${stepsName}`, 'true'); + onExit(); + }; + return ( + <> + + + ); +}; + +export default IntroGuide; diff --git a/frontend/src/components/KanbanBoard/KanbanBoard.tsx b/frontend/src/components/KanbanBoard/KanbanBoard.tsx new file mode 100644 index 0000000..76d6849 --- /dev/null +++ b/frontend/src/components/KanbanBoard/KanbanBoard.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import KanbanColumn from './KanbanColumn'; +import { AsyncThunk } from '@reduxjs/toolkit'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; + +type Props = { + columns: Array<{ id: string; label: string }>; + filtersQuery: string; + entityName: string; + columnFieldName: string; + showFieldName: string; + deleteThunk: AsyncThunk; + updateThunk: AsyncThunk; +}; + +const KanbanBoard = ({ + columns, + entityName, + columnFieldName, + filtersQuery, + showFieldName, + deleteThunk, + updateThunk, +}: Props) => { + return ( +
    + + {columns.map((column) => ( + + ))} + +
    + ); +}; + +export default KanbanBoard; diff --git a/frontend/src/components/KanbanBoard/KanbanCard.tsx b/frontend/src/components/KanbanBoard/KanbanCard.tsx new file mode 100644 index 0000000..5441aea --- /dev/null +++ b/frontend/src/components/KanbanBoard/KanbanCard.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import Link from 'next/link'; +import moment from 'moment'; +import ListActionsPopover from '../ListActionsPopover'; +import { DragSourceMonitor, useDrag } from 'react-dnd'; + +type Props = { + item: any; + column: { id: string; label: string }; + entityName: string; + showFieldName: string; + setItemIdToDelete: (id: string) => void; +}; + +const KanbanCard = ({ + item, + entityName, + showFieldName, + setItemIdToDelete, + column, +}: Props) => { + const [{ isDragging }, drag] = useDrag( + () => ({ + type: 'box', + item: { item, column }, + collect: (monitor: DragSourceMonitor) => ({ + isDragging: monitor.isDragging(), + }), + }), + [item], + ); + + return ( +
    +
    + + {item[showFieldName] ?? 'No data'} + +
    +
    +

    {moment(item.createdAt).format('MMM DD hh:mm a')}

    + setItemIdToDelete(id)} + hasUpdatePermission={true} + className={'w-2 h-2 text-white'} + iconClassName={'w-5'} + /> +
    +
    + ); +}; + +export default KanbanCard; diff --git a/frontend/src/components/KanbanBoard/KanbanColumn.tsx b/frontend/src/components/KanbanBoard/KanbanColumn.tsx new file mode 100644 index 0000000..614957e --- /dev/null +++ b/frontend/src/components/KanbanBoard/KanbanColumn.tsx @@ -0,0 +1,211 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import Axios from 'axios'; +import CardBox from '../CardBox'; +import CardBoxModal from '../CardBoxModal'; +import { AsyncThunk } from '@reduxjs/toolkit'; +import { useDrop } from 'react-dnd'; +import KanbanCard from './KanbanCard'; + +type Props = { + column: { id: string; label: string }; + entityName: string; + columnFieldName: string; + showFieldName: string; + filtersQuery: any; + deleteThunk: AsyncThunk; + updateThunk: AsyncThunk; +}; + +type DropResult = { + sourceColumn: { id: string; label: string }; + item: any; +}; + +const perPage = 10; + +const KanbanColumn = ({ + column, + entityName, + columnFieldName, + showFieldName, + filtersQuery, + deleteThunk, + updateThunk, +}: Props) => { + const [currentPage, setCurrentPage] = useState(0); + const [count, setCount] = useState(0); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [itemIdToDelete, setItemIdToDelete] = useState(''); + const currentUser = useAppSelector((state) => state.auth.currentUser); + const listInnerRef = useRef(); + const dispatch = useAppDispatch(); + + const [{ dropResult }, drop] = useDrop< + { + item: any; + column: { + id: string; + label: string; + }; + }, + unknown, + { + dropResult: DropResult; + } + >( + () => ({ + accept: 'box', + drop: ({ + item, + column: sourceColumn, + }: { + item: any; + column: { id: string; label: string }; + }) => { + if (sourceColumn.id === column.id) return; + + dispatch( + updateThunk({ + id: item.id, + data: { + [columnFieldName]: column.id, + }, + }), + ).then((res) => { + console.log('res', res); + setData((prevState) => (prevState ? [...prevState, item] : [item])); + setCount((prevState) => prevState + 1); + }); + + return { sourceColumn, item }; + }, + collect: (monitor) => ({ + dropResult: monitor.getDropResult(), + }), + }), + [], + ); + + const loadData = useCallback( + (page: number, filters = '') => { + const query = `?page=${page}&limit=${perPage}&field=createdAt&sort=desc&${columnFieldName}=${column.id}&${filters}`; + setLoading(true); + Axios.get(`${entityName}${query}`) + .then((res) => { + setData((prevState) => + page === 0 ? res.data.rows : [...prevState, ...res.data.rows], + ); + setCount(res.data.count); + setCurrentPage(page); + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + setLoading(false); + }); + }, + [currentUser, column], + ); + + useEffect(() => { + if (!currentUser) return; + loadData(0, filtersQuery); + }, [currentUser, loadData, filtersQuery]); + + useEffect(() => { + loadData(0, filtersQuery); + }, [loadData, filtersQuery]); + + useEffect(() => { + if (dropResult?.sourceColumn && dropResult.sourceColumn.id === column.id) { + setData((prevState) => + prevState.filter((item) => item.id !== dropResult.item.id), + ); + setCount((prevState) => prevState - 1); + } + }, [dropResult]); + + const onScroll = () => { + if (listInnerRef.current) { + const { scrollTop, scrollHeight, clientHeight } = listInnerRef.current; + if (Math.floor(scrollTop + clientHeight) === scrollHeight) { + if (data.length < count && !loading) { + loadData(currentPage + 1, filtersQuery); + } + } + } + }; + + const onDeleteConfirm = () => { + if (!itemIdToDelete) return; + + dispatch(deleteThunk(itemIdToDelete)) + .then((res) => { + if (res.meta.requestStatus === 'fulfilled') { + setItemIdToDelete(''); + loadData(0, filtersQuery); + } + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + setItemIdToDelete(''); + }); + }; + + return ( + <> + +
    +

    {column.label}

    +

    {count}

    +
    +
    { + drop(node); + listInnerRef.current = node; + }} + className={'p-3 space-y-3 flex-1 overflow-y-auto max-h-[400px]'} + onScroll={onScroll} + > + {data?.map((item) => ( + + ))} + {!data?.length && ( +

    + No data +

    + )} +
    +
    + setItemIdToDelete('')} + > +

    Are you sure you want to delete this item?

    +
    + + ); +}; + +export default KanbanColumn; diff --git a/frontend/src/components/ListActionsPopover.tsx b/frontend/src/components/ListActionsPopover.tsx new file mode 100644 index 0000000..7465d91 --- /dev/null +++ b/frontend/src/components/ListActionsPopover.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import Link from 'next/link'; +import Button from '@mui/material/Button'; +import BaseIcon from './BaseIcon'; +import { + mdiDotsVertical, + mdiEye, + mdiPencilOutline, + mdiTrashCan, +} from '@mdi/js'; +import Popover from '@mui/material/Popover'; +import { IconButton } from '@mui/material'; + +type Props = { + itemId: string; + onDelete: (id: string) => void; + hasUpdatePermission: boolean; + className?: string; + iconClassName?: string; + pathEdit: string; + pathView: string; +}; + +const ListActionsPopover = ({ + itemId, + onDelete, + hasUpdatePermission, + className, + iconClassName, + pathEdit, + pathView, +}: Props) => { + const [anchorEl, setAnchorEl] = React.useState(null); + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const linkView = pathView; + const linkEdit = pathEdit; + const handleClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + const id = open ? 'simple-popover' : undefined; + + return ( + <> + + + + +
    + + {hasUpdatePermission && ( + + )} + {hasUpdatePermission && ( + + )} +
    +
    + + ); +}; + +export default ListActionsPopover; diff --git a/frontend/src/components/LoadingSpinner.tsx b/frontend/src/components/LoadingSpinner.tsx new file mode 100644 index 0000000..2a56d8d --- /dev/null +++ b/frontend/src/components/LoadingSpinner.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +const LoadingSpinner = () => { + return ( +
    +
    +
    +
    +
    +
    + ); +}; + +export default LoadingSpinner; diff --git a/frontend/src/components/Logo/index.tsx b/frontend/src/components/Logo/index.tsx new file mode 100644 index 0000000..7d9123c --- /dev/null +++ b/frontend/src/components/Logo/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +type Props = { + className?: string; +}; + +export default function Logo({ className = '' }: Props) { + return ( + {'Flatlogic + ); +} diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx new file mode 100644 index 0000000..3490690 --- /dev/null +++ b/frontend/src/components/NavBar.tsx @@ -0,0 +1,64 @@ +import React, { ReactNode, useState, useEffect } from 'react'; +import { mdiClose, mdiDotsVertical } from '@mdi/js'; +import { containerMaxW } from '../config'; +import BaseIcon from './BaseIcon'; +import NavBarItemPlain from './NavBarItemPlain'; +import NavBarMenuList from './NavBarMenuList'; +import { MenuNavBarItem } from '../interfaces'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + menu: MenuNavBarItem[]; + className: string; + children: ReactNode; +}; + +export default function NavBar({ menu, className = '', children }: Props) { + const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false); + const [isScrolled, setIsScrolled] = useState(false); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + + useEffect(() => { + const handleScroll = () => { + const scrolled = window.scrollY > 0; + setIsScrolled(scrolled); + }; + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + + const handleMenuNavBarToggleClick = () => { + setIsMenuNavBarActive(!isMenuNavBarActive); + }; + + return ( + + ); +} diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx new file mode 100644 index 0000000..aeeaaa8 --- /dev/null +++ b/frontend/src/components/NavBarItem.tsx @@ -0,0 +1,149 @@ +import React, { useEffect, useRef } from 'react'; +import Link from 'next/link'; +import { useState } from 'react'; +import { mdiChevronUp, mdiChevronDown } from '@mdi/js'; +import BaseDivider from './BaseDivider'; +import BaseIcon from './BaseIcon'; +import UserAvatarCurrentUser from './UserAvatarCurrentUser'; +import NavBarMenuList from './NavBarMenuList'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import { MenuNavBarItem } from '../interfaces'; +import { setDarkMode } from '../stores/styleSlice'; +import { logoutUser } from '../stores/authSlice'; +import { useRouter } from 'next/router'; +import ClickOutside from './ClickOutside'; + +type Props = { + item: MenuNavBarItem; +}; + +export default function NavBarItem({ item }: Props) { + const router = useRouter(); + const dispatch = useAppDispatch(); + const excludedRef = useRef(null); + + const navBarItemLabelActiveColorStyle = useAppSelector( + (state) => state.style.navBarItemLabelActiveColorStyle, + ); + const navBarItemLabelStyle = useAppSelector( + (state) => state.style.navBarItemLabelStyle, + ); + const navBarItemLabelHoverStyle = useAppSelector( + (state) => state.style.navBarItemLabelHoverStyle, + ); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + + const userName = `${currentUser?.firstName ? currentUser?.firstName : ''} ${ + currentUser?.lastName ? currentUser?.lastName : '' + }`; + + const [isDropdownActive, setIsDropdownActive] = useState(false); + + useEffect(() => { + return () => setIsDropdownActive(false); + }, [router.pathname]); + + const componentClass = [ + 'block lg:flex items-center relative cursor-pointer', + isDropdownActive + ? `${navBarItemLabelActiveColorStyle} dark:text-slate-400` + : `${navBarItemLabelStyle} dark:text-white dark:hover:text-slate-400 ${navBarItemLabelHoverStyle}`, + item.menu ? 'lg:py-2 lg:px-3' : 'py-2 px-3', + item.isDesktopNoLabel ? 'lg:w-16 lg:justify-center' : '', + ].join(' '); + + const itemLabel = item.isCurrentUser ? userName : item.label; + + const handleMenuClick = () => { + if (item.menu) { + setIsDropdownActive(!isDropdownActive); + } + + if (item.isToggleLightDark) { + dispatch(setDarkMode(null)); + } + + if (item.isLogout) { + dispatch(logoutUser()); + router.push('/login'); + } + }; + + const getItemId = (label) => { + switch (label) { + case 'Light/Dark': + return 'themeToggle'; + case 'Log out': + return 'logout'; + default: + return undefined; + } + }; + + const NavBarItemComponentContents = ( + <> +
    + {item.icon && ( + + )} + + {itemLabel} + + {item.isCurrentUser && ( + + )} + {item.menu && ( + + )} +
    + {item.menu && ( +
    + setIsDropdownActive(false)} + excludedElements={[excludedRef]} + > + + +
    + )} + + ); + + if (item.isDivider) { + return ; + } + + if (item.href) { + return ( + + {NavBarItemComponentContents} + + ); + } + + return ( +
    + {NavBarItemComponentContents} +
    + ); +} diff --git a/frontend/src/components/NavBarItemPlain.tsx b/frontend/src/components/NavBarItemPlain.tsx new file mode 100644 index 0000000..e9b7748 --- /dev/null +++ b/frontend/src/components/NavBarItemPlain.tsx @@ -0,0 +1,35 @@ +import React, { ReactNode } from 'react'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + display?: string; + useMargin?: boolean; + children: ReactNode; + onClick?: (e: React.MouseEvent) => void; +}; + +export default function NavBarItemPlain({ + display = 'flex', + useMargin = false, + onClick, + children, +}: Props) { + const navBarItemLabelStyle = useAppSelector( + (state) => state.style.navBarItemLabelStyle, + ); + const navBarItemLabelHoverStyle = useAppSelector( + (state) => state.style.navBarItemLabelHoverStyle, + ); + + const classBase = + 'items-center cursor-pointer dark:text-white dark:hover:text-slate-400'; + const classAddon = `${display} ${navBarItemLabelStyle} ${navBarItemLabelHoverStyle} ${ + useMargin ? 'my-2 mx-3' : 'py-2 px-3' + }`; + + return ( +
    + {children} +
    + ); +} diff --git a/frontend/src/components/NavBarMenuList.tsx b/frontend/src/components/NavBarMenuList.tsx new file mode 100644 index 0000000..d85a7b9 --- /dev/null +++ b/frontend/src/components/NavBarMenuList.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { MenuNavBarItem } from '../interfaces'; +import NavBarItem from './NavBarItem'; + +type Props = { + menu: MenuNavBarItem[]; +}; + +export default function NavBarMenuList({ menu }: Props) { + return ( + <> + {menu.map((item, index) => ( + + ))} + + ); +} diff --git a/frontend/src/components/NotificationBar.tsx b/frontend/src/components/NotificationBar.tsx new file mode 100644 index 0000000..e91f880 --- /dev/null +++ b/frontend/src/components/NotificationBar.tsx @@ -0,0 +1,65 @@ +import { mdiClose } from '@mdi/js'; +import React, { ReactNode, useState } from 'react'; +import { ColorKey } from '../interfaces'; +import { colorsBgLight, colorsOutline } from '../colors'; +import BaseButton from './BaseButton'; +import BaseIcon from './BaseIcon'; + +type Props = { + color: ColorKey; + icon?: string; + outline?: boolean; + children: ReactNode; + button?: ReactNode; +}; + +const NotificationBar = ({ outline = false, children, ...props }: Props) => { + const componentColorClass = outline + ? colorsOutline[props.color] + : colorsBgLight[props.color]; + + const [isDismissed, setIsDismissed] = useState(false); + + const dismiss = (e: React.MouseEvent) => { + e.preventDefault(); + + setIsDismissed(true); + }; + + if (isDismissed) { + return null; + } + + return ( +
    +
    +
    + {props.icon && ( + + )} + {children} +
    + {props.button} + {!props.button && ( + + )} +
    +
    + ); +}; + +export default NotificationBar; diff --git a/frontend/src/components/OverlayLayer.tsx b/frontend/src/components/OverlayLayer.tsx new file mode 100644 index 0000000..53c681d --- /dev/null +++ b/frontend/src/components/OverlayLayer.tsx @@ -0,0 +1,41 @@ +import React, { ReactNode } from 'react'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + zIndex?: string; + type?: string; + children?: ReactNode; + className?: string; + onClick: (e: React.MouseEvent) => void; +}; + +export default function OverlayLayer({ + zIndex = 'z-50', + type = 'flex', + children, + className, + ...props +}: Props) { + const overlayStyle = useAppSelector((state) => state.style.overlayStyle); + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + + if (props.onClick) { + props.onClick(e); + } + }; + + return ( +
    +
    + + {children} +
    + ); +} diff --git a/frontend/src/components/Pagination.tsx b/frontend/src/components/Pagination.tsx new file mode 100644 index 0000000..8203969 --- /dev/null +++ b/frontend/src/components/Pagination.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { + mdiChevronDoubleLeft, + mdiChevronDoubleRight, + mdiChevronLeft, + mdiChevronRight, +} from '@mdi/js'; +import BaseIcon from './BaseIcon'; + +type Props = { + currentPage: number; + numPages: number; + setCurrentPage: any; +}; + +export const Pagination = ({ + currentPage, + numPages, + setCurrentPage, +}: Props) => { + return ( +
    + {currentPage === 0 && ( +
    + + +
    + )} + {currentPage !== 0 && ( +
    +
    setCurrentPage(0)}> + +
    +
    setCurrentPage(currentPage - 1)}> + +
    +
    + )} +

    + Page {currentPage + 1} of {numPages} +

    + {currentPage !== numPages - 1 && ( +
    +
    setCurrentPage(currentPage + 1)}> + +
    + +
    setCurrentPage(numPages - 1)}> + +
    +
    + )} + {currentPage === numPages - 1 && ( +
    + + +
    + )} +
    + ); +}; diff --git a/frontend/src/components/PasswordSetOrReset.tsx b/frontend/src/components/PasswordSetOrReset.tsx new file mode 100644 index 0000000..519c894 --- /dev/null +++ b/frontend/src/components/PasswordSetOrReset.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { toast, ToastContainer } from 'react-toastify'; + +import Head from 'next/head'; +import CardBox from '../components/CardBox'; +import SectionFullScreen from '../components/SectionFullScreen'; +import { useRouter } from 'next/router'; +import { getPageTitle } from '../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../components/FormField'; +import BaseButtons from '../components/BaseButtons'; +import BaseButton from '../components/BaseButton'; +import { passwordReset } from '../stores/authSlice'; +import { useAppDispatch } from '../stores/hooks'; + +export default function PasswordSetOrReset() { + const [loading, setLoading] = React.useState(false); + const [isInvitation, setIsInvitation] = React.useState(false); + const router = useRouter(); + const { token, invitation } = router.query; + + const notify = (type, msg) => toast(msg, { type }); + + const dispatch = useAppDispatch(); + + React.useEffect(() => { + if (invitation) { + setIsInvitation(true); + } + }, [invitation]); + + const handleSubmit = async (value) => { + setLoading(true); + if (typeof token === 'string') { + await dispatch( + passwordReset({ + token, + password: value.password, + type: isInvitation && 'invitation', + }), + ); + await router.push('/login'); + } + + setLoading(false); + }; + + return ( + <> + + {isInvitation && {getPageTitle('Set Password')}} + {!isInvitation && {getPageTitle('Reset Password')}} + + + +
    + + {isInvitation &&

    Set Password

    } + {!isInvitation &&

    Reset Password

    } +

    Enter your new password

    + + handleSubmit(values)} + > + {({ errors, touched }) => ( +
    + + + + + + + + + + +
    + )} +
    +
    +
    +
    + + + ); +} diff --git a/frontend/src/components/Permissions/CardPermissions.tsx b/frontend/src/components/Permissions/CardPermissions.tsx new file mode 100644 index 0000000..1948252 --- /dev/null +++ b/frontend/src/components/Permissions/CardPermissions.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + permissions: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardPermissions = ({ + permissions, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_PERMISSIONS'); + + return ( +
    + {loading && } +
      + {!loading && + permissions.map((item, index) => ( +
    • +
      + + {item.name} + + +
      + +
      +
      +
      +
      +
      Name
      +
      +
      {item.name}
      +
      +
      +
      +
    • + ))} + {!loading && permissions.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardPermissions; diff --git a/frontend/src/components/Permissions/ListPermissions.tsx b/frontend/src/components/Permissions/ListPermissions.tsx new file mode 100644 index 0000000..08f24b9 --- /dev/null +++ b/frontend/src/components/Permissions/ListPermissions.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + permissions: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListPermissions = ({ + permissions, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_PERMISSIONS'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + permissions.map((item) => ( + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Name

    +

    {item.name}

    +
    + + +
    +
    + ))} + {!loading && permissions.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListPermissions; diff --git a/frontend/src/components/Permissions/TablePermissions.tsx b/frontend/src/components/Permissions/TablePermissions.tsx new file mode 100644 index 0000000..21ac91c --- /dev/null +++ b/frontend/src/components/Permissions/TablePermissions.tsx @@ -0,0 +1,484 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/permissions/permissionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configurePermissionsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSamplePermissions = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + permissions, + loading, + count, + notify: permissionsNotify, + refetch, + } = useAppSelector((state) => state.permissions); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (permissionsNotify.showNotification) { + notify( + permissionsNotify.typeNotification, + permissionsNotify.textNotification, + ); + } + }, [permissionsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `permissions`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={permissions ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSamplePermissions; diff --git a/frontend/src/components/Permissions/configurePermissionsCols.tsx b/frontend/src/components/Permissions/configurePermissionsCols.tsx new file mode 100644 index 0000000..f4af556 --- /dev/null +++ b/frontend/src/components/Permissions/configurePermissionsCols.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_PERMISSIONS'); + + return [ + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ + , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Qr_codes/CardQr_codes.tsx b/frontend/src/components/Qr_codes/CardQr_codes.tsx new file mode 100644 index 0000000..9de9db1 --- /dev/null +++ b/frontend/src/components/Qr_codes/CardQr_codes.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + qr_codes: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardQr_codes = ({ + qr_codes, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_QR_CODES'); + + return ( +
    + {loading && } +
      + {!loading && + qr_codes.map((item, index) => ( +
    • +
      + + {item.code} + + +
      + +
      +
      +
      +
      +
      + QRCode +
      +
      +
      {item.code}
      +
      +
      + +
      +
      Video
      +
      +
      + {dataFormatter.videosOneListFormatter(item.video)} +
      +
      +
      + +
      +
      + ValidFrom +
      +
      +
      + {dataFormatter.dateTimeFormatter(item.valid_from)} +
      +
      +
      + +
      +
      + ValidUntil +
      +
      +
      + {dataFormatter.dateTimeFormatter(item.valid_until)} +
      +
      +
      +
      +
    • + ))} + {!loading && qr_codes.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardQr_codes; diff --git a/frontend/src/components/Qr_codes/ListQr_codes.tsx b/frontend/src/components/Qr_codes/ListQr_codes.tsx new file mode 100644 index 0000000..5b13d16 --- /dev/null +++ b/frontend/src/components/Qr_codes/ListQr_codes.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + qr_codes: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListQr_codes = ({ + qr_codes, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_QR_CODES'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + qr_codes.map((item) => ( + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    QRCode

    +

    {item.code}

    +
    + +
    +

    Video

    +

    + {dataFormatter.videosOneListFormatter(item.video)} +

    +
    + +
    +

    ValidFrom

    +

    + {dataFormatter.dateTimeFormatter(item.valid_from)} +

    +
    + +
    +

    ValidUntil

    +

    + {dataFormatter.dateTimeFormatter(item.valid_until)} +

    +
    + + +
    +
    + ))} + {!loading && qr_codes.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListQr_codes; diff --git a/frontend/src/components/Qr_codes/TableQr_codes.tsx b/frontend/src/components/Qr_codes/TableQr_codes.tsx new file mode 100644 index 0000000..60d0c9c --- /dev/null +++ b/frontend/src/components/Qr_codes/TableQr_codes.tsx @@ -0,0 +1,510 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/qr_codes/qr_codesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureQr_codesCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +import BigCalendar from '../BigCalendar'; +import { SlotInfo } from 'react-big-calendar'; + +const perPage = 100; + +const TableSampleQr_codes = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + qr_codes, + loading, + count, + notify: qr_codesNotify, + refetch, + } = useAppSelector((state) => state.qr_codes); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (qr_codesNotify.showNotification) { + notify(qr_codesNotify.typeNotification, qr_codesNotify.textNotification); + } + }, [qr_codesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleCreateEventAction = ({ start, end }: SlotInfo) => { + router.push( + `/qr_codes/qr_codes-new?dateRangeStart=${start.toISOString()}&dateRangeEnd=${end.toISOString()}`, + ); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `qr_codes`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={qr_codes ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {!showGrid && ( + { + loadData( + 0, + `&calendarStart=${range.start}&calendarEnd=${range.end}`, + ); + }} + entityName={'qr_codes'} + /> + )} + + {showGrid && dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleQr_codes; diff --git a/frontend/src/components/Qr_codes/configureQr_codesCols.tsx b/frontend/src/components/Qr_codes/configureQr_codesCols.tsx new file mode 100644 index 0000000..a472ccb --- /dev/null +++ b/frontend/src/components/Qr_codes/configureQr_codesCols.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_QR_CODES'); + + return [ + { + field: 'code', + headerName: 'QRCode', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'video', + headerName: 'Video', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('videos'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'valid_from', + headerName: 'ValidFrom', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.valid_from), + }, + + { + field: 'valid_until', + headerName: 'ValidUntil', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.valid_until), + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ + , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/RichTextField.tsx b/frontend/src/components/RichTextField.tsx new file mode 100644 index 0000000..29b43a1 --- /dev/null +++ b/frontend/src/components/RichTextField.tsx @@ -0,0 +1,41 @@ +import React, { useEffect, useId, useState } from 'react'; +import { Editor } from '@tinymce/tinymce-react'; +import { tinyKey } from '../config'; +import { useAppSelector } from '../stores/hooks'; + +export const RichTextField = ({ options, field, form, itemRef, showField }) => { + const [value, setValue] = useState(null); + const darkMode = useAppSelector((state) => state.style.darkMode); + + useEffect(() => { + if (field.value) { + setValue(field.value); + } + }, [field.value]); + + const handleChange = (value) => { + form.setFieldValue(field.name, value); + setValue(value); + }; + + return ( + + ); +}; diff --git a/frontend/src/components/Roles/CardRoles.tsx b/frontend/src/components/Roles/CardRoles.tsx new file mode 100644 index 0000000..4daa835 --- /dev/null +++ b/frontend/src/components/Roles/CardRoles.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + roles: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardRoles = ({ + roles, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ROLES'); + + return ( +
    + {loading && } +
      + {!loading && + roles.map((item, index) => ( +
    • +
      + + {item.name} + + +
      + +
      +
      +
      +
      +
      Name
      +
      +
      {item.name}
      +
      +
      + +
      +
      + Permissions +
      +
      +
      + {dataFormatter + .permissionsManyListFormatter(item.permissions) + .join(', ')} +
      +
      +
      +
      +
    • + ))} + {!loading && roles.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardRoles; diff --git a/frontend/src/components/Roles/ListRoles.tsx b/frontend/src/components/Roles/ListRoles.tsx new file mode 100644 index 0000000..f1ff4dd --- /dev/null +++ b/frontend/src/components/Roles/ListRoles.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + roles: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListRoles = ({ + roles, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ROLES'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + roles.map((item) => ( + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Name

    +

    {item.name}

    +
    + +
    +

    Permissions

    +

    + {dataFormatter + .permissionsManyListFormatter(item.permissions) + .join(', ')} +

    +
    + + +
    +
    + ))} + {!loading && roles.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListRoles; diff --git a/frontend/src/components/Roles/TableRoles.tsx b/frontend/src/components/Roles/TableRoles.tsx new file mode 100644 index 0000000..701b6e3 --- /dev/null +++ b/frontend/src/components/Roles/TableRoles.tsx @@ -0,0 +1,481 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/roles/rolesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureRolesCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleRoles = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + roles, + loading, + count, + notify: rolesNotify, + refetch, + } = useAppSelector((state) => state.roles); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (rolesNotify.showNotification) { + notify(rolesNotify.typeNotification, rolesNotify.textNotification); + } + }, [rolesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `roles`, currentUser).then((newCols) => + setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={roles ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleRoles; diff --git a/frontend/src/components/Roles/configureRolesCols.tsx b/frontend/src/components/Roles/configureRolesCols.tsx new file mode 100644 index 0000000..8846f0c --- /dev/null +++ b/frontend/src/components/Roles/configureRolesCols.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_ROLES'); + + return [ + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'permissions', + headerName: 'Permissions', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: false, + sortable: false, + type: 'singleSelect', + valueFormatter: ({ value }) => + dataFormatter.permissionsManyListFormatter(value).join(', '), + renderEditCell: (params) => ( + + ), + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ + , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Search.tsx b/frontend/src/components/Search.tsx new file mode 100644 index 0000000..4f6de00 --- /dev/null +++ b/frontend/src/components/Search.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { Field, Form, Formik } from 'formik'; +import { useRouter } from 'next/router'; +import { useAppSelector } from '../stores/hooks'; + +const Search = () => { + const router = useRouter(); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const corners = useAppSelector((state) => state.style.corners); + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + const validateSearch = (value) => { + let error; + if (!value) { + error = 'Required'; + } else if (value.length < 2) { + error = 'Minimum length: 2 characters'; + } + return error; + }; + return ( + { + router.push(`/search?query=${values.search}`); + resetForm(); + setSubmitting(false); + }} + validateOnBlur={false} + validateOnChange={false} + > + {({ errors, touched, values }) => ( +
    + + {errors.search && touched.search && values.search.length < 2 ? ( +
    + {errors.search} +
    + ) : null} + + )} +
    + ); +}; +export default Search; diff --git a/frontend/src/components/SearchResults.tsx b/frontend/src/components/SearchResults.tsx new file mode 100644 index 0000000..4a417ca --- /dev/null +++ b/frontend/src/components/SearchResults.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import CardBox from './CardBox'; +import { useRouter } from 'next/router'; +import { humanize } from '../helpers/humanize'; + +const SearchResults = ({ searchResults, searchQuery }) => { + const router = useRouter(); + + return ( + <> +

    Matches with: {searchQuery}

    + {Object.keys(searchResults).map((tableName) => ( + <> +

    {humanize(tableName)}

    + +
    + + + + {searchResults[tableName].length > 0 && + Object.keys(searchResults[tableName][0]).map((key) => { + if ( + key !== 'tableName' && + key !== 'id' && + key !== 'matchAttribute' + ) { + return ( + + ); + } + return null; + })} + + + + {searchResults[tableName].map((item, index) => ( + + {Object.keys(item).map((key) => { + if ( + key !== 'tableName' && + key !== 'id' && + key !== 'matchAttribute' + ) { + return ( + + ); + } + return null; + })} + + ))} + +
    + {humanize(key)} +
    + router.push( + `/${tableName}/${tableName}-view/?id=${item['id']}`, + ) + } + > + {item[key]} +
    +
    + {!Object.keys(searchResults).length && ( +
    No data
    + )} +
    + + ))} + {!Object.keys(searchResults).length && ( +
    No matches
    + )} + + ); +}; + +export default SearchResults; diff --git a/frontend/src/components/SectionFullScreen.tsx b/frontend/src/components/SectionFullScreen.tsx new file mode 100644 index 0000000..96f8ef3 --- /dev/null +++ b/frontend/src/components/SectionFullScreen.tsx @@ -0,0 +1,32 @@ +import React, { ReactNode } from 'react'; +import { BgKey } from '../interfaces'; +import { + gradientBgPurplePink, + gradientBgDark, + gradientBgPinkRed, + gradientBgViolet, +} from '../colors'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + bg: BgKey; + children: ReactNode; +}; + +export default function SectionFullScreen({ bg, children }: Props) { + const darkMode = useAppSelector((state) => state.style.darkMode); + + let componentClass = 'flex min-h-screen items-center justify-center '; + + if (darkMode) { + componentClass += gradientBgDark; + } else if (bg === 'violet') { + componentClass += gradientBgViolet; + } else if (bg === 'purplePink') { + componentClass += gradientBgPurplePink; + } else if (bg === 'pinkRed') { + componentClass += gradientBgPinkRed; + } + + return
    {children}
    ; +} diff --git a/frontend/src/components/SectionMain.tsx b/frontend/src/components/SectionMain.tsx new file mode 100644 index 0000000..ba57321 --- /dev/null +++ b/frontend/src/components/SectionMain.tsx @@ -0,0 +1,10 @@ +import React, { ReactNode } from 'react'; +import { containerMaxW } from '../config'; + +type Props = { + children: ReactNode; +}; + +export default function SectionMain({ children }: Props) { + return
    {children}
    ; +} diff --git a/frontend/src/components/SectionTitle.tsx b/frontend/src/components/SectionTitle.tsx new file mode 100644 index 0000000..c07d85b --- /dev/null +++ b/frontend/src/components/SectionTitle.tsx @@ -0,0 +1,38 @@ +import React, { ReactNode } from 'react'; + +type Props = { + custom?: boolean; + first?: boolean; + last?: boolean; + children: ReactNode; +}; + +const SectionTitle = ({ + custom = false, + first = false, + last = false, + children, +}: Props) => { + let classAddon = '-my-6'; + + if (first) { + classAddon = '-mb-6'; + } else if (last) { + classAddon = '-mt-6'; + } + + return ( +
    + {custom && children} + {!custom && ( +

    + {children} +

    + )} +
    + ); +}; + +export default SectionTitle; diff --git a/frontend/src/components/SectionTitleLineWithButton.tsx b/frontend/src/components/SectionTitleLineWithButton.tsx new file mode 100644 index 0000000..8cfd157 --- /dev/null +++ b/frontend/src/components/SectionTitleLineWithButton.tsx @@ -0,0 +1,40 @@ +import { mdiCog } from '@mdi/js'; +import React, { Children, ReactNode } from 'react'; +import BaseButton from './BaseButton'; +import BaseIcon from './BaseIcon'; +import IconRounded from './IconRounded'; +import { humanize } from '../helpers/humanize'; + +type Props = { + icon: string; + title: string; + main?: boolean; + children?: ReactNode; +}; + +export default function SectionTitleLineWithButton({ + icon, + title, + main = false, + children, +}: Props) { + const hasChildren = !!Children.count(children); + + return ( +
    +
    + {icon && main && ( + + )} + {icon && !main && } +

    + {humanize(title)} +

    +
    + {children} + {!hasChildren && } +
    + ); +} diff --git a/frontend/src/components/SelectField.tsx b/frontend/src/components/SelectField.tsx new file mode 100644 index 0000000..56fb8fc --- /dev/null +++ b/frontend/src/components/SelectField.tsx @@ -0,0 +1,60 @@ +import React, { useEffect, useId, useState } from 'react'; +import { AsyncPaginate } from 'react-select-async-paginate'; +import axios from 'axios'; + +export const SelectField = ({ + options, + field, + form, + itemRef, + showField, + disabled, +}) => { + const [value, setValue] = useState(null); + const PAGE_SIZE = 100; + + useEffect(() => { + if (options?.id && field?.value?.id) { + setValue({ value: field.value?.id, label: field.value[showField] }); + form.setFieldValue(field.name, field.value?.id); + } else if (!field.value) { + setValue(null); + } + }, [options?.id, field?.value?.id, field?.value]); + + const mapResponseToValuesAndLabels = (data) => ({ + value: data.id, + label: data.label, + }); + const handleChange = (option) => { + form.setFieldValue(field.name, option?.value || null); + setValue(option); + }; + + async function callApi(inputValue: string, loadedOptions: any[]) { + const path = `/${itemRef}/autocomplete?limit=${PAGE_SIZE}&offset=${ + loadedOptions.length + }${inputValue ? `&query=${inputValue}` : ''}`; + const { data } = await axios(path); + return { + options: data.map(mapResponseToValuesAndLabels), + hasMore: data.length === PAGE_SIZE, + }; + } + return ( + 'px-1 py-2', + }} + classNamePrefix='react-select' + instanceId={useId()} + value={value} + debounceTimeout={1000} + loadOptions={callApi} + onChange={handleChange} + defaultOptions + isDisabled={disabled} + isClearable + /> + ); +}; diff --git a/frontend/src/components/SelectFieldMany.tsx b/frontend/src/components/SelectFieldMany.tsx new file mode 100644 index 0000000..c496ac6 --- /dev/null +++ b/frontend/src/components/SelectFieldMany.tsx @@ -0,0 +1,75 @@ +import React, { useEffect, useId, useState } from 'react'; +import { AsyncPaginate } from 'react-select-async-paginate'; +import axios from 'axios'; + +export const SelectFieldMany = ({ + options, + field, + form, + itemRef, + showField, +}) => { + const [value, setValue] = useState([]); + const PAGE_SIZE = 100; + + useEffect(() => { + if (field.value?.[0] && typeof field.value[0] !== 'string') { + form.setFieldValue( + field.name, + field.value.map((el) => el.id), + ); + } else if (!field.value || field.value.length === 0) { + setValue([]); + } + }, [field.name, field.value, form]); + + useEffect(() => { + if (options) { + setValue(options.map((el) => ({ value: el.id, label: el[showField] }))); + form.setFieldValue( + field.name, + options.map((el) => ({ value: el.id, label: el[showField] })), + ); + } + }, [options]); + + const mapResponseToValuesAndLabels = (data) => ({ + value: data.id, + label: data.label, + }); + + const handleChange = (data: any) => { + setValue(data); + form.setFieldValue( + field.name, + data.map((el) => el?.value || null), + ); + }; + + async function callApi(inputValue: string, loadedOptions: any[]) { + const path = `/${itemRef}/autocomplete?limit=${PAGE_SIZE}&offset=${ + loadedOptions.length + }${inputValue ? `&query=${inputValue}` : ''}`; + const { data } = await axios(path); + return { + options: data.map(mapResponseToValuesAndLabels), + hasMore: data.length === PAGE_SIZE, + }; + } + return ( + 'px-1 py-2', + }} + classNamePrefix='react-select' + instanceId={useId()} + value={value} + isMulti + debounceTimeout={1000} + loadOptions={callApi} + onChange={handleChange} + defaultOptions + isClearable + /> + ); +}; diff --git a/frontend/src/components/SmartWidget/SmartWidget.tsx b/frontend/src/components/SmartWidget/SmartWidget.tsx new file mode 100644 index 0000000..ef76c98 --- /dev/null +++ b/frontend/src/components/SmartWidget/SmartWidget.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import BaseButton from '../BaseButton'; +import BaseIcon from '../BaseIcon'; +import * as icons from '@mdi/js'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; + +import { fetchWidgets, removeWidget } from '../../stores/roles/rolesSlice'; +import { WidgetChartType, WidgetType } from './models/widget.model'; +import { BarChart } from './components/BarChart'; +import { PieChart } from './components/PieChart'; +import { AreaChart } from './components/AreaChart'; +import { LineChart } from './components/LineChart'; + +export const SmartWidget = ({ widget, userId, admin, roleId }) => { + const dispatch = useAppDispatch(); + const corners = useAppSelector((state) => state.style.corners); + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + + const deleteWidget = async () => { + await dispatch( + removeWidget({ id: userId, widgetId: widget.widget_id, roleId }), + ); + await dispatch(fetchWidgets(roleId)); + }; + + return ( +
    +
    +
    +
    + {widget.label} +
    + + {admin && ( + + )} +
    + +
    +
    + {widget.value ? ( + widget.widget_type === WidgetType.chart ? ( + widget.chart_type === WidgetChartType.bar ? ( + + ) : widget.chart_type === WidgetChartType.line ? ( + + ) : widget.chart_type === WidgetChartType.pie ? ( + + ) : widget.chart_type === WidgetChartType.area ? ( + + ) : widget.chart_type === WidgetChartType.funnel ? ( + + ) : null + ) : ( +
    + {widget.value} +
    + ) + ) : ( +
    + Something went wrong, please try again or use a different query. +
    + )} +
    + + {widget.type === WidgetType.scalar && widget.mdiIcon && ( +
    + +
    + )} +
    +
    +
    + ); +}; diff --git a/frontend/src/components/SmartWidget/components/AreaChart.tsx b/frontend/src/components/SmartWidget/components/AreaChart.tsx new file mode 100644 index 0000000..50bd5c7 --- /dev/null +++ b/frontend/src/components/SmartWidget/components/AreaChart.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { WidgetLibName } from '../models/widget.model'; +import { ApexAreaChart } from './AreaChart/ApexAreaChart'; +import { ChartJSAreaChart } from './AreaChart/ChartJSAreaChart'; + +export const AreaChart = ({ widget }) => { + return ( + <> + {!widget.lib_name && } + {widget.lib_name === WidgetLibName.chartjs && ( + + )} + {widget.lib_name === WidgetLibName.apex && ( + + )} + + ); +}; diff --git a/frontend/src/components/SmartWidget/components/AreaChart/ApexAreaChart.tsx b/frontend/src/components/SmartWidget/components/AreaChart/ApexAreaChart.tsx new file mode 100644 index 0000000..656008a --- /dev/null +++ b/frontend/src/components/SmartWidget/components/AreaChart/ApexAreaChart.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import dynamic from 'next/dynamic'; +import { humanize } from '../../../../helpers/humanize'; + +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); +type ValueType = { [key: string]: string | number }[]; + +export const ApexAreaChart = ({ widget }) => { + const dataForLineChart = (value: any[]) => { + if (!value?.length || value?.length > 10000) + return [{ name: '', data: [] }]; + + const valueKey = Object.keys(value[0])[1]; + const data = value.map((el) => +el[valueKey]); + + return [{ name: humanize(valueKey), data }]; + }; + + const optionsForLineChart = ( + value: ValueType, + chartColor: string[], + currency: boolean, + ) => { + const chartColors = Array.isArray(chartColor) + ? chartColor + : [chartColor || '#3751FF']; + const defaultOptions = { + xaxis: {}, + chart: { + toolbar: { + show: true, + offsetX: 0, + offsetY: 0, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + }, + export: { + csv: { + filename: undefined, + columnDelimiter: ',', + headerCategory: 'category', + headerValue: 'value', + }, + svg: { + filename: undefined, + }, + png: { + filename: undefined, + }, + }, + }, + }, + plotOptions: { + bar: { + distributed: false, + }, + }, + colors: [], + }; + + if (!value?.length || value?.length > 10000) return defaultOptions; + + const key = Object.keys(value[0])[0]; + const categories = value + .map((el) => el[key]) + .map((item) => + typeof item === 'string' && item?.length > 7 + ? item?.slice(0, 7) + : item || '', + ); + + if (categories.length <= 3) { + defaultOptions.plotOptions = { + bar: { + distributed: true, + }, + }; + } + + const colors = []; + for (let i = 0; i < categories.length; i++) { + colors.push(chartColors[i % chartColors.length]); + } + + return { + ...defaultOptions, + yaxis: { + labels: { + formatter: function (value) { + if (currency) { + return '$' + value; + } else { + return value; + } + }, + }, + }, + dataLabels: { + formatter: (val) => { + if (currency) { + return '$' + val; + } else { + return val; + } + }, + }, + legend: { + show: false, + }, + xaxis: { + categories, + }, + colors, + }; + }; + + return ( + + ); +}; diff --git a/frontend/src/components/SmartWidget/components/AreaChart/ChartJSAreaChart.tsx b/frontend/src/components/SmartWidget/components/AreaChart/ChartJSAreaChart.tsx new file mode 100644 index 0000000..5bb4cd2 --- /dev/null +++ b/frontend/src/components/SmartWidget/components/AreaChart/ChartJSAreaChart.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { Line } from 'react-chartjs-2'; +import chroma from 'chroma-js'; +import { humanize } from '../../../../helpers/humanize'; +import { collectOtherData, findFirstNumericKey } from '../../widgetHelpers'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Filler, + Legend, + ChartData, +} from 'chart.js'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Filler, + Legend, +); + +export const ChartJSAreaChart = ({ widget }) => { + const options = { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + display: true, + }, + x: { + display: true, + }, + }, + plugins: { + legend: { + display: true, + }, + }, + }; + + const dataForBarChart = ( + value: any[], + chartColors: string[], + ): ChartData<'line', number[], string> => { + if (!value?.length) return { labels: [''], datasets: [{ data: [] }] }; + const initColors = Array.isArray(chartColors) + ? chartColors + : [chartColors || '#3751FF']; + + const valueKey = findFirstNumericKey(value[0]); + const label = humanize(valueKey); + const data = value.map((el) => +el[valueKey]); + const labels = value.map((el) => + Object.keys(el).length <= 2 + ? humanize(String(el[Object.keys(el)[0]])) + : collectOtherData(el, valueKey), + ); + + const backgroundColor = + labels.length > initColors.length + ? chroma + .scale([ + chroma(initColors[0]).brighten(), + chroma(initColors.slice(-1)[0]).darken(), + ]) + .colors(labels.length) + : initColors; + + return { + labels, + datasets: [ + { + label, + data, + backgroundColor, + }, + ], + }; + }; + + return ( + + ); +}; diff --git a/frontend/src/components/SmartWidget/components/BarChart.tsx b/frontend/src/components/SmartWidget/components/BarChart.tsx new file mode 100644 index 0000000..bf7a79b --- /dev/null +++ b/frontend/src/components/SmartWidget/components/BarChart.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { ChartJSBarChart } from './BarChart/ChartJSBarChart'; +import { ApexBarChart } from './BarChart/ApexBarChart'; +import { WidgetLibName } from '../models/widget.model'; + +export const BarChart = ({ widget }) => { + return ( + <> + {!widget.lib_name && } + {widget.lib_name === WidgetLibName.chartjs && ( + + )} + {widget.lib_name === WidgetLibName.apex && ( + + )} + + ); +}; diff --git a/frontend/src/components/SmartWidget/components/BarChart/ApexBarChart.tsx b/frontend/src/components/SmartWidget/components/BarChart/ApexBarChart.tsx new file mode 100644 index 0000000..5b3d3e7 --- /dev/null +++ b/frontend/src/components/SmartWidget/components/BarChart/ApexBarChart.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import dynamic from 'next/dynamic'; +import { humanize } from '../../../../helpers/humanize'; + +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); +type ValueType = { [key: string]: string | number }[]; + +export const ApexBarChart = ({ widget }) => { + const dataForBarChart = (value: any[]) => { + if (!value?.length || value?.length > 10000) + return [{ name: '', data: [] }]; + + const valueKey = Object.keys(value[0])[1]; + const data = value.map((el) => +el[valueKey]); + + return [{ name: humanize(valueKey), data }]; + }; + const optionsForBarChart = ( + value: ValueType, + chartColor: string[], + currency: boolean, + ) => { + const chartColors = Array.isArray(chartColor) + ? chartColor + : [chartColor || '#3751FF']; + const defaultOptions = { + xaxis: {}, + chart: { + toolbar: { + show: true, + offsetX: 0, + offsetY: 0, + tools: { + download: true, + selection: true, + zoom: true, + }, + export: { + csv: { + filename: undefined, + columnDelimiter: ',', + headerCategory: 'category', + headerValue: 'value', + }, + svg: { + filename: undefined, + }, + png: { + filename: undefined, + }, + }, + }, + }, + plotOptions: { + bar: { + distributed: false, + }, + }, + colors: [], + }; + + if (!value?.length || value?.length > 10000) return defaultOptions; + + const key = Object.keys(value[0])[0]; + const categories = value + .map((el) => el[key]) + .map((item) => + typeof item === 'string' && item?.length > 20 + ? item?.slice(0, 15) + : item || '', + ); + + if (categories.length <= 3) { + defaultOptions.plotOptions = { + bar: { + distributed: true, + }, + }; + } + + const colors = []; + for (let i = 0; i < categories.length; i++) { + colors.push(chartColors[i % chartColors.length]); + } + + return { + ...defaultOptions, + yaxis: { + labels: { + formatter: function (value) { + if (currency) { + return '$' + value; + } else { + return value; + } + }, + }, + }, + dataLabels: { + formatter: (val) => { + if (currency) { + return '$' + val; + } else { + return val; + } + }, + }, + legend: { + show: true, + }, + xaxis: { + categories, + }, + colors, + }; + }; + + return ( + + ); +}; diff --git a/frontend/src/components/SmartWidget/components/BarChart/ChartJSBarChart.tsx b/frontend/src/components/SmartWidget/components/BarChart/ChartJSBarChart.tsx new file mode 100644 index 0000000..eac4769 --- /dev/null +++ b/frontend/src/components/SmartWidget/components/BarChart/ChartJSBarChart.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { humanize } from '../../../../helpers/humanize'; +import { Bar } from 'react-chartjs-2'; +import { collectOtherData, findFirstNumericKey } from '../../widgetHelpers'; +import { + BarElement, + CategoryScale, + Chart as ChartJS, + ChartData, + Legend, + LinearScale, + Title, + Tooltip, +} from 'chart.js'; +import chroma from 'chroma-js'; + +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +); + +export const ChartJSBarChart = ({ widget }) => { + console.log(widget); + const options = () => { + return { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: widget.label, + }, + }, + }; + }; + + const dataForBarChart = ( + value: any[], + chartColors: string[], + ): ChartData<'bar', number[], string> => { + if (!value?.length) return { labels: [''], datasets: [{ data: [] }] }; + + const initColors = Array.isArray(chartColors) + ? chartColors + : [chartColors || '#3751FF']; + + const valueKey = findFirstNumericKey(value[0]); + const label = humanize(valueKey); + const data = value.map((el) => +el[valueKey]); + const labels = value.map((el) => + Object.keys(el).length <= 2 + ? humanize(String(el[Object.keys(el)[0]])) + : collectOtherData(el, valueKey), + ); + + const backgroundColor = + labels.length > initColors.length + ? chroma + .scale([ + chroma(initColors[0]).brighten(), + chroma(initColors.slice(-1)[0]).darken(), + ]) + .colors(labels.length) + : initColors; + + return { + labels, + datasets: [ + { + label, + data, + backgroundColor, + }, + ], + }; + }; + + return ( + + ); +}; diff --git a/frontend/src/components/SmartWidget/components/FunnelChart.tsx b/frontend/src/components/SmartWidget/components/FunnelChart.tsx new file mode 100644 index 0000000..3d91280 --- /dev/null +++ b/frontend/src/components/SmartWidget/components/FunnelChart.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import dynamic from 'next/dynamic'; +import { humanize } from '../../../helpers/humanize'; + +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); +type ValueType = { [key: string]: string | number }[]; + +export const FunnelChart = ({ widget }) => { + const dataForBarChart = (value: any[]) => { + if (!value?.length || value?.length > 10000) + return [{ name: '', data: [] }]; + const valueKey = Object.keys(value[0])[1]; + const data = value.map((el) => +el[valueKey]); + + return [{ name: humanize(valueKey), data }]; + }; + const optionsForBarChart = ( + value: ValueType, + chartColor: string[], + currency: boolean, + ) => { + const chartColors = Array.isArray(chartColor) + ? chartColor + : [chartColor || '#3751FF']; + const defaultOptions = { + xaxis: {}, + chart: { + toolbar: { + show: true, + offsetX: 0, + offsetY: 0, + tools: { + download: true, + selection: true, + zoom: true, + }, + export: { + csv: { + filename: undefined, + columnDelimiter: ',', + headerCategory: 'category', + headerValue: 'value', + }, + svg: { + filename: undefined, + }, + png: { + filename: undefined, + }, + }, + }, + }, + plotOptions: { + bar: { + distributed: false, + horizontal: true, + isFunnel: true, + }, + }, + colors: [], + }; + + if (!value?.length || value?.length > 10000) return defaultOptions; + + const key = Object.keys(value[0])[0]; + const categories = value + .map((el) => el[key]) + .map((item) => + typeof item === 'string' && item?.length > 20 + ? item?.slice(0, 15) + : item || '', + ); + + if (categories.length <= 3) { + defaultOptions.plotOptions = { + bar: { + distributed: true, + horizontal: true, + isFunnel: true, + }, + }; + } + + const colors = []; + for (let i = 0; i < categories.length; i++) { + colors.push(chartColors[i % chartColors.length]); + } + + return { + ...defaultOptions, + yaxis: { + labels: { + formatter: function (value) { + if (currency) { + return '$' + value; + } else { + return value; + } + }, + }, + }, + dataLabels: { + formatter: (val) => { + if (currency) { + return '$' + val; + } else { + return val; + } + }, + }, + legend: { + show: true, + }, + xaxis: { + categories, + }, + colors, + }; + }; + + return ( + + ); +}; diff --git a/frontend/src/components/SmartWidget/components/LineChart.tsx b/frontend/src/components/SmartWidget/components/LineChart.tsx new file mode 100644 index 0000000..3b9e7b0 --- /dev/null +++ b/frontend/src/components/SmartWidget/components/LineChart.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { WidgetLibName } from '../models/widget.model'; +import { ApexLineChart } from './LineChart/ApexLineChart'; +import { ChartJSLineChart } from './LineChart/ChartJSLineChart'; + +export const LineChart = ({ widget }) => { + return ( + <> + {!widget.lib_name && } + {widget.lib_name === WidgetLibName.chartjs && ( + + )} + {widget.lib_name === WidgetLibName.apex && ( + + )} + + ); +}; diff --git a/frontend/src/components/SmartWidget/components/LineChart/ApexLineChart.tsx b/frontend/src/components/SmartWidget/components/LineChart/ApexLineChart.tsx new file mode 100644 index 0000000..aa77a76 --- /dev/null +++ b/frontend/src/components/SmartWidget/components/LineChart/ApexLineChart.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import dynamic from 'next/dynamic'; +import { humanize } from '../../../../helpers/humanize'; + +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); +type ValueType = { [key: string]: string | number }[]; + +export const ApexLineChart = ({ widget }) => { + const dataForLineChart = (value: any[]) => { + if (!value?.length || value?.length > 10000) + return [{ name: '', data: [] }]; + + const valueKey = Object.keys(value[0])[1]; + const data = value.map((el) => +el[valueKey]); + + return [{ name: humanize(valueKey), data }]; + }; + + const optionsForLineChart = ( + value: ValueType, + chartColor: string[], + currency: boolean, + ) => { + const chartColors = Array.isArray(chartColor) + ? chartColor + : [chartColor || '#3751FF']; + const defaultOptions = { + xaxis: {}, + chart: { + toolbar: { + show: true, + offsetX: 0, + offsetY: 0, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + }, + export: { + csv: { + filename: undefined, + columnDelimiter: ',', + headerCategory: 'category', + headerValue: 'value', + }, + svg: { + filename: undefined, + }, + png: { + filename: undefined, + }, + }, + }, + }, + plotOptions: { + bar: { + distributed: false, + }, + }, + colors: [], + }; + + if (!value?.length || value?.length > 10000) return defaultOptions; + + const key = Object.keys(value[0])[0]; + const categories = value + .map((el) => el[key]) + .map((item) => + typeof item === 'string' && item?.length > 7 + ? item?.slice(0, 7) + : item || '', + ); + + if (categories.length <= 3) { + defaultOptions.plotOptions = { + bar: { + distributed: true, + }, + }; + } + + const colors = []; + for (let i = 0; i < categories.length; i++) { + colors.push(chartColors[i % chartColors.length]); + } + + return { + ...defaultOptions, + yaxis: { + labels: { + formatter: function (value) { + if (currency) { + return '$' + value; + } else { + return value; + } + }, + }, + }, + dataLabels: { + formatter: (val) => { + if (currency) { + return '$' + val; + } else { + return val; + } + }, + }, + legend: { + show: false, + }, + xaxis: { + categories, + }, + colors, + }; + }; + + return ( + + ); +}; diff --git a/frontend/src/components/SmartWidget/components/LineChart/ChartJSLineChart.tsx b/frontend/src/components/SmartWidget/components/LineChart/ChartJSLineChart.tsx new file mode 100644 index 0000000..a2fe5ba --- /dev/null +++ b/frontend/src/components/SmartWidget/components/LineChart/ChartJSLineChart.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { humanize } from '../../../../helpers/humanize'; +import { Line } from 'react-chartjs-2'; +import chroma from 'chroma-js'; +import { collectOtherData, findFirstNumericKey } from '../../widgetHelpers'; +import { Widget } from '../../models/widget.model'; +import { + Chart, + LineElement, + PointElement, + LineController, + LinearScale, + CategoryScale, + Tooltip, + ChartData, +} from 'chart.js'; + +Chart.register( + LineElement, + PointElement, + LineController, + LinearScale, + CategoryScale, + Tooltip, +); + +interface Props { + widget: Widget; +} + +export const ChartJSLineChart = (props: Props) => { + const options = { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + display: true, + }, + x: { + display: true, + }, + }, + plugins: { + legend: { + display: true, + }, + }, + }; + + const dataForBarChart = ( + value: any[], + chartColors: string[], + ): ChartData<'line', number[], string> => { + if (!value?.length) return { labels: [''], datasets: [{ data: [] }] }; + const initColors = Array.isArray(chartColors) + ? chartColors + : [chartColors || '#3751FF']; + + const valueKey = findFirstNumericKey(value[0]); + const label = humanize(valueKey); + const data = value.map((el) => +el[valueKey]); + const labels = value.map((el) => + Object.keys(el).length <= 2 + ? humanize(String(el[Object.keys(el)[0]])) + : collectOtherData(el, valueKey), + ); + + const backgroundColor = + labels.length > initColors.length + ? chroma + .scale([ + chroma(initColors[0]).brighten(), + chroma(initColors.slice(-1)[0]).darken(), + ]) + .colors(labels.length) + : initColors; + + return { + labels, + datasets: [ + { + label, + data, + backgroundColor, + }, + ], + }; + }; + + return ( + + ); +}; diff --git a/frontend/src/components/SmartWidget/components/PieChart.tsx b/frontend/src/components/SmartWidget/components/PieChart.tsx new file mode 100644 index 0000000..f6f0fd3 --- /dev/null +++ b/frontend/src/components/SmartWidget/components/PieChart.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { WidgetLibName } from '../models/widget.model'; +import { ApexPieChart } from './PieChart/ApexPieChart'; +import { ChartJSPieChart } from './PieChart/ChartJSPieChart'; + +export const PieChart = ({ widget }) => { + return ( + <> + {!widget.lib_name && } + {widget.lib_name === WidgetLibName.chartjs && ( + + )} + {widget.lib_name === WidgetLibName.apex && ( + + )} + + ); +}; diff --git a/frontend/src/components/SmartWidget/components/PieChart/ApexPieChart.tsx b/frontend/src/components/SmartWidget/components/PieChart/ApexPieChart.tsx new file mode 100644 index 0000000..dfe5572 --- /dev/null +++ b/frontend/src/components/SmartWidget/components/PieChart/ApexPieChart.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import dynamic from 'next/dynamic'; +import chroma from 'chroma-js'; + +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); +type ValueType = { [key: string]: string | number }[]; + +export const ApexPieChart = ({ widget }) => { + const optionsForPieChart = (value: ValueType, chartColor: string) => { + const chartColors = Array.isArray(chartColor) + ? chartColor + : [chartColor || '#3751FF']; + const defaultOptions = { + xaxis: {}, + toolbar: { + show: true, + offsetX: 0, + offsetY: 0, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + customIcons: [], + }, + export: { + csv: { + filename: undefined, + columnDelimiter: ',', + headerCategory: 'category', + headerValue: 'value', + }, + svg: { + filename: undefined, + }, + png: { + filename: undefined, + }, + }, + autoSelected: 'zoom', + }, + colors: [], + }; + + if (!value?.length || value?.length > 10000) return defaultOptions; + + if ( + !isNaN(Number(value[0][Object.keys(value[0])[1]])) && + isFinite(Number(value[0][Object.keys(value[0])[1]])) + ) { + const labels = value + .map((el) => String(el[Object.keys(value[0])[0]])) + .reverse(); + + let colors: string[] | (string & any[]); + if (labels.length > chartColors.length) { + colors = chroma + .scale([ + chroma(chartColors.at(0)).brighten(), + chroma(chartColors.at(-1)).darken(), + ]) + .colors(labels.length); + } else { + colors = chartColors; + } + + return { + ...defaultOptions, + colors, + labels, + }; + } + const key = Object.keys(value[0])[1]; + const categories = value.map((el) => String(el[key])).reverse(); + + return { + ...defaultOptions, + labels: categories, + }; + }; + const dataForPieChart = (value: any[]) => { + if (!value?.length || value?.length > 10000) + return [{ name: '', data: [] }]; + + if ( + !isNaN(parseFloat(value[0][Object.keys(value[0])[1]])) && + isFinite(value[0][Object.keys(value[0])[1]]) + ) { + return value.map((el) => +el[Object.keys(value[0])[1]]).reverse(); + } + const valueKey = Object.keys(value[0])[0]; + return value.map((el) => +el[valueKey]).reverse(); + }; + + return ( + + ); +}; diff --git a/frontend/src/components/SmartWidget/components/PieChart/ChartJSPieChart.tsx b/frontend/src/components/SmartWidget/components/PieChart/ChartJSPieChart.tsx new file mode 100644 index 0000000..2a20155 --- /dev/null +++ b/frontend/src/components/SmartWidget/components/PieChart/ChartJSPieChart.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { humanize } from '../../../../helpers/humanize'; +import { Pie } from 'react-chartjs-2'; +import chroma from 'chroma-js'; +import { collectOtherData, findFirstNumericKey } from '../../widgetHelpers'; + +import { + Chart as ChartJS, + ArcElement, + Tooltip, + Legend, + ChartData, +} from 'chart.js'; + +ChartJS.register(ArcElement, Tooltip, Legend); + +export const ChartJSPieChart = ({ widget }) => { + const options = () => { + return { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'right' as const, + }, + title: { + display: true, + text: widget.label, + }, + }, + }; + }; + + const dataForBarChart = ( + value: any[], + chartColors: string[], + ): ChartData<'pie', number[], string> => { + if (!value?.length) return { labels: [''], datasets: [{ data: [] }] }; + const initColors = Array.isArray(chartColors) + ? chartColors + : [chartColors || '#3751FF']; + + const valueKey = findFirstNumericKey(value[0]); + const label = humanize(valueKey); + const data = value.map((el) => +el[valueKey]); + const labels = value.map((el) => + Object.keys(el).length <= 2 + ? humanize(String(el[Object.keys(el)[0]])) + : collectOtherData(el, valueKey), + ); + + const backgroundColor = + labels.length > initColors.length + ? chroma + .scale([ + chroma(initColors[0]).brighten(), + chroma(initColors.slice(-1)[0]).darken(), + ]) + .colors(labels.length) + : initColors; + + return { + labels, + datasets: [ + { + label, + data, + backgroundColor, + }, + ], + }; + }; + + return ( + + ); +}; diff --git a/frontend/src/components/SmartWidget/models/widget.model.ts b/frontend/src/components/SmartWidget/models/widget.model.ts new file mode 100644 index 0000000..362d65c --- /dev/null +++ b/frontend/src/components/SmartWidget/models/widget.model.ts @@ -0,0 +1,35 @@ +export enum WidgetLibName { + apex = 'apex', + chartjs = 'chartjs', +} + +export enum WidgetChartType { + scalar = 'scalar', + bar = 'bar', + line = 'line', + pie = 'pie', + area = 'area', + funnel = 'funnel', +} + +export enum WidgetType { + chart = 'chart', + scalar = 'scalar', +} + +export interface Widget { + type: WidgetType; + chartType: WidgetChartType; + query: string; + mdiIcon: string; + iconColor: string; + label: string; + id: string; + lib?: WidgetLibName; + value: any[]; + chartColors: string[]; + options?: any; + prompt: string; + color: string; + color_array: string[]; +} diff --git a/frontend/src/components/SmartWidget/widgetHelpers.tsx b/frontend/src/components/SmartWidget/widgetHelpers.tsx new file mode 100644 index 0000000..73f6d91 --- /dev/null +++ b/frontend/src/components/SmartWidget/widgetHelpers.tsx @@ -0,0 +1,38 @@ +import { humanize } from '../../helpers/humanize'; + +interface DataObject { + [key: string]: any; +} + +export const findFirstNumericKey = ( + obj: Record, +): string | undefined => { + for (const [key, value] of Object.entries(obj)) { + if (typeof value === 'string') { + const trimmedValue = value.trim(); + + // Only allow numbers, and optionally a single decimal point + const isNumeric = /^-?\d+(\.\d+)?$/.test(trimmedValue); + + if (isNumeric) { + // Check if the number is the same as the trimmed value + // This is to avoid cases like '1.0' being treated as a number + const numberValue = parseFloat(trimmedValue); + if (numberValue.toString() === trimmedValue) { + return key; + } + } + } + } + return undefined; +}; + +export const collectOtherData = ( + obj: DataObject, + excludeKey: string, +): string => { + return Object.entries(obj) + .filter(([key, _]) => key !== excludeKey) + .map(([_, value]) => humanize(value)) + .join(' / '); +}; diff --git a/frontend/src/components/SwitchField.tsx b/frontend/src/components/SwitchField.tsx new file mode 100644 index 0000000..324a287 --- /dev/null +++ b/frontend/src/components/SwitchField.tsx @@ -0,0 +1,19 @@ +import React, { useEffect, useId, useState } from 'react'; +import Switch from 'react-switch'; + +export const SwitchField = ({ field, form, disabled }) => { + const handleChange = (data: any) => { + form.setFieldValue(field.name, data); + }; + + return ( + + ); +}; diff --git a/frontend/src/components/TableSampleClients.tsx b/frontend/src/components/TableSampleClients.tsx new file mode 100644 index 0000000..5eaf9d1 --- /dev/null +++ b/frontend/src/components/TableSampleClients.tsx @@ -0,0 +1,149 @@ +import { mdiEye, mdiTrashCan } from '@mdi/js'; +import React, { useState } from 'react'; +import { useSampleClients } from '../hooks/sampleData'; +import { Client } from '../interfaces'; +import BaseButton from './BaseButton'; +import BaseButtons from './BaseButtons'; +import CardBoxModal from './CardBoxModal'; +import UserAvatar from './UserAvatar'; + +const TableSampleClients = () => { + const { clients } = useSampleClients(); + + const perPage = 5; + + const [currentPage, setCurrentPage] = useState(0); + + const clientsPaginated = clients.slice( + perPage * currentPage, + perPage * (currentPage + 1), + ); + + const numPages = clients.length / perPage; + + const pagesList = []; + + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + return ( + <> + +

    + Lorem ipsum dolor sit amet adipiscing elit +

    +

    This is sample modal

    +
    + + +

    + Lorem ipsum dolor sit amet adipiscing elit +

    +

    This is sample modal

    +
    + + + + + + + + + + + + + {clientsPaginated.map((client: Client) => ( + + + + + + + + + + ))} + +
    + NameCompanyCityProgressCreated +
    + + {client.name}{client.company}{client.city} + + {client.progress} + + + + {client.created} + + + + setIsModalInfoActive(true)} + small + /> + setIsModalTrashActive(true)} + small + /> + +
    +
    +
    + + {pagesList.map((page) => ( + setCurrentPage(page)} + /> + ))} + + + Page {currentPage + 1} of {numPages} + +
    +
    + + ); +}; + +export default TableSampleClients; diff --git a/frontend/src/components/Uploaders/FilesUploader.js b/frontend/src/components/Uploaders/FilesUploader.js new file mode 100644 index 0000000..c68ffb9 --- /dev/null +++ b/frontend/src/components/Uploaders/FilesUploader.js @@ -0,0 +1,133 @@ +import React, { useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import FileUploader from 'components/FormItems/uploaders/UploadService'; +import Errors from '../../../components/FormItems/error/errors'; + +const FilesUploader = (props) => { + const { value, onChange, schema, path, max, readonly } = props; + + const [loading, setLoading] = useState(false); + const inputElement = useRef(null); + + const valuesArr = () => { + if (!value) { + return []; + } + return Array.isArray(value) ? value : [value]; + }; + + const fileList = () => { + return valuesArr().map((item) => ({ + uid: item.id || undefined, + name: item.name, + status: 'done', + url: item.publicUrl, + })); + }; + + const handleRemove = (id) => { + onChange(valuesArr().filter((item) => item.id !== id)); + }; + + const handleChange = async (event) => { + try { + const files = event.target.files; + + if (!files || !files.length) { + return; + } + + let file = files[0]; + + FileUploader.validate(file, schema); + + setLoading(true); + + file = await FileUploader.upload(path, file, schema); + + inputElement.current.value = ''; + setLoading(false); + onChange([...valuesArr(), file]); + } catch (error) { + inputElement.current.value = ''; + console.log('error', error); + setLoading(false); + Errors.showMessage(error); + } + }; + + const formats = () => { + if (schema && schema.formats) { + return schema.formats.map((format) => `.${format}`).join(','); + } + return undefined; + }; + + const uploadButton = ( + + ); + + return ( +
    + {readonly || (max && fileList().length >= max) ? null : uploadButton} + + {valuesArr() && valuesArr().length ? ( +
    + {valuesArr().map((item) => { + return ( +
    + + + {item.name} + + + {!readonly && ( + + )} +
    + ); + })} +
    + ) : null} +
    + ); +}; + +FilesUploader.propTypes = { + readonly: PropTypes.bool, + path: PropTypes.string, + max: PropTypes.number, + schema: PropTypes.shape({ + image: PropTypes.bool, + size: PropTypes.number, + formats: PropTypes.arrayOf(PropTypes.string), + }), + value: PropTypes.any, + onChange: PropTypes.func, +}; + +export default FilesUploader; diff --git a/frontend/src/components/Uploaders/ImagesUploader.js b/frontend/src/components/Uploaders/ImagesUploader.js new file mode 100644 index 0000000..f4e14d0 --- /dev/null +++ b/frontend/src/components/Uploaders/ImagesUploader.js @@ -0,0 +1,227 @@ +import React, { useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import Button from '@mui/material/Button'; +import CloseIcon from '@mui/icons-material/Close'; +import SearchIcon from '@mui/icons-material/Search'; +import Grid from '@mui/material/Grid'; +import Box from '@mui/material/Box'; +import Dialog from '@mui/material/Dialog'; +import FileUploader from 'components/FormItems/uploaders/UploadService'; +import Errors from '../../../components/FormItems/error/errors'; +import { makeStyles } from '@mui/styles'; + +const useStyles = makeStyles({ + actionButtonsWrapper: { + position: 'relative', + }, + previewContent: { + padding: '0px !important', + }, + imageItem: { + '&.MuiGrid-root': { + margin: 10, + boxShadow: '2px 2px 8px 0 rgb(0 0 0 / 40%)', + borderRadius: 10, + }, + height: '100px', + }, + actionButtons: { + position: 'absolute', + bottom: 5, + right: 4, + }, + previewContainer: { + '& button': { + position: 'absolute', + top: 10, + right: 10, + '& svg': { + height: 50, + width: 50, + fill: '#FFF', + stroke: '#909090', + strokeWidth: 0.5, + }, + }, + }, + button: { + padding: '0px !important', + minWidth: '45px !important', + '& svg': { + height: 36, + width: 36, + fill: '#FFF', + stroke: '#909090', + strokeWidth: 0.5, + }, + }, +}); + +const ImagesUploader = (props) => { + const classes = useStyles(); + const { value, onChange, schema, path, max, readonly, name } = props; + + const [loading, setLoading] = useState(false); + const [showPreview, setShowPreview] = useState(false); + const [imageMeta, setImageMeta] = useState({ + imageSrc: null, + imageAlt: null, + }); + const inputElement = useRef(null); + + const valuesArr = () => { + if (!value) { + return []; + } + return Array.isArray(value) ? value : [value]; + }; + + const fileList = () => { + return valuesArr().map((item) => ({ + uid: item.id || undefined, + name: item.name, + status: 'done', + url: item.publicUrl, + })); + }; + + const handleRemove = (id) => { + onChange(valuesArr().filter((item) => item.id !== id)); + }; + + const handleChange = async (event) => { + try { + const files = event.target.files; + + if (!files || !files.length) { + return; + } + + let file = files[0]; + + FileUploader.validate(file, schema); + + setLoading(true); + + file = await FileUploader.upload(path, file, schema); + + inputElement.current.value = ''; + setLoading(false); + onChange([...valuesArr(), file]); + } catch (error) { + inputElement.current.value = ''; + console.log('error', error); + setLoading(false); + Errors.showMessage(error); + } + }; + + const doPreviewImage = (image) => { + setImageMeta({ + imageSrc: image.publicUrl, + imageAlt: image.name, + }); + setShowPreview(true); + }; + + const doCloseImageModal = () => { + setImageMeta({ + imageSrc: null, + imageAlt: null, + }); + setShowPreview(false); + }; + + const uploadButton = ( + + + + ); + + return ( + + {readonly || (max && fileList().length >= max) ? null : uploadButton} + + {valuesArr() && valuesArr().length ? ( + + {valuesArr().map((item) => { + return ( + + {item.name} + +
    +
    + + {!readonly && ( + + )} +
    +
    +
    + ); + })} +
    + ) : null} + + + {imageMeta.imageAlt} + +
    + ); +}; + +ImagesUploader.propTypes = { + readonly: PropTypes.bool, + path: PropTypes.string, + max: PropTypes.number, + schema: PropTypes.shape({ + image: PropTypes.bool, + size: PropTypes.number, + formats: PropTypes.arrayOf(PropTypes.string), + }), + value: PropTypes.any, + onChange: PropTypes.func, + name: PropTypes.string, +}; + +export default ImagesUploader; diff --git a/frontend/src/components/Uploaders/UploadService.js b/frontend/src/components/Uploaders/UploadService.js new file mode 100644 index 0000000..19c5304 --- /dev/null +++ b/frontend/src/components/Uploaders/UploadService.js @@ -0,0 +1,82 @@ +import { v4 as uuidv4 } from 'uuid'; +import Axios from 'axios'; +import { baseURLApi } from '../../config'; + +function extractExtensionFrom(filename) { + if (!filename) { + return null; + } + + const regex = /(?:\.([^.]+))?$/; + return regex.exec(filename)[1]; +} + +export default class FileUploader { + static validate(file, schema) { + if (!schema) { + return; + } + + if (schema.image) { + if (!file.type.startsWith('image')) { + throw new Error('You must upload an image'); + } + } + + if (schema.size && file.size > schema.size) { + throw new Error('File is too big.'); + } + + const extension = extractExtensionFrom(file.name); + + if (schema.formats && !schema.formats.includes(extension)) { + throw new Error('Invalid format'); + } + } + + static async upload(path, file, schema) { + try { + this.validate(file, schema); + } catch (error) { + return Promise.reject(error); + } + + const extension = extractExtensionFrom(file.name); + const id = uuidv4(); + const filename = `${id}.${extension}`; + const privateUrl = `${path}/${filename}`; + + const publicUrl = await this.uploadToServer(file, path, filename); + + return { + id: id, + name: file.name, + sizeInBytes: file.size, + privateUrl, + publicUrl, + new: true, + }; + } + + static async uploadToServer(file, path, filename) { + const formData = new FormData(); + formData.append('file', file); + formData.append('filename', filename); + const uri = `/file/upload/${path}`; + await Axios.post(uri, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + const privateUrl = `${path}/${filename}`; + + console.log( + 'process.env.NODE_ENV in uploadToServer function', + process.env.NODE_ENV, + ); + console.log('baseURLApi in uploadToServer function', baseURLApi); + + return `${baseURLApi}/file/download?privateUrl=${privateUrl}`; + } +} diff --git a/frontend/src/components/UserAvatar.tsx b/frontend/src/components/UserAvatar.tsx new file mode 100644 index 0000000..a517a4c --- /dev/null +++ b/frontend/src/components/UserAvatar.tsx @@ -0,0 +1,48 @@ +/* eslint-disable @next/next/no-img-element */ +// Why disabled: +// avatars.dicebear.com provides svg avatars +// next/image needs dangerouslyAllowSVG option for that + +import React, { ReactNode } from 'react'; +import BaseIcon from './BaseIcon'; +import { mdiAccountCircleOutline } from '@mdi/js'; + +type Props = { + username: string; + avatar?: string | null; + image?: object | null; + api?: string; + className?: string; + children?: ReactNode; +}; + +export default function UserAvatar({ + username, + image, + avatar, + className = '', + children, +}: Props) { + const avatarImage = image && image[0] ? `${image[0].publicUrl}` : '#'; + + return ( +
    + {avatarImage === '#' ? ( + + ) : ( + {username} + )} + {children} +
    + ); +} diff --git a/frontend/src/components/UserAvatarCurrentUser.tsx b/frontend/src/components/UserAvatarCurrentUser.tsx new file mode 100644 index 0000000..1bcf833 --- /dev/null +++ b/frontend/src/components/UserAvatarCurrentUser.tsx @@ -0,0 +1,48 @@ +import React, { ReactNode, useEffect, useState } from 'react'; +import { useAppSelector } from '../stores/hooks'; +import UserAvatar from './UserAvatar'; + +type Props = { + className?: string; + children?: ReactNode; +}; + +export default function UserAvatarCurrentUser({ + className = '', + children, +}: Props) { + const userName = useAppSelector((state) => state.main.userName); + const userAvatar = useAppSelector((state) => state.main.userAvatar); + const { currentUser, isFetching, token } = useAppSelector( + (state) => state.auth, + ); + const { users, loading } = useAppSelector((state) => state.users); + + const [avatar, setAvatar] = useState(null); + + useEffect(() => { + currentUserAvatarCheck(); + }, []); + + useEffect(() => { + currentUserAvatarCheck(); + }, [currentUser?.id, users]); + + const currentUserAvatarCheck = () => { + if (currentUser?.id) { + const image = currentUser?.avatar; + setAvatar(image); + } + }; + + return ( + + {children} + + ); +} diff --git a/frontend/src/components/UserCard.tsx b/frontend/src/components/UserCard.tsx new file mode 100644 index 0000000..15fc7e5 --- /dev/null +++ b/frontend/src/components/UserCard.tsx @@ -0,0 +1,47 @@ +import { mdiCheckDecagram } from '@mdi/js'; +import { Field, Form, Formik } from 'formik'; +import { useAppSelector } from '../stores/hooks'; +import CardBox from './CardBox'; +import FormCheckRadio from './FormCheckRadio'; +import UserAvatarCurrentUser from './UserAvatarCurrentUser'; + +type Props = { + className?: string; +}; + +const UserCard = ({ className }: Props) => { + const userName = useAppSelector((state) => state.main.userName); + + return ( + +
    + +
    +
    + alert(JSON.stringify(values, null, 2))} + > +
    + + + +
    +
    +
    +

    + Howdy, {userName}! +

    +

    + Last login 12 mins ago from 127.0.0.1 +

    +
    Verified
    +
    +
    +
    + ); +}; + +export default UserCard; diff --git a/frontend/src/components/Users/CardUsers.tsx b/frontend/src/components/Users/CardUsers.tsx new file mode 100644 index 0000000..f4ce406 --- /dev/null +++ b/frontend/src/components/Users/CardUsers.tsx @@ -0,0 +1,196 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + users: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardUsers = ({ + users, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_USERS'); + + return ( +
    + {loading && } +
      + {!loading && + users.map((item, index) => ( +
    • +
      + + +

      {item.firstName}

      + + +
      + +
      +
      +
      +
      +
      + First Name +
      +
      +
      + {item.firstName} +
      +
      +
      + +
      +
      + Last Name +
      +
      +
      + {item.lastName} +
      +
      +
      + +
      +
      + Phone Number +
      +
      +
      + {item.phoneNumber} +
      +
      +
      + +
      +
      + E-Mail +
      +
      +
      {item.email}
      +
      +
      + +
      +
      + Disabled +
      +
      +
      + {dataFormatter.booleanFormatter(item.disabled)} +
      +
      +
      + +
      +
      + Avatar +
      +
      +
      + +
      +
      +
      + +
      +
      + App Role +
      +
      +
      + {dataFormatter.rolesOneListFormatter(item.app_role)} +
      +
      +
      + +
      +
      + Custom Permissions +
      +
      +
      + {dataFormatter + .permissionsManyListFormatter(item.custom_permissions) + .join(', ')} +
      +
      +
      +
      +
    • + ))} + {!loading && users.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardUsers; diff --git a/frontend/src/components/Users/ListUsers.tsx b/frontend/src/components/Users/ListUsers.tsx new file mode 100644 index 0000000..5e9a3aa --- /dev/null +++ b/frontend/src/components/Users/ListUsers.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + users: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListUsers = ({ + users, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_USERS'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + users.map((item) => ( + +
    + + + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    First Name

    +

    {item.firstName}

    +
    + +
    +

    Last Name

    +

    {item.lastName}

    +
    + +
    +

    Phone Number

    +

    {item.phoneNumber}

    +
    + +
    +

    E-Mail

    +

    {item.email}

    +
    + +
    +

    Disabled

    +

    + {dataFormatter.booleanFormatter(item.disabled)} +

    +
    + +
    +

    Avatar

    + +
    + +
    +

    App Role

    +

    + {dataFormatter.rolesOneListFormatter(item.app_role)} +

    +
    + +
    +

    + Custom Permissions +

    +

    + {dataFormatter + .permissionsManyListFormatter(item.custom_permissions) + .join(', ')} +

    +
    + + +
    +
    + ))} + {!loading && users.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListUsers; diff --git a/frontend/src/components/Users/TableUsers.tsx b/frontend/src/components/Users/TableUsers.tsx new file mode 100644 index 0000000..f39c7f5 --- /dev/null +++ b/frontend/src/components/Users/TableUsers.tsx @@ -0,0 +1,482 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/users/usersSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureUsersCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleUsers = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + users, + loading, + count, + notify: usersNotify, + refetch, + } = useAppSelector((state) => state.users); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (usersNotify.showNotification) { + notify(usersNotify.typeNotification, usersNotify.textNotification); + } + }, [usersNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `users`, currentUser).then((newCols) => + setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + delete data?.password; + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={users ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleUsers; diff --git a/frontend/src/components/Users/configureUsersCols.tsx b/frontend/src/components/Users/configureUsersCols.tsx new file mode 100644 index 0000000..d75e835 --- /dev/null +++ b/frontend/src/components/Users/configureUsersCols.tsx @@ -0,0 +1,182 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_USERS'); + + return [ + { + field: 'firstName', + headerName: 'First Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'lastName', + headerName: 'Last Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'phoneNumber', + headerName: 'Phone Number', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'email', + headerName: 'E-Mail', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'disabled', + headerName: 'Disabled', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'boolean', + }, + + { + field: 'avatar', + headerName: 'Avatar', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: false, + sortable: false, + renderCell: (params: GridValueGetterParams) => ( + + ), + }, + + { + field: 'app_role', + headerName: 'App Role', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('roles'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'custom_permissions', + headerName: 'Custom Permissions', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: false, + sortable: false, + type: 'singleSelect', + valueFormatter: ({ value }) => + dataFormatter.permissionsManyListFormatter(value).join(', '), + renderEditCell: (params) => ( + + ), + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ + , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Videos/CardVideos.tsx b/frontend/src/components/Videos/CardVideos.tsx new file mode 100644 index 0000000..9f58f97 --- /dev/null +++ b/frontend/src/components/Videos/CardVideos.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + videos: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardVideos = ({ + videos, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_VIDEOS'); + + return ( +
    + {loading && } +
      + {!loading && + videos.map((item, index) => ( +
    • +
      + + {item.title} + + +
      + +
      +
      +
      +
      +
      Title
      +
      +
      {item.title}
      +
      +
      + +
      +
      URL
      +
      +
      {item.url}
      +
      +
      + +
      +
      + Status +
      +
      +
      + {item.status} +
      +
      +
      +
      +
    • + ))} + {!loading && videos.length === 0 && ( +
      +

      No data to display

      +
      + )} +
    +
    + +
    +
    + ); +}; + +export default CardVideos; diff --git a/frontend/src/components/Videos/ListVideos.tsx b/frontend/src/components/Videos/ListVideos.tsx new file mode 100644 index 0000000..4d03649 --- /dev/null +++ b/frontend/src/components/Videos/ListVideos.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + videos: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListVideos = ({ + videos, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_VIDEOS'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
    + {loading && } + {!loading && + videos.map((item) => ( + +
    + dark:divide-dark-700 overflow-x-auto' + } + > +
    +

    Title

    +

    {item.title}

    +
    + +
    +

    URL

    +

    {item.url}

    +
    + +
    +

    Status

    +

    {item.status}

    +
    + + +
    +
    + ))} + {!loading && videos.length === 0 && ( +
    +

    No data to display

    +
    + )} +
    +
    + +
    + + ); +}; + +export default ListVideos; diff --git a/frontend/src/components/Videos/TableVideos.tsx b/frontend/src/components/Videos/TableVideos.tsx new file mode 100644 index 0000000..d6e231e --- /dev/null +++ b/frontend/src/components/Videos/TableVideos.tsx @@ -0,0 +1,481 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/videos/videosSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureVideosCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleVideos = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + videos, + loading, + count, + notify: videosNotify, + refetch, + } = useAppSelector((state) => state.videos); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (videosNotify.showNotification) { + notify(videosNotify.typeNotification, videosNotify.textNotification); + } + }, [videosNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `videos`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
    + `datagrid--row`} + rows={videos ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
    + ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
    + <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
    +
    +
    + Filter +
    + + {filters.map((selectOption) => ( + + ))} + +
    + {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
    +
    Value
    + + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
    +
    +
    + From +
    + +
    +
    +
    + To +
    + +
    +
    + ) : ( +
    +
    + Contains +
    + +
    + )} +
    +
    + Action +
    + { + deleteFilter(filterItem.id); + }} + /> +
    +
    + ); + })} +
    + + +
    + +
    +
    +
    + ) : null} + +

    Are you sure you want to delete this item?

    +
    + + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleVideos; diff --git a/frontend/src/components/Videos/configureVideosCols.tsx b/frontend/src/components/Videos/configureVideosCols.tsx new file mode 100644 index 0000000..b1458d9 --- /dev/null +++ b/frontend/src/components/Videos/configureVideosCols.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_VIDEOS'); + + return [ + { + field: 'title', + headerName: 'Title', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'url', + headerName: 'URL', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ + , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/WebPageComponents/AboutUsComponent/designs/ImageLeft.tsx b/frontend/src/components/WebPageComponents/AboutUsComponent/designs/ImageLeft.tsx new file mode 100644 index 0000000..b02478c --- /dev/null +++ b/frontend/src/components/WebPageComponents/AboutUsComponent/designs/ImageLeft.tsx @@ -0,0 +1,54 @@ +import React, { useEffect, useState } from 'react'; +import { getMultiplePexelsImages } from '../../../../helpers/pexels'; +import { useAppSelector } from '../../../../stores/hooks'; +import BaseButton from '../../../BaseButton'; + +const ImageLeft = ({ + projectName, + mainText, + subTitle, + imageAbout, + buttonText, + corners, + textSecondary, +}) => { + return ( +
    +
    + + + {/* Text Section (Теперь справа) */} +
    +

    {mainText}

    +

    {subTitle}

    + +
    +
    +
    + ); +}; + +export default ImageLeft; diff --git a/frontend/src/components/WebPageComponents/AboutUsComponent/designs/ImageRight.tsx b/frontend/src/components/WebPageComponents/AboutUsComponent/designs/ImageRight.tsx new file mode 100644 index 0000000..3af73eb --- /dev/null +++ b/frontend/src/components/WebPageComponents/AboutUsComponent/designs/ImageRight.tsx @@ -0,0 +1,53 @@ +import React, { useEffect, useState } from 'react'; +import { getMultiplePexelsImages } from '../../../../helpers/pexels'; +import { useAppSelector } from '../../../../stores/hooks'; +import BaseButton from '../../../BaseButton'; + +const ImageRight = ({ + projectName, + mainText, + subTitle, + imageAbout, + buttonText, + corners, + textSecondary, +}) => { + return ( +
    +
    +
    +

    {mainText}

    +

    {subTitle}

    + +
    + + +
    +
    + ); +}; + +export default ImageRight; diff --git a/frontend/src/components/WebPageComponents/AboutUsComponent/index.tsx b/frontend/src/components/WebPageComponents/AboutUsComponent/index.tsx new file mode 100644 index 0000000..1c19eb0 --- /dev/null +++ b/frontend/src/components/WebPageComponents/AboutUsComponent/index.tsx @@ -0,0 +1,85 @@ +import React, { useEffect, useState } from 'react'; +import { getMultiplePexelsImages } from '../../../helpers/pexels'; +import { useAppSelector } from '../../../stores/hooks'; +import { AboutUsDesigns } from '../designs'; +import ImageLeft from './designs/ImageLeft'; +import ImageRight from './designs/ImageRight'; + +const AboutUsComponent = ({ + projectName, + image, + mainText, + subTitle, + design, + buttonText, +}) => { + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const corners = useAppSelector((state) => state.style.corners); + const [imageAbout, setImages] = useState([]); + const pexelsQueriesWebSite = image; + + useEffect(() => { + const fetchImages = async () => { + try { + const images = await getMultiplePexelsImages(pexelsQueriesWebSite); + const formattedImages = images.map((image) => ({ + src: image.src || undefined, + photographer: image.photographer || undefined, + photographer_url: image.photographer_url || undefined, + })); + setImages(formattedImages); + } catch (error) { + console.error('Error fetching images:', error); + } + }; + + fetchImages(); + }, [pexelsQueriesWebSite]); + + const renderComponent = () => { + switch (design) { + case AboutUsDesigns.IMAGE_LEFT: + return ( + + ); + + case AboutUsDesigns.IMAGE_RIGHT: + return ( + + ); + + default: + return ( + + ); + } + }; + + return renderComponent(); +}; + +export default AboutUsComponent; diff --git a/frontend/src/components/WebPageComponents/ContactFormComponent/designs/FormWithImage.tsx b/frontend/src/components/WebPageComponents/ContactFormComponent/designs/FormWithImage.tsx new file mode 100644 index 0000000..1a94a31 --- /dev/null +++ b/frontend/src/components/WebPageComponents/ContactFormComponent/designs/FormWithImage.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { Formik, Form, Field } from 'formik'; +import BaseButton from '../../../BaseButton'; +import FormField from '../../../../components/FormField'; +import { useAppSelector, useAppDispatch } from '../../../../stores/hooks'; + +const FormWithImage = ({ mainText, subTitle, onSubmit, imageContactForm }) => { + const corners = useAppSelector((state) => state.style.corners); + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + return ( +
    +
    +
    + + +
    +
    +

    {mainText}

    +

    {subTitle}

    + + + {({ isSubmitting }) => ( +
    + + + + + + + + + + + + + + + )} +
    +
    +
    +
    +
    +
    + ); +}; +export default FormWithImage; diff --git a/frontend/src/components/WebPageComponents/ContactFormComponent/designs/HighlightedForm.tsx b/frontend/src/components/WebPageComponents/ContactFormComponent/designs/HighlightedForm.tsx new file mode 100644 index 0000000..a47a427 --- /dev/null +++ b/frontend/src/components/WebPageComponents/ContactFormComponent/designs/HighlightedForm.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { Formik, Form, Field } from 'formik'; +import BaseButton from '../../../BaseButton'; +import BaseIcon from '../../../BaseIcon'; +import * as icon from '@mdi/js'; +import { ContactFormDesigns } from '../../designs'; +import FormField from '../../../../components/FormField'; +import { useAppSelector, useAppDispatch } from '../../../../stores/hooks'; +const HighlightedForm = ({ mainText, subTitle, onSubmit, design }) => { + const corners = useAppSelector((state) => state.style.corners); + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + + return ( +
    +
    +
    +
    +

    {mainText}

    +

    {subTitle}

    +
    +

    + + +1XXX XXXX XXX +

    +

    + + + krystsinavaida@gmail.com + +

    +
    +
    +
    + + {({ isSubmitting }) => ( +
    + + + + + + + + + + + + + + + )} +
    +
    +
    +
    +
    + ); +}; + +export default HighlightedForm; diff --git a/frontend/src/components/WebPageComponents/ContactFormComponent/designs/SimpleAndCleanForm.tsx b/frontend/src/components/WebPageComponents/ContactFormComponent/designs/SimpleAndCleanForm.tsx new file mode 100644 index 0000000..52778e5 --- /dev/null +++ b/frontend/src/components/WebPageComponents/ContactFormComponent/designs/SimpleAndCleanForm.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { Formik, Form, Field } from 'formik'; +import BaseButton from '../../../BaseButton'; +import FormField from '../../../../components/FormField'; +import BaseIcon from '../../../BaseIcon'; +import * as icon from '@mdi/js'; +import { ContactFormDesigns } from '../../designs'; +import { useAppSelector, useAppDispatch } from '../../../../stores/hooks'; +const SimpleAndCleanForm = ({ mainText, subTitle, onSubmit, design }) => { + const corners = useAppSelector((state) => state.style.corners); + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + return ( +
    +
    +
    +
    +

    {mainText}

    +

    + {subTitle} +

    +
    +

    + + +1XXX XXXX XXX +

    +

    + + seatrend84@gmail.com +

    +
    +
    +
    + + {({ isSubmitting }) => ( +
    + + + + + + + + + + + + + + + )} +
    +
    +
    +
    +
    + ); +}; + +export default SimpleAndCleanForm; diff --git a/frontend/src/components/WebPageComponents/ContactFormComponent/index.tsx b/frontend/src/components/WebPageComponents/ContactFormComponent/index.tsx new file mode 100644 index 0000000..65210af --- /dev/null +++ b/frontend/src/components/WebPageComponents/ContactFormComponent/index.tsx @@ -0,0 +1,89 @@ +// src/components/WebPageComponents/ContactFormSection.tsx +import React, { useEffect, useState } from 'react'; +import { getMultiplePexelsImages } from '../../../helpers/pexels'; +import { useAppSelector } from '../../../stores/hooks'; +import { ContactFormDesigns } from '../designs'; +import SimpleAndCleanForm from './designs/SimpleAndCleanForm'; +import HighlightedForm from './designs/HighlightedForm'; +import FormWithImage from './designs/FormWithImage'; +import { ToastContainer, toast } from 'react-toastify'; +import axios from 'axios'; +import 'react-toastify/dist/ReactToastify.css'; + +export default function ContactFormSection({ + projectName, + withBg = 0, + mainText, + subTitle, + design, + image, +}) { + const [imageContactForm, setImages] = useState([]); + const pexelsQueriesWebSite = image; + const textSecondary = useAppSelector((state) => state.style.textSecondary); + + useEffect(() => { + const fetchImages = async () => { + if (design === ContactFormDesigns.WITH_IMAGE) { + try { + const images = await getMultiplePexelsImages(pexelsQueriesWebSite); + const formattedImages = images.map((image) => ({ + src: image.src || undefined, + photographer: image.photographer || undefined, + photographer_url: image.photographer_url || undefined, + })); + setImages(formattedImages); + } catch (error) { + console.error('Error fetching images:', error); + } + } + }; + + fetchImages(); + }, [pexelsQueriesWebSite, design]); + + const handleSubmit = async (values, { setSubmitting, resetForm }) => { + try { + await axios.post('/contact-form/send', values); + toast.success('Your message has been sent successfully!'); + resetForm(); + } catch (error) { + toast.error('There was an error sending your message'); + } finally { + setSubmitting(false); + } + }; + + let DesignComponent; + + switch (design) { + case ContactFormDesigns.SIMPLE_CLEAN: + case ContactFormDesigns.SIMPLE_CLEAN_DIVERSITY: + DesignComponent = SimpleAndCleanForm; + break; + case ContactFormDesigns.HIGHLIGHTED: + case ContactFormDesigns.HIGHLIGHTED_DIVERSITY: + DesignComponent = HighlightedForm; + break; + case ContactFormDesigns.WITH_IMAGE: + DesignComponent = FormWithImage; + break; + default: + DesignComponent = SimpleAndCleanForm; + break; + } + + return ( +
    + + +
    + ); +} diff --git a/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQAccordion.tsx b/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQAccordion.tsx new file mode 100644 index 0000000..04a4aa5 --- /dev/null +++ b/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQAccordion.tsx @@ -0,0 +1,74 @@ +// components/FAQ/FAQAccordion.js +import React, { useState } from 'react'; + +const FAQAccordion = ({ + faqs, + projectName, + textSecondary, + corners, + borders, + mainText, +}) => { + const [openIndex, setOpenIndex] = useState(null); + + const toggleFAQ = (index) => { + setOpenIndex(openIndex === index ? null : index); + }; + + return ( +
    +
    +
    +
    +

    + {mainText} +

    +
    + +
    + {faqs.map((faq, index) => ( +
    +
    toggleFAQ(index)} + > +

    + {faq.question.replace(/\${projectName}/g, projectName)} +

    + + {openIndex === index ? '−' : '+'} + +
    + +
    +
    +

    {faq.answer.replace(/\${projectName}/g, projectName)}

    +
    +
    +
    + ))} +
    +
    +
    +
    + ); +}; + +export default FAQAccordion; diff --git a/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQSplitList.tsx b/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQSplitList.tsx new file mode 100644 index 0000000..5c07f77 --- /dev/null +++ b/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQSplitList.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { FaqDesigns } from '../../designs'; + +const FAQSplitList = ({ + faqs, + projectName, + textSecondary, + borders, + mainText, + websiteSectionStyle, + design, +}) => ( +
    +
    +
    +
    +

    + {mainText} +

    +
    + +
    + {faqs.map((faq, index) => ( +
    +
    +

    + {faq.question.replace(/\${projectName}/g, projectName)} +

    +
    + +
    +

    + {faq.answer.replace(/\${projectName}/g, projectName)} +

    +
    +
    + ))} +
    +
    +
    +
    +); + +export default FAQSplitList; diff --git a/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQTwoColumn.tsx b/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQTwoColumn.tsx new file mode 100644 index 0000000..cdd2917 --- /dev/null +++ b/frontend/src/components/WebPageComponents/FaqComponent/designs/FAQTwoColumn.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +const FAQTwoColumn = ({ faqs, projectName, textSecondary, mainText }) => ( +
    +
    +
    +
    +

    FAQ

    +

    {mainText}

    +
    +
    + {faqs.map((faq, index) => ( +
    +

    + {faq.question.replace(/\${projectName}/g, projectName)} +

    +

    + {faq.answer.replace(/\${projectName}/g, projectName)} +

    +
    + ))} +
    +
    +
    +
    +); + +export default FAQTwoColumn; diff --git a/frontend/src/components/WebPageComponents/FaqComponent/index.tsx b/frontend/src/components/WebPageComponents/FaqComponent/index.tsx new file mode 100644 index 0000000..edc77fe --- /dev/null +++ b/frontend/src/components/WebPageComponents/FaqComponent/index.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { useAppSelector } from '../../../stores/hooks'; +import FAQAccordion from './designs/FAQAccordion'; +import FAQSplitList from './designs/FAQSplitList'; +import FAQTwoColumn from './designs/FAQTwoColumn'; +import { FaqDesigns } from '../designs'; + +const FaqSection = ({ projectName, mainText, faqs, design }) => { + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const borders = useAppSelector((state) => state.style.borders); + const corners = useAppSelector((state) => state.style.corners); + const websiteSectionStyle = useAppSelector( + (state) => state.style.websiteSectionStyle, + ); + + let designComponent; + + switch (design) { + case FaqDesigns.ACCORDION: + designComponent = ( + + ); + break; + + case FaqDesigns.SPLIT_LIST: + case FaqDesigns.SPLIT_LIST_DIVERSITY: + designComponent = ( + + ); + break; + + case FaqDesigns.TWO_COLUMN: + designComponent = ( + + ); + break; + + default: + designComponent = ( + + ); + break; + } + + return
    {designComponent}
    ; +}; + +export default FaqSection; diff --git a/frontend/src/components/WebPageComponents/FeaturesComponent/designs/CardsGridWithIcons.tsx b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/CardsGridWithIcons.tsx new file mode 100644 index 0000000..463b5e3 --- /dev/null +++ b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/CardsGridWithIcons.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import BaseIcon from '../../../BaseIcon'; +import * as icon from '@mdi/js'; +import { FeaturesDesigns } from '../../designs'; + +const CardsGridWithIcons = ({ + features, + projectName, + design, + iconsColor, + corners, + mainText, + subTitle, + websiteSectionStyle, + textSecondary, + borders, + shadow, +}) => ( +
    +
    +
    +

    {mainText}

    +

    {subTitle}

    +
    + +
    + {features.map((feature: any, index) => ( +
    +
    +

    + {feature.name.replace(/\${projectName}/g, projectName)} +

    +

    + {feature.description.replace(/\${projectName}/g, projectName)} +

    +
    +
    + +
    +
    + ))} +
    +
    +
    +); + +export default CardsGridWithIcons; diff --git a/frontend/src/components/WebPageComponents/FeaturesComponent/designs/IconsTop.tsx b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/IconsTop.tsx new file mode 100644 index 0000000..37c6cb1 --- /dev/null +++ b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/IconsTop.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import BaseIcon from '../../../BaseIcon'; +import * as icon from '@mdi/js'; + +const IconsTop = ({ + features, + projectName, + withBg, + iconsColor, + mainText, + subTitle, + textSecondary, + websiteSectionStyle, +}) => ( +
    +
    +
    +

    {mainText}

    +

    {subTitle}

    +
    + +
    + {features.map((feature: any, index) => ( +
    +
    + +
    +
    +

    + {feature.name.replace(/\${projectName}/g, projectName)} +

    +

    + {feature.description.replace(/\${projectName}/g, projectName)} +

    +
    +
    + ))} +
    +
    +
    +); + +export default IconsTop; diff --git a/frontend/src/components/WebPageComponents/FeaturesComponent/designs/IconsWithImage.tsx b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/IconsWithImage.tsx new file mode 100644 index 0000000..cd0bb88 --- /dev/null +++ b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/IconsWithImage.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import BaseIcon from '../../../BaseIcon'; +import * as icon from '@mdi/js'; + +const IconsWithImage = ({ + features, + projectName, + withBg, + iconsColor, + mainText, + subTitle, + image, + textSecondary, + websiteSectionStyle, + corners, +}) => { + const displayedFeatures = features.slice(0, 4); + + return ( +
    +
    +
    + + +
    +
    + {displayedFeatures.map((feature, index) => ( +
    +
    + +
    + + {/* Текст */} +
    +

    + {feature.name.replace(/\${projectName}/g, projectName)} +

    +

    + {feature.description.replace( + /\${projectName}/g, + projectName, + )} +

    +
    +
    + ))} +
    +
    +
    +
    +
    + ); +}; + +export default IconsWithImage; diff --git a/frontend/src/components/WebPageComponents/FeaturesComponent/designs/LargeNumbers.tsx b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/LargeNumbers.tsx new file mode 100644 index 0000000..88358f2 --- /dev/null +++ b/frontend/src/components/WebPageComponents/FeaturesComponent/designs/LargeNumbers.tsx @@ -0,0 +1,52 @@ +import React from 'react'; + +const LargeNumbers = ({ + features, + projectName, + withBg, + iconsColor, + mainText, + subTitle, + textSecondary, + websiteSectionStyle, +}) => ( +
    +
    +
    +

    {mainText}

    +

    {subTitle}

    +
    + +
    + {features.map((feature: any, index) => ( +
    +
    +
    + + {index + 1} + +
    +
    +
    +

    + {feature.name.replace(/\${projectName}/g, projectName)} +

    +

    + {feature.description.replace(/\${projectName}/g, projectName)} +

    +
    +
    + ))} +
    +
    +
    +); + +export default LargeNumbers; diff --git a/frontend/src/components/WebPageComponents/FeaturesComponent/index.tsx b/frontend/src/components/WebPageComponents/FeaturesComponent/index.tsx new file mode 100644 index 0000000..7eb2916 --- /dev/null +++ b/frontend/src/components/WebPageComponents/FeaturesComponent/index.tsx @@ -0,0 +1,136 @@ +import React, { useEffect, useState } from 'react'; +import { getMultiplePexelsImages } from '../../../helpers/pexels'; +import { useAppSelector } from '../../../stores/hooks'; +import IconsTop from './designs/IconsTop'; +import LargeNumbers from './designs/LargeNumbers'; +import CardsGridWithIcons from './designs/CardsGridWithIcons'; +import { FeaturesDesigns } from '../designs'; +import IconsWithImage from './designs/IconsWithImage'; + +export default function FeaturesSection({ + projectName, + withBg = 0, + features, + mainText, + subTitle, + design, + image, +}) { + const textColor = useAppSelector((state) => state.style.linkColor); + const iconsColor = useAppSelector((state) => state.style.iconsColor); + const corners = useAppSelector((state) => state.style.corners); + const shadow = useAppSelector((state) => state.style.shadow); + const websiteSectionStyle = useAppSelector( + (state) => state.style.websiteSectionStyle, + ); + const borders = useAppSelector((state) => state.style.borders); + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const pexelsQueriesWebSite = image; + const [imageFeatures, setImages] = useState([]); + useEffect(() => { + const fetchImages = async () => { + if (design === FeaturesDesigns.ICONS_WITH_IMAGE) { + try { + const images = await getMultiplePexelsImages(pexelsQueriesWebSite); + const formattedImages = images.map((image) => ({ + src: image.src || undefined, + photographer: image.photographer || undefined, + photographer_url: image.photographer_url || undefined, + })); + setImages(formattedImages); + } catch (error) { + console.error('Error fetching images:', error); + } + } + }; + + fetchImages(); + }, [pexelsQueriesWebSite, design]); + + let designComponent; + + switch (design) { + case FeaturesDesigns.ICONS_TOP: + designComponent = ( + + ); + break; + + case FeaturesDesigns.LARGE_NUMBERS: + designComponent = ( + + ); + break; + + case FeaturesDesigns.CARDS_GRID_WITH_ICONS: + case FeaturesDesigns.CARDS_GRID_WITH_ICONS_DIVERSITY: + designComponent = ( + + ); + break; + + case FeaturesDesigns.ICONS_WITH_IMAGE: + designComponent = ( + + ); + break; + + default: + designComponent = ( + + ); + break; + } + + return
    {designComponent}
    ; +} diff --git a/frontend/src/components/WebPageComponents/Footer.tsx b/frontend/src/components/WebPageComponents/Footer.tsx new file mode 100644 index 0000000..78d5af7 --- /dev/null +++ b/frontend/src/components/WebPageComponents/Footer.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { FooterStyle } from './designs'; +import { FooterDesigns } from './designs'; +import Link from 'next/link'; +import { humanize } from '../../helpers/humanize'; +import { useRouter } from 'next/router'; +import { useAppSelector } from '../../stores/hooks'; +interface WebSiteFooterProps { + projectName: string; + pages: any; +} +export default function WebSiteFooter({ + projectName, + pages, +}: WebSiteFooterProps) { + const currentYear = new Date().getFullYear(); + const router = useRouter(); + const borders = useAppSelector((state) => state.style.borders); + const websiteHeder = useAppSelector((state) => state.style.websiteHeder); + + const style = FooterStyle.WITH_PAGES; + + const design = FooterDesigns.DESIGN_DIVERSITY; + + return ( +
    +
    +
    +

    + © {currentYear} Flatlogic. All rights reserved +

    + {style ? ( +

    {projectName}

    + ) : ( +
    + {pages.map((page, index) => { + const isRootRoute = router.pathname === '/'; + const isActive = isRootRoute + ? index === 0 + : router.pathname.includes(page.href); + + return ( + + {humanize(page.label)} + + ); + })} +
    + )} +
    + + Terms of Use + + + Privacy Policy + +
    +
    +
    +
    + ); +} diff --git a/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/designs/HorizontalGalleryWithButtons.tsx b/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/designs/HorizontalGalleryWithButtons.tsx new file mode 100644 index 0000000..732041c --- /dev/null +++ b/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/designs/HorizontalGalleryWithButtons.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import BaseButton from '../../../BaseButton'; + +const HorizontalGallery = ({ + images, + currentIndex, + prevSlide, + nextSlide, + getPrevIndex, + getNextIndex, + mainText, + corners, +}) => { + return ( + + ); +}; + +export default HorizontalGallery; diff --git a/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/designs/OverlappingCentralImage.tsx b/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/designs/OverlappingCentralImage.tsx new file mode 100644 index 0000000..bb8926b --- /dev/null +++ b/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/designs/OverlappingCentralImage.tsx @@ -0,0 +1,128 @@ +import React from 'react'; + +const OverlappingGallery = ({ + images, + currentIndex, + prevSlide, + nextSlide, + getPrevIndex, + getNextIndex, + mainText, + corners, +}) => { + const applyLeftCorner = (corners) => { + if (corners === 'rounded-full') { + return 'rounded-l-2xl'; + } + if (corners.startsWith('rounded-lg')) { + return 'rounded-l-lg'; + } + if (corners.startsWith('rounded-md')) { + return 'rounded-l-md'; + } + if (corners.startsWith('rounded-sm')) { + return 'rounded-l-sm'; + } + if (corners.startsWith('rounded-xl')) { + return 'rounded-l-xl'; + } + if (corners.startsWith('rounded')) { + return 'rounded-l'; + } + return corners; + }; + + const applyRightCorner = (corners) => { + if (corners === 'rounded-full') { + return 'rounded-r-2xl'; + } + if (corners.startsWith('rounded')) { + return corners.replace('rounded', 'rounded-r'); + } + return corners.replace('rounded-', 'rounded-r-'); + }; + const buttonCornersLeft = applyLeftCorner(corners); + const buttonCornersRight = applyRightCorner(corners); + return ( + + ); +}; + +export default OverlappingGallery; diff --git a/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/index.tsx b/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/index.tsx new file mode 100644 index 0000000..bcd7856 --- /dev/null +++ b/frontend/src/components/WebPageComponents/GalleryPortfolioComponent/index.tsx @@ -0,0 +1,75 @@ +import React, { useState } from 'react'; +import HorizontalGallery from './designs/HorizontalGalleryWithButtons'; +import OverlappingGallery from './designs/OverlappingCentralImage'; +import { GalleryPortfolioDesigns } from '../designs'; +import { useAppSelector } from '../../../stores/hooks'; + +export default function GalleryPortfolioSection({ + projectName, + images, + mainText, + design, +}) { + const [currentIndex, setCurrentIndex] = useState(0); + const corners = useAppSelector((state) => state.style.corners); + const prevSlide = () => { + setCurrentIndex((prevIndex) => + prevIndex === 0 ? images.length - 1 : prevIndex - 1, + ); + }; + + const nextSlide = () => { + setCurrentIndex((prevIndex) => + prevIndex === images.length - 1 ? 0 : prevIndex + 1, + ); + }; + + const getPrevIndex = () => + currentIndex === 0 ? images.length - 1 : currentIndex - 1; + const getNextIndex = () => + currentIndex === images.length - 1 ? 0 : currentIndex + 1; + + switch (design) { + case GalleryPortfolioDesigns.HORIZONTAL_WITH_BUTTONS: + return ( + + ); + + case GalleryPortfolioDesigns.OVERLAPPING_CENTRAL_IMAGE: + return ( + + ); + + default: + return ( + + ); + } +} diff --git a/frontend/src/components/WebPageComponents/Header.tsx b/frontend/src/components/WebPageComponents/Header.tsx new file mode 100644 index 0000000..1c882e7 --- /dev/null +++ b/frontend/src/components/WebPageComponents/Header.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import Link from 'next/link'; +import BaseButton from '../BaseButton'; +import { humanize } from '../../helpers/humanize'; +import { useRouter } from 'next/router'; +import { useAppSelector } from '../../stores/hooks'; +import { HeaderStyle } from './designs'; +import { HeaderDesigns } from './designs'; + +interface WebSiteHeaderProps { + projectName: string; + pages: any; +} +export default function WebSiteHeader({ + projectName, + pages, +}: WebSiteHeaderProps) { + const router = useRouter(); + const websiteHeder = useAppSelector((state) => state.style.websiteHeder); + const borders = useAppSelector((state) => state.style.borders); + + const style = HeaderStyle.PAGES_RIGHT; + + const design = HeaderDesigns.DEFAULT_DESIGN; + return ( +
    +
    +
    +
    +
    +
    + {projectName} +
    +
    + {pages.map((page, index) => { + const isRootRoute = router.pathname === '/'; + const isActive = isRootRoute + ? index === 0 + : router.pathname.includes(page.href); + + return ( + + {humanize(page.label)} + + ); + })} +
    +
    +
    + +
    +
    +
    +
    +
    + ); +} diff --git a/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageBg.tsx b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageBg.tsx new file mode 100644 index 0000000..53e43ef --- /dev/null +++ b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageBg.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import BaseButton from '../../../BaseButton'; + +const HeroImageBg = ({ + mainText, + subTitle, + buttonText, + imageHero, + textSecondary, +}) => ( +
    +
    +
    +

    + {mainText} +

    +

    {subTitle}

    + +
    + +
    +); + +export default HeroImageBg; diff --git a/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageLeft.tsx b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageLeft.tsx new file mode 100644 index 0000000..173e82d --- /dev/null +++ b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageLeft.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import BaseButton from '../../../BaseButton'; + +const HeroImageLeft = ({ + mainText, + subTitle, + buttonText, + imageHero, + textSecondary, +}) => ( +
    +
    +
    +

    + {mainText} +

    +
    +

    {subTitle}

    +
    + +
    +
    + +
    +); + +export default HeroImageLeft; diff --git a/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageRight.tsx b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageRight.tsx new file mode 100644 index 0000000..95184bb --- /dev/null +++ b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroImageRight.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import BaseButton from '../../../BaseButton'; + +const HeroImageRight = ({ + mainText, + subTitle, + buttonText, + imageHero, + textSecondary, +}) => ( +
    + +
    +
    +

    + {mainText} +

    +
    +

    {subTitle}

    +
    + +
    +
    +
    +); + +export default HeroImageRight; diff --git a/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroTextCenter.tsx b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroTextCenter.tsx new file mode 100644 index 0000000..cbbf691 --- /dev/null +++ b/frontend/src/components/WebPageComponents/HeroComponent/designs/HeroTextCenter.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import BaseButton from '../../../BaseButton'; + +const HeroTextCenter = ({ mainText, subTitle, buttonText, textSecondary }) => ( +
    +
    +
    +

    + {mainText} +

    +

    {subTitle}

    + +
    +
    +); + +export default HeroTextCenter; diff --git a/frontend/src/components/WebPageComponents/HeroComponent/index.tsx b/frontend/src/components/WebPageComponents/HeroComponent/index.tsx new file mode 100644 index 0000000..7b178ce --- /dev/null +++ b/frontend/src/components/WebPageComponents/HeroComponent/index.tsx @@ -0,0 +1,73 @@ +// src/components/WebPageComponents/HeroSection.tsx +import React, { useEffect, useState } from 'react'; +import { getMultiplePexelsImages } from '../../../helpers/pexels'; +import { useAppSelector } from '../../../stores/hooks'; +import { HeroDesigns } from '../designs'; +import HeroImageLeft from './designs/HeroImageLeft'; +import HeroImageRight from './designs/HeroImageRight'; +import HeroImageBg from './designs/HeroImageBg'; +import HeroTextCenter from './designs/HeroTextCenter'; + +export default function HeroSection({ + projectName, + image, + mainText, + subTitle, + design, + buttonText, +}) { + const textSecondary = useAppSelector((state) => state.style.textSecondary); + + const [imageHero, setImages] = useState([]); + const pexelsQueriesWebSite = image; + + useEffect(() => { + const fetchImages = async () => { + if (design !== HeroDesigns.TEXT_CENTER) { + try { + const images = await getMultiplePexelsImages(pexelsQueriesWebSite); + const formattedImages = images.map((image) => ({ + src: image.src || undefined, + photographer: image.photographer || undefined, + photographer_url: image.photographer_url || undefined, + })); + setImages(formattedImages); + } catch (error) { + console.error('Error fetching images:', error); + } + } + }; + + fetchImages(); + }, [pexelsQueriesWebSite, design]); + + let DesignComponent; + + switch (design) { + case HeroDesigns.IMAGE_LEFT: + DesignComponent = HeroImageLeft; + break; + case HeroDesigns.IMAGE_RIGHT: + DesignComponent = HeroImageRight; + break; + case HeroDesigns.IMAGE_BG: + DesignComponent = HeroImageBg; + break; + case HeroDesigns.TEXT_CENTER: + DesignComponent = HeroTextCenter; + break; + default: + DesignComponent = HeroImageRight; + break; + } + + return ( + + ); +} diff --git a/frontend/src/components/WebPageComponents/PricingComponent/index.tsx b/frontend/src/components/WebPageComponents/PricingComponent/index.tsx new file mode 100644 index 0000000..e0973d6 --- /dev/null +++ b/frontend/src/components/WebPageComponents/PricingComponent/index.tsx @@ -0,0 +1,240 @@ +import React, { useEffect, useState } from 'react'; +import Link from 'next/link'; +import { useAppSelector, useAppDispatch } from '../../../stores/hooks'; +import BaseButton from '../../BaseButton'; +import BaseIcon from '../../BaseIcon'; +import * as icon from '@mdi/js'; + +export default function PricingSection({ + projectName, + withBg = 0, + features, + description, +}) { + const textColor = useAppSelector((state) => state.style.linkColor); + const iconsColor = useAppSelector((state) => state.style.iconsColor); + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + const borders = useAppSelector((state) => state.style.borders); + const shadow = useAppSelector((state) => state.style.shadow); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const websiteSectionStyle = useAppSelector( + (state) => state.style.websiteSectionStyle, + ); + const navBarItemLabelActiveColorStyle = useAppSelector( + (state) => state.style.navBarItemLabelActiveColorStyle, + ); + const asideMenuItemActiveStyle = useAppSelector( + (state) => state.style.asideMenuItemActiveStyle, + ); + const corners = useAppSelector((state) => state.style.corners); + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const activeLinkColor = useAppSelector( + (state) => state.style.activeLinkColor, + ); + + return ( +
    +
    +
    +

    + Choose the plan that’s right for you +

    +
    Pricing Table
    +
    +
    +
    +
    +

    Standard

    +

    + {description['standard'] + ? description['standard'] + : 'For solo designer'} +

    +
    +
    +
    + +

    29

    +
    +

    + per person, per month +

    +
    + +
      + {features.standard.features.map((feature, index) => ( +
    • + + {feature} +
    • + ))} +
    +

    Limited to

    +
      + {features.standard.limited_features.map((feature, index) => ( +
    • + + {feature} +
    • + ))} +
    +
    + +
    + +
    +
    +

    + Premium{' '} + + MOST POPULAR + +

    +

    + {description['premium'] + ? description['premium'] + : ' For small startup and agency'} +

    +
    +
    +
    + +

    49

    +
    +

    + per person, per month +

    +
    + +
      + {features.premium.features.map((feature, index) => ( +
    • + + {feature} +
    • + ))} +
    +

    Also Include

    + +
      + {features.premium.also_included.map((feature, index) => ( +
    • + + {feature} +
    • + ))} +
    +
    + +
    +
    +
    +

    + Business +

    +

    + {description['business'] + ? description['business'] + : ' Custom solution'} +

    +
    +
    +
    + +

    99

    +
    +

    + per person, per month +

    +
    + +
      + {features.business.features.map((feature, index) => ( +
    • + + {feature} +
    • + ))} +
    +
    + +
    +
    +
    +
    + ); +} diff --git a/frontend/src/components/WebPageComponents/TestimonialsComponent/designs/HorizontalCarousel.tsx b/frontend/src/components/WebPageComponents/TestimonialsComponent/designs/HorizontalCarousel.tsx new file mode 100644 index 0000000..0812c65 --- /dev/null +++ b/frontend/src/components/WebPageComponents/TestimonialsComponent/designs/HorizontalCarousel.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import BaseButton from '../../../BaseButton'; +import { TestimonialsDesigns } from '../../designs'; + +const HorizontalCarousel = ({ + projectName, + testimonials, + currentIndex, + handlePrev, + handleNext, + design, + websiteSectionStyle, + textSecondary, + iconsColor, +}) => { + return ( +
    +
    + + +
    +

    + “ +

    +

    + {testimonials[currentIndex].text.replace( + /\${projectName}/g, + projectName, + )} +

    +

    + {testimonials[currentIndex].company} +

    +

    + - {testimonials[currentIndex].user_name} +

    +
    + + +
    +
    + ); +}; + +export default HorizontalCarousel; diff --git a/frontend/src/components/WebPageComponents/TestimonialsComponent/designs/MultiCardDisplay.tsx b/frontend/src/components/WebPageComponents/TestimonialsComponent/designs/MultiCardDisplay.tsx new file mode 100644 index 0000000..1088171 --- /dev/null +++ b/frontend/src/components/WebPageComponents/TestimonialsComponent/designs/MultiCardDisplay.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import BaseButton from '../../../BaseButton'; + +const MultiCardDisplay = ({ + projectName, + testimonials, + getVisibleTestimonials, + handlePrev, + handleNext, + textSecondary, + corners, + mainText, + shadow, +}) => { + return ( +
    +
    +

    + {mainText} +

    +
    + + +
    +
    + +
    + {getVisibleTestimonials().map((testimonial, index) => ( +
    +
    +

    + Efficient Collaborating +

    +

    + {testimonial.text.replace(/\${projectName}/g, projectName)} +

    +
    +
    +

    + {testimonial.company} +

    +

    + - {testimonial.user_name} +

    +
    +
    + ))} +
    +
    + ); +}; + +export default MultiCardDisplay; diff --git a/frontend/src/components/WebPageComponents/TestimonialsComponent/index.tsx b/frontend/src/components/WebPageComponents/TestimonialsComponent/index.tsx new file mode 100644 index 0000000..822cefb --- /dev/null +++ b/frontend/src/components/WebPageComponents/TestimonialsComponent/index.tsx @@ -0,0 +1,112 @@ +// TestimonialsSection.js +import React, { useEffect, useState } from 'react'; +import HorizontalCarousel from './designs/HorizontalCarousel'; +import MultiCardDisplay from './designs/MultiCardDisplay'; +import { useAppSelector } from '../../../stores/hooks'; +import { TestimonialsDesigns } from '../designs'; + +export default function TestimonialsSection({ + projectName, + mainText, + testimonials, + design, +}) { + const textSecondary = useAppSelector((state) => state.style.textSecondary); + const corners = useAppSelector((state) => state.style.corners); + const websiteSectionStyle = useAppSelector( + (state) => state.style.websiteSectionStyle, + ); + const iconsColor = useAppSelector((state) => state.style.iconsColor); + const shadow = useAppSelector((state) => state.style.shadow); + const [currentIndex, setCurrentIndex] = useState(0); + + const handlePrev = () => { + setCurrentIndex((prevIndex) => + prevIndex === 0 ? testimonials.length - 1 : prevIndex - 1, + ); + }; + + const handleNext = () => { + setCurrentIndex((prevIndex) => + prevIndex === testimonials.length - 1 ? 0 : prevIndex + 1, + ); + }; + + const getVisibleTestimonials = () => { + const visibleTestimonials = []; + for (let i = 0; i < 3; i++) { + visibleTestimonials.push( + testimonials[(currentIndex + i) % testimonials.length], + ); + } + return visibleTestimonials; + }; + + const renderDesign = () => { + switch (design) { + case TestimonialsDesigns.HORIZONTAL_CAROUSEL: + case TestimonialsDesigns.HORIZONTAL_CAROUSEL_WITH_BG: + case TestimonialsDesigns.HORIZONTAL_CAROUSEL_DIVERSITY: + return ( + + ); + + case TestimonialsDesigns.MULTI_CARD_DISPLAY: + return ( + + ); + + default: + return ( + + ); + } + }; + + return ( +
    + {renderDesign()} +
    + ); +} diff --git a/frontend/src/components/WebPageComponents/designs.ts b/frontend/src/components/WebPageComponents/designs.ts new file mode 100644 index 0000000..3b3e061 --- /dev/null +++ b/frontend/src/components/WebPageComponents/designs.ts @@ -0,0 +1,77 @@ +export enum GalleryPortfolioDesigns { + DEFAULT_DESIGN, + HORIZONTAL_WITH_BUTTONS, + OVERLAPPING_CENTRAL_IMAGE, +} + +export enum HeroDesigns { + DEFAULT_DESIGN, + IMAGE_LEFT, + IMAGE_RIGHT, + IMAGE_BG, + TEXT_CENTER, +} + +export enum ContactFormDesigns { + DEFAULT_DESIGN, + SIMPLE_CLEAN, + SIMPLE_CLEAN_DIVERSITY, + HIGHLIGHTED, + HIGHLIGHTED_DIVERSITY, + WITH_IMAGE, +} + +export enum FaqDesigns { + DEFAULT_DESIGN, + ACCORDION, + SPLIT_LIST, + SPLIT_LIST_DIVERSITY, + TWO_COLUMN, +} + +export enum FeaturesDesigns { + DEFAULT_DESIGN, + CARDS_GRID_WITH_ICONS, + CARDS_GRID_WITH_ICONS_DIVERSITY, + ICONS_TOP, + LARGE_NUMBERS, + ICONS_WITH_IMAGE, +} + +export enum TestimonialsDesigns { + DEFAULT_DESIGN, + HORIZONTAL_CAROUSEL, + HORIZONTAL_CAROUSEL_WITH_BG, + HORIZONTAL_CAROUSEL_DIVERSITY, + MULTI_CARD_DISPLAY, +} + +export enum AboutUsDesigns { + DEFAULT_DESIGN, + IMAGE_LEFT, + IMAGE_RIGHT, +} + +export enum PricingDesigns { + DEFAULT_DESIGN, +} + +export enum HeaderStyle { + PAGES_RIGHT, + PAGES_LEFT, +} + +export enum FooterStyle { + WITH_PAGES, + WITH_PROJECT_NAME, +} + +export enum HeaderDesigns { + DESIGN_DIVERSITY, + DEFAULT_DESIGN, +} + +export enum FooterDesigns { + DESIGN_DIVERSITY, + DEFAULT_DESIGN, +} diff --git a/frontend/src/components/WidgetCreator/RoleSelect.tsx b/frontend/src/components/WidgetCreator/RoleSelect.tsx new file mode 100644 index 0000000..c2da317 --- /dev/null +++ b/frontend/src/components/WidgetCreator/RoleSelect.tsx @@ -0,0 +1,65 @@ +import React, { useEffect, useId, useState } from 'react'; +import { AsyncPaginate } from 'react-select-async-paginate'; +import axios from 'axios'; + +export const RoleSelect = ({ + options, + field, + form, + itemRef, + disabled, + currentUser, +}) => { + const [value, setValue] = useState(null); + const PAGE_SIZE = 100; + + React.useEffect(() => { + if (currentUser.app_role.id) { + setValue({ + value: currentUser.app_role.id, + label: currentUser.app_role.name, + }); + } + }, [currentUser]); + + useEffect(() => { + if (options?.value && options?.label) { + setValue({ value: options.value, label: options.label }); + } + }, [options?.id, field?.value?.id]); + + const mapResponseToValuesAndLabels = (data) => ({ + value: data.id, + label: data.label, + }); + const handleChange = (option) => { + form.setFieldValue(field.name, option); + setValue(option); + }; + + async function callApi(inputValue: string, loadedOptions: any[]) { + const path = `/${itemRef}/autocomplete?limit=${PAGE_SIZE}&offset=${ + loadedOptions.length + }${inputValue ? `&query=${inputValue}` : ''}`; + const { data } = await axios(path); + return { + options: data.map(mapResponseToValuesAndLabels), + hasMore: data.length === PAGE_SIZE, + }; + } + return ( + 'px-1 py-2', + }} + classNamePrefix={'react-select'} + instanceId={useId()} + value={value} + debounceTimeout={1000} + loadOptions={callApi} + onChange={handleChange} + defaultOptions + isDisabled={disabled} + /> + ); +}; diff --git a/frontend/src/components/WidgetCreator/WidgetCreator.tsx b/frontend/src/components/WidgetCreator/WidgetCreator.tsx new file mode 100644 index 0000000..e0dcd48 --- /dev/null +++ b/frontend/src/components/WidgetCreator/WidgetCreator.tsx @@ -0,0 +1,149 @@ +import CardBox from '../CardBox'; +import { mdiCog } from '@mdi/js'; +import { Field, Form, Formik } from 'formik'; +import { ToastContainer, toast } from 'react-toastify'; +import FormField from '../FormField'; +import React from 'react'; +import { + aiPrompt, + setErrorNotification, + resetNotify, +} from '../../stores/openAiSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; + +import { fetchWidgets } from '../../stores/roles/rolesSlice'; + +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import { RoleSelect } from './RoleSelect'; + +export const WidgetCreator = ({ + currentUser, + isFetchingQuery, + setWidgetsRole, + widgetsRole, +}) => { + const dispatch = useAppDispatch(); + const [isModalOpen, setIsModalOpen] = React.useState(false); + const { notify: openAiNotify } = useAppSelector((state) => state.openAi); + + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + React.useEffect(() => { + if (openAiNotify.showNotification) { + notify(openAiNotify.typeNotification, openAiNotify.textNotification); + dispatch(resetNotify()); + } + }, [openAiNotify.showNotification]); + + const openModal = (): void => { + setIsModalOpen(true); + }; + + const handleCloseModal = (value = {}) => { + setWidgetsRole(value); + setIsModalOpen(false); + }; + + const getWidgets = async () => { + await dispatch(fetchWidgets(widgetsRole?.role?.value || '')); + }; + + const smartSearch = async ( + values: { description: string }, + resetForm: any, + ) => { + const description = values.description; + const projectId = '29638'; + + const payload = { + roleId: widgetsRole?.role?.value, + description, + projectId, + userId: currentUser?.id, + }; + const { payload: responcePayload, error }: any = await dispatch( + aiPrompt(payload), + ); + + await getWidgets().then(); + + resetForm({ values: { description: '' } }); + if (responcePayload.data?.error || error) { + const errorMessage = + responcePayload.data?.error?.message || error?.message; + await dispatch( + setErrorNotification(errorMessage || 'Error with widget creation'), + ); + } + }; + + return ( + <> + + + smartSearch(values, resetForm)} + > +
    + + + +
    +
    +
    + handleCloseModal(values)} + > + {({ submitForm }) => ( + setIsModalOpen(false)} + > +

    What role are we showing and creating widgets for?

    + +
    + + + +
    +
    + )} +
    + + + ); +}; diff --git a/frontend/src/config.ts b/frontend/src/config.ts new file mode 100644 index 0000000..b45c575 --- /dev/null +++ b/frontend/src/config.ts @@ -0,0 +1,22 @@ +export const hostApi = + process.env.NODE_ENV === 'development' && !process.env.NEXT_PUBLIC_BACK_API + ? 'http://localhost' + : ''; +export const portApi = + process.env.NODE_ENV === 'development' && !process.env.NEXT_PUBLIC_BACK_API + ? 8080 + : ''; +export const baseURLApi = `${hostApi}${portApi ? `:${portApi}` : ``}/api`; + +export const localStorageDarkModeKey = 'darkMode'; + +export const localStorageStyleKey = 'style'; + +export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'; + +export const appTitle = 'created by Flatlogic generator!'; + +export const getPageTitle = (currentPageTitle: string) => + `${currentPageTitle} — ${appTitle}`; + +export const tinyKey = 'cnslp6h943xbg36t2tf2xglmrxiw5b7tatycf3kir7n2j7eh'; diff --git a/frontend/src/css/_app.css b/frontend/src/css/_app.css new file mode 100644 index 0000000..4179b23 --- /dev/null +++ b/frontend/src/css/_app.css @@ -0,0 +1,34 @@ +html { + @apply h-full; +} + +body { + @apply pt-14 xl:pl-60 h-full; +} + +#app { + @apply w-screen transition-position lg:w-auto h-full flex flex-col; +} + +.dropdown { + @apply cursor-pointer; +} + +li.stack-item:not(:last-child):after { + content: '/'; + @apply inline-block pl-2; +} + +.m-clipped, +.m-clipped body { + @apply overflow-hidden lg:overflow-visible; +} + +.full-screen body { + @apply p-0; +} + +.main-navbar, +.app-sidebar-brand { + box-shadow: 0px -1px 40px rgba(112, 144, 176, 0.2); +} diff --git a/frontend/src/css/_calendar.css b/frontend/src/css/_calendar.css new file mode 100644 index 0000000..79f8fe3 --- /dev/null +++ b/frontend/src/css/_calendar.css @@ -0,0 +1,37 @@ +.rbc-event { + @apply bg-blue-600 !important; +} + +.rbc-show-more { + @apply dark:text-white bg-transparent !important; +} + +.rbc-btn-group button { + @apply dark:text-white !important; +} + +.rbc-btn-group button:hover { + @apply text-white dark:bg-dark-700 !important; +} + +.rbc-btn-group button.rbc-active { + @apply text-black dark:bg-blue-600 !important; +} + +.rbc-btn-group button:focus { + @apply dark:bg-blue-600 !important; +} + +.rbc-day-bg.rbc-off-range-bg { + @apply dark:bg-dark-800 !important; +} +.rbc-current-time-indicator { + @apply h-1 !important; +} +.rbc-today { + @apply dark:bg-dark-600/40 !important; +} + +.rbc-day-bg.rbc-selected-cell { + @apply dark:bg-dark-500 !important; +} diff --git a/frontend/src/css/_checkbox-radio-switch.css b/frontend/src/css/_checkbox-radio-switch.css new file mode 100644 index 0000000..f05523e --- /dev/null +++ b/frontend/src/css/_checkbox-radio-switch.css @@ -0,0 +1,73 @@ +@layer components { + .checkbox, + .radio, + .switch { + @apply inline-flex items-center cursor-pointer relative; + } + + .checkbox input[type='checkbox'], + .radio input[type='radio'], + .switch input[type='checkbox'] { + @apply absolute left-0 opacity-0 -z-1; + } + + .checkbox input[type='checkbox'] + .check, + .radio input[type='radio'] + .check, + .switch input[type='checkbox'] + .check { + @apply border-gray-700 border transition-colors duration-200 dark:bg-slate-800; + } + + .checkbox input[type='checkbox']:focus + .check, + .radio input[type='radio']:focus + .check, + .switch input[type='checkbox']:focus + .check { + @apply ring ring-blue-700; + } + + .checkbox input[type='checkbox'] + .check, + .radio input[type='radio'] + .check { + @apply block w-5 h-5; + } + + .checkbox input[type='checkbox'] + .check { + @apply rounded; + } + + .switch input[type='checkbox'] + .check { + @apply flex items-center shrink-0 w-12 h-6 p-0.5 bg-gray-200; + } + + .radio input[type='radio'] + .check, + .switch input[type='checkbox'] + .check, + .switch input[type='checkbox'] + .check:before { + @apply rounded-full; + } + + .checkbox input[type='checkbox']:checked + .check, + .radio input[type='radio']:checked + .check { + @apply bg-no-repeat bg-center border-4; + } + + .checkbox input[type='checkbox']:checked + .check { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E"); + } + + .radio input[type='radio']:checked + .check { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z' /%3E%3C/svg%3E"); + } + + .switch input[type='checkbox']:checked + .check, + .checkbox input[type='checkbox']:checked + .check, + .radio input[type='radio']:checked + .check { + @apply bg-blue-600 border-blue-600; + } + + .switch input[type='checkbox'] + .check:before { + content: ''; + @apply block w-5 h-5 bg-white border border-gray-700; + } + + .switch input[type='checkbox']:checked + .check:before { + transform: translate3d(110%, 0, 0); + @apply border-blue-600; + } +} diff --git a/frontend/src/css/_helper.css b/frontend/src/css/_helper.css new file mode 100644 index 0000000..9425a7c --- /dev/null +++ b/frontend/src/css/_helper.css @@ -0,0 +1,24 @@ +.helper-container { + right: 0; + top: 70px; + transform: translateX(100%); + + .tab { + top: 0; + left: 0; + transform: translateX(-100%); + } + + .tab:hover { + @apply bg-gray-900 cursor-pointer; + } +} + +.helper-container.open { + transform: translateX(0); +} + +.react-datepicker-wrapper, +.react-datepicker-popper { + z-index: 10 !important; +} diff --git a/frontend/src/css/_progress.css b/frontend/src/css/_progress.css new file mode 100644 index 0000000..d419f78 --- /dev/null +++ b/frontend/src/css/_progress.css @@ -0,0 +1,21 @@ +@layer base { + progress { + @apply h-3 rounded-full overflow-hidden; + } + + progress::-webkit-progress-bar { + @apply bg-blue-200; + } + + progress::-webkit-progress-value { + @apply bg-blue-500; + } + + progress::-moz-progress-bar { + @apply bg-blue-500; + } + + progress::-ms-fill { + @apply bg-blue-500 border-0; + } +} diff --git a/frontend/src/css/_rich-text.css b/frontend/src/css/_rich-text.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/css/_scrollbars.css b/frontend/src/css/_scrollbars.css new file mode 100644 index 0000000..11445a9 --- /dev/null +++ b/frontend/src/css/_scrollbars.css @@ -0,0 +1,41 @@ +@layer base { + html { + scrollbar-width: thin; + scrollbar-color: rgb(156, 163, 175) rgb(249, 250, 251); + } + + body::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + body::-webkit-scrollbar-track { + @apply bg-gray-50; + } + + body::-webkit-scrollbar-thumb { + @apply bg-gray-400 rounded; + } + + body::-webkit-scrollbar-thumb:hover { + @apply bg-gray-500; + } +} + +@layer utilities { + .dark-scrollbars-compat { + scrollbar-color: rgb(71, 85, 105) rgb(30, 41, 59); + } + + .dark-scrollbars::-webkit-scrollbar-track { + @apply bg-slate-800; + } + + .dark-scrollbars::-webkit-scrollbar-thumb { + @apply bg-slate-600; + } + + .dark-scrollbars::-webkit-scrollbar-thumb:hover { + @apply bg-slate-500; + } +} diff --git a/frontend/src/css/_select-dropdown.css b/frontend/src/css/_select-dropdown.css new file mode 100644 index 0000000..980f207 --- /dev/null +++ b/frontend/src/css/_select-dropdown.css @@ -0,0 +1,32 @@ +.react-select__control { + @apply dark:bg-dark-800 dark:border-dark-700 !important; +} + +.react-select__single-value { + @apply dark:text-white !important; +} + +.react-select__menu { + @apply dark:border-dark-700; +} + +.react-select__menu-list { + @apply dark:bg-dark-800 dark:border-dark-700 dark:rounded !important; +} + +.react-select__option { + @apply cursor-pointer hover:bg-gray-200 dark:hover:bg-dark-700 !important; +} + +.react-select__option--is-focused { + @apply dark:bg-dark-800 dark:text-white hover:dark:bg-dark-700 hover:dark:text-white !important; +} + +.react-select__option--is-selected, +.react-select__option--is-selected:hover { + @apply dark:bg-dark-600 !important; +} + +.react-select__multi-value__remove { + @apply dark:bg-dark-600 dark:text-white hover:dark:bg-red-300 hover:dark:text-red-600 !important; +} diff --git a/frontend/src/css/_table.css b/frontend/src/css/_table.css new file mode 100644 index 0000000..3e62e83 --- /dev/null +++ b/frontend/src/css/_table.css @@ -0,0 +1,117 @@ +@layer base { + table { + @apply w-full; + } + + thead { + @apply hidden lg:table-header-group; + } + + tr { + @apply max-w-full block relative border-b-4 border-gray-100 + lg:table-row lg:border-b-0 dark:border-slate-800; + } + + tr:last-child { + @apply border-b-0; + } + + td:not(:first-child) { + @apply lg:border-l lg:border-t-0 lg:border-r-0 lg:border-b-0 lg:border-gray-100 lg:dark:border-slate-700; + } + + th { + @apply lg:text-left lg:p-3 border-b; + } + + th.sortable { + cursor: pointer; + } + + th.sortable:hover:after { + transition: all 1s; + position: absolute; + + content: '↕'; + + margin-left: 1rem; + } + + th.sortable.asc:hover:after { + content: '↑'; + } + th.sortable.desc:hover:after { + content: '↓'; + } + + td { + @apply flex justify-between text-right py-3 px-4 align-top border-b border-gray-100 + lg:table-cell lg:text-left lg:p-3 lg:align-middle lg:border-b-0 dark:border-slate-800 dark:text-white; + } + + td:last-child { + @apply border-b-0; + } + + tbody tr, + tbody tr:nth-child(odd) { + @apply lg:hover:bg-pavitra-300/70; + } + + tbody tr:nth-child(even) { + @apply lg:bg-pavitra-300 dark:bg-pavitra-300/70; + } + + td:before { + content: attr(data-label); + @apply font-semibold pr-3 text-left lg:hidden; + } + + tbody tr td { + @apply text-sm font-normal text-pavitra-900 dark:text-white; + } + + .datagrid--table, + .MuiDataGrid-root { + @apply rounded border-none !important; + } + + .datagrid--header { + @apply uppercase !important; + } + + .datagrid--header, + .datagrid--header .MuiIconButton-root, + .datagrid--cell, + .datagrid--cell .MuiIconButton-root { + @apply dark:text-white; + } + + .datagrid--cell .MuiDataGrid-booleanCell { + @apply dark:text-white !important; + } + + .datagrid--cell .MuiIconButton-root:hover { + @apply dark:text-white dark:bg-dark-700; + } + + .datagrid--row { + @apply even:bg-gray-100 dark:even:bg-[#1B1D22] dark:odd:bg-dark-900 !important; + } + + .datagrid--table .MuiTablePagination-root { + @apply dark:text-white; + } + + .datagrid--table .MuiTablePagination-root .MuiButtonBase-root:disabled { + @apply dark:text-dark-700; + } + + .datagrid--table .MuiTablePagination-root .MuiButtonBase-root:hover { + @apply dark:bg-dark-700; + } + + .MuiButton-colorInherit { + @apply text-blue-600 dark:text-dark-700 !important; + } +} diff --git a/frontend/src/css/_theme.css b/frontend/src/css/_theme.css new file mode 100644 index 0000000..765b8d3 --- /dev/null +++ b/frontend/src/css/_theme.css @@ -0,0 +1,105 @@ +.theme-pink { + .app-sidebar { + @apply bg-pavitra-900 text-white; + + .menu-title, + .menu-item-icon, + .menu-item-link { + @apply text-white; + } + } + + .app-sidebar-brand { + @apply bg-white; + } + + .bg-blue-600 { + @apply bg-pavitra-800; + } + + .border-blue-700 { + @apply border-pink-700; + } + + .checkbox input[type='checkbox']:checked + .check, + .radio input[type='radio']:checked + .check { + @apply border-pavitra-800; + } + + .helper-container .tab { + @apply bg-pavitra-900; + } + + .focus\:ring:focus { + --tw-ring-color: #14142a; + } + + .checkbox input[type='checkbox']:focus + .check, + .radio input[type='radio']:focus + .check, + .switch input[type='checkbox']:focus + .check { + --tw-ring-color: #14142a; + } +} + +.theme-green { + .app-sidebar { + @apply bg-pavitra-800 text-white; + + .menu-title, + .menu-item-icon, + .menu-item-link { + @apply text-white; + } + } + + .app-sidebar-brand { + @apply bg-white; + } + + .bg-blue-600 { + @apply bg-pavitra-800; + } + + .border-blue-700 { + @apply bg-pavitra-700; + } + + .hover\:bg-blue-700:hover { + @apply bg-pavitra-700; + } + + .text-blue-600 { + @apply text-pavitra-900; + } + + .checkbox input[type='checkbox']:checked + .check, + .radio input[type='radio']:checked + .check { + @apply border-pavitra-800; + } + + .helper-container .tab { + @apply bg-pavitra-700; + } + + .focus\:ring:focus { + --tw-ring-color: #4e4b66; + } + + .checkbox input[type='checkbox']:focus + .check, + .radio input[type='radio']:focus + .check, + .switch input[type='checkbox']:focus + .check { + --tw-ring-color: #4e4b66; + } + + .text-blue-500 { + @apply text-pavitra-800; + } + + .hover\:text-blue-600:hover { + @apply text-pavitra-800; + } + + .active\:text-blue-700:active { + @apply text-pavitra-800; + } +} diff --git a/frontend/src/css/main.css b/frontend/src/css/main.css new file mode 100644 index 0000000..62d1477 --- /dev/null +++ b/frontend/src/css/main.css @@ -0,0 +1,34 @@ +@import 'tailwind/_base.css'; +@import 'tailwind/_components.css'; +@import 'tailwind/_utilities.css'; +@import 'intro.js/introjs.css'; +@import '_checkbox-radio-switch.css'; +@import '_progress.css'; +@import '_scrollbars.css'; +@import '_table.css'; +@import '_helper.css'; +@import '_calendar.css'; +@import '_select-dropdown.css'; +@import '_theme.css'; +@import '_rich-text.css'; + +.introjs-tooltip { + @apply min-w-[400px] max-w-[480px] p-2 !important; +} + +.good-img { + @apply -mt-96 !important; +} +.end-img { + @apply -mt-72 !important; +} +.introjs-button { + @apply bg-blue-600 text-white !important; + text-shadow: none !important; +} +.introjs-bullets ul li a.active { + @apply bg-blue-600 !important; +} +.introjs-prevbutton { + @apply bg-transparent border border-blue-600 text-blue-600 !important; +} diff --git a/frontend/src/css/tailwind/_base.css b/frontend/src/css/tailwind/_base.css new file mode 100644 index 0000000..2f02db5 --- /dev/null +++ b/frontend/src/css/tailwind/_base.css @@ -0,0 +1 @@ +@tailwind base; diff --git a/frontend/src/css/tailwind/_components.css b/frontend/src/css/tailwind/_components.css new file mode 100644 index 0000000..020aaba --- /dev/null +++ b/frontend/src/css/tailwind/_components.css @@ -0,0 +1 @@ +@tailwind components; diff --git a/frontend/src/css/tailwind/_utilities.css b/frontend/src/css/tailwind/_utilities.css new file mode 100644 index 0000000..65dd5f6 --- /dev/null +++ b/frontend/src/css/tailwind/_utilities.css @@ -0,0 +1 @@ +@tailwind utilities; diff --git a/frontend/src/helpers/dataFormatter.js b/frontend/src/helpers/dataFormatter.js new file mode 100644 index 0000000..4b3baa7 --- /dev/null +++ b/frontend/src/helpers/dataFormatter.js @@ -0,0 +1,136 @@ +import dayjs from 'dayjs'; +import _ from 'lodash'; + +export default { + filesFormatter(arr) { + if (!arr || !arr.length) return []; + return arr.map((item) => item); + }, + imageFormatter(arr) { + if (!arr || !arr.length) return []; + return arr.map((item) => ({ + publicUrl: item.publicUrl || '', + })); + }, + oneImageFormatter(arr) { + if (!arr || !arr.length) return ''; + return arr[0].publicUrl || ''; + }, + dateFormatter(date) { + if (!date) return ''; + return dayjs(date).format('YYYY-MM-DD'); + }, + dateTimeFormatter(date) { + if (!date) return ''; + return dayjs(date).format('YYYY-MM-DD HH:mm'); + }, + booleanFormatter(val) { + return val ? 'Yes' : 'No'; + }, + dataGridEditFormatter(obj) { + return _.transform(obj, (result, value, key) => { + if (_.isArray(value)) { + result[key] = _.map(value, 'id'); + } else if (_.isObject(value)) { + result[key] = value.id; + } else { + result[key] = value; + } + }); + }, + + usersManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.firstName); + }, + usersOneListFormatter(val) { + if (!val) return ''; + return val.firstName; + }, + usersManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.firstName }; + }); + }, + usersOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.firstName, id: val.id }; + }, + + qr_codesManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.code); + }, + qr_codesOneListFormatter(val) { + if (!val) return ''; + return val.code; + }, + qr_codesManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.code }; + }); + }, + qr_codesOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.code, id: val.id }; + }, + + videosManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.title); + }, + videosOneListFormatter(val) { + if (!val) return ''; + return val.title; + }, + videosManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.title }; + }); + }, + videosOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.title, id: val.id }; + }, + + rolesManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + rolesOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + rolesManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + rolesOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, + + permissionsManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + permissionsOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + permissionsManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + permissionsOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, +}; diff --git a/frontend/src/helpers/fileSaver.ts b/frontend/src/helpers/fileSaver.ts new file mode 100644 index 0000000..242b540 --- /dev/null +++ b/frontend/src/helpers/fileSaver.ts @@ -0,0 +1,6 @@ +import { saveAs } from 'file-saver'; + +export const saveFile = (e, url: string, name: string) => { + e.stopPropagation(); + saveAs(url, name); +}; diff --git a/frontend/src/helpers/humanize.ts b/frontend/src/helpers/humanize.ts new file mode 100644 index 0000000..61b6407 --- /dev/null +++ b/frontend/src/helpers/humanize.ts @@ -0,0 +1,12 @@ +export function humanize(str: string) { + if (!str) { + return ''; + } + return str + .toString() + .replace(/^[\s_]+|[\s_]+$/g, '') + .replace(/[_\s]+/g, ' ') + .replace(/^[a-z]/, function (m) { + return m.toUpperCase(); + }); +} diff --git a/frontend/src/helpers/notifyStateHandler.ts b/frontend/src/helpers/notifyStateHandler.ts new file mode 100644 index 0000000..c13987b --- /dev/null +++ b/frontend/src/helpers/notifyStateHandler.ts @@ -0,0 +1,30 @@ +export const resetNotify = (state) => { + state.notify.showNotification = false; + state.notify.typeNotification = ''; + state.notify.textNotification = ''; +}; +export const rejectNotify = (state, action) => { + if (typeof action.payload === 'string') { + state.notify.textNotification = action.payload; + } else if (typeof action === 'object') { + const obj = { ...action.payload?.errors }; + delete obj['_errors']; + + let msg = ''; + + for (const key in obj) { + msg += `${key}: ${obj[key]['_errors']}; \n `; + } + + state.notify.textNotification = msg; + } else { + state.notify.textNotification = ''; + } + state.notify.typeNotification = 'error'; + state.notify.showNotification = true; +}; +export const fulfilledNotify = (state, msg) => { + state.notify.textNotification = msg; + state.notify.typeNotification = 'success'; + state.notify.showNotification = true; +}; diff --git a/frontend/src/helpers/pexels.ts b/frontend/src/helpers/pexels.ts new file mode 100644 index 0000000..524a1b2 --- /dev/null +++ b/frontend/src/helpers/pexels.ts @@ -0,0 +1,75 @@ +import axios from 'axios'; + +export async function getPexelsImage() { + try { + const response = await axios.get(`/pexels/image`); + return response.data; + } catch (error) { + console.error('Error fetching image:', error); + return null; + } +} + +export async function getPexelsVideo() { + try { + const response = await axios.get(`/pexels/video`); + return response.data; + } catch (error) { + console.error('Error fetching video:', error); + return null; + } +} + +let localStorageLock = false; + +export async function getMultiplePexelsImages( + queries = ['home', 'apple', 'pizza', 'mountains', 'cat'], +) { + const normalizeQuery = (query) => + query.trim().toLowerCase().replace(/\s+/g, ''); + + while (localStorageLock) { + await new Promise((resolve) => setTimeout(resolve, 50)); + } + localStorageLock = true; + + const cachedImages = + JSON.parse(localStorage.getItem('pexelsImagesCache')) || {}; + + const isImageCached = (query) => { + const normalizedQuery = normalizeQuery(query); + const cached = cachedImages[normalizedQuery]; + const isCached = + cached && cached.src && cached.photographer && cached.photographer_url; + return isCached; + }; + + const missingQueries = queries.filter((query) => !isImageCached(query)); + + if (missingQueries.length > 0) { + const queryString = missingQueries.join(','); + + try { + const response = await axios.get(`/pexels/multiple-images`, { + params: { queries: queryString }, + }); + + missingQueries.forEach((query, index) => { + const normalizedQuery = normalizeQuery(query); + if (!cachedImages[normalizedQuery]) { + cachedImages[normalizedQuery] = response.data[index]; + } + }); + + localStorage.setItem('pexelsImagesCache', JSON.stringify(cachedImages)); + } catch (error) { + console.error(error); + } + } + + const result = queries.map((query) => cachedImages[normalizeQuery(query)]); + + localStorageLock = false; + + return result; +} diff --git a/frontend/src/helpers/userPermissions.ts b/frontend/src/helpers/userPermissions.ts new file mode 100644 index 0000000..377546b --- /dev/null +++ b/frontend/src/helpers/userPermissions.ts @@ -0,0 +1,18 @@ +export function hasPermission(user, permission_name: string | string[]) { + if (!user?.app_role?.name) return false; + if (!permission_name) { + return true; + } + const permissions = new Set([ + ...(user?.custom_permissions ?? []).map((p) => p.name), + ...(user?.app_role_permissions ?? []).map((p) => p.name), + ]); + + if (typeof permission_name === 'string') { + return ( + permissions.has(permission_name) || user.app_role.name === 'Administrator' + ); + } else { + return permission_name.some((permission) => permissions.has(permission)); + } +} diff --git a/frontend/src/hooks/sampleData.ts b/frontend/src/hooks/sampleData.ts new file mode 100644 index 0000000..8c74ad5 --- /dev/null +++ b/frontend/src/hooks/sampleData.ts @@ -0,0 +1,22 @@ +import useSWR from 'swr'; +const fetcher = (url: string) => fetch(url).then((res) => res.json()); + +export const useSampleClients = () => { + const { data, error } = useSWR('/data-sources/clients.json', fetcher); + + return { + clients: data?.data ?? [], + isLoading: !error && !data, + isError: error, + }; +}; + +export const useSampleTransactions = () => { + const { data, error } = useSWR('/data-sources/history.json', fetcher); + + return { + transactions: data?.data ?? [], + isLoading: !error && !data, + isError: error, + }; +}; diff --git a/frontend/src/interfaces/index.ts b/frontend/src/interfaces/index.ts new file mode 100644 index 0000000..75048f3 --- /dev/null +++ b/frontend/src/interfaces/index.ts @@ -0,0 +1,122 @@ +export type UserPayloadObject = { + name: string; + email: string; + avatar: string; +}; + +export type MenuAsideItem = { + label: string; + icon?: string; + href?: string; + target?: string; + color?: ColorButtonKey; + isLogout?: boolean; + withDevider?: boolean; + menu?: MenuAsideItem[]; + permissions?: string | string[]; +}; + +export type MenuNavBarItem = { + label?: string; + icon?: string; + href?: string; + target?: string; + isDivider?: boolean; + isLogout?: boolean; + isDesktopNoLabel?: boolean; + isToggleLightDark?: boolean; + isCurrentUser?: boolean; + menu?: MenuNavBarItem[]; +}; + +export type ColorKey = + | 'white' + | 'light' + | 'contrast' + | 'success' + | 'danger' + | 'warning' + | 'info'; + +export type ColorButtonKey = + | 'white' + | 'whiteDark' + | 'lightDark' + | 'contrast' + | 'success' + | 'danger' + | 'warning' + | 'info' + | 'void'; + +export type BgKey = 'purplePink' | 'pinkRed' | 'violet'; + +export type TrendType = + | 'up' + | 'down' + | 'success' + | 'danger' + | 'warning' + | 'info'; + +export type TransactionType = 'withdraw' | 'deposit' | 'invoice' | 'payment'; + +export type Transaction = { + id: number; + amount: number; + account: string; + name: string; + date: string; + type: TransactionType; + business: string; +}; + +export type Client = { + id: number; + avatar: string; + login: string; + name: string; + city: string; + company: string; + firstName: string; + lastName: string; + phoneNumber: string; + email: string; + progress: number; + role: string; + disabled: boolean; + created: string; + created_mm_dd_yyyy: string; +}; + +export interface User { + id: string; + firstName: string; + lastName?: any; + phoneNumber?: any; + email: string; + role: string; + disabled: boolean; + password: string; + emailVerified: boolean; + emailVerificationToken?: any; + emailVerificationTokenExpiresAt?: any; + passwordResetToken?: any; + passwordResetTokenExpiresAt?: any; + provider: string; + importHash?: any; + createdAt: Date; + updatedAt: Date; + deletedAt?: any; + createdById?: any; + updatedById?: any; + avatar: any[]; + notes: any[]; +} + +export type StyleKey = 'white' | 'basic'; + +export type UserForm = { + name: string; + email: string; +}; diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx new file mode 100644 index 0000000..7d616a3 --- /dev/null +++ b/frontend/src/layouts/Authenticated.tsx @@ -0,0 +1,132 @@ +import React, { ReactNode, useEffect } from 'react'; +import { useState } from 'react'; +import jwt from 'jsonwebtoken'; +import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'; +import menuAside from '../menuAside'; +import menuNavBar from '../menuNavBar'; +import BaseIcon from '../components/BaseIcon'; +import NavBar from '../components/NavBar'; +import NavBarItemPlain from '../components/NavBarItemPlain'; +import AsideMenu from '../components/AsideMenu'; +import FooterBar from '../components/FooterBar'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import Search from '../components/Search'; +import { useRouter } from 'next/router'; +import { findMe, logoutUser } from '../stores/authSlice'; + +import { hasPermission } from '../helpers/userPermissions'; + +type Props = { + children: ReactNode; + + permission?: string; +}; + +export default function LayoutAuthenticated({ + children, + + permission, +}: Props) { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { token, currentUser } = useAppSelector((state) => state.auth); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + let localToken; + if (typeof window !== 'undefined') { + // Perform localStorage action + localToken = localStorage.getItem('token'); + } + + const isTokenValid = () => { + const token = localStorage.getItem('token'); + if (!token) return; + const date = new Date().getTime() / 1000; + const data = jwt.decode(token); + if (!data) return; + return date < data.exp; + }; + + useEffect(() => { + dispatch(findMe()); + if (!isTokenValid()) { + dispatch(logoutUser()); + router.push('/login'); + } + }, [token, localToken]); + + useEffect(() => { + if (!permission || !currentUser) return; + + if (!hasPermission(currentUser, permission)) router.push('/error'); + }, [currentUser, permission]); + + const darkMode = useAppSelector((state) => state.style.darkMode); + + const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false); + const [isAsideLgActive, setIsAsideLgActive] = useState(false); + + useEffect(() => { + const handleRouteChangeStart = () => { + setIsAsideMobileExpanded(false); + setIsAsideLgActive(false); + }; + + router.events.on('routeChangeStart', handleRouteChangeStart); + + // If the component is unmounted, unsubscribe + // from the event with the `off` method: + return () => { + router.events.off('routeChangeStart', handleRouteChangeStart); + }; + }, [router.events, dispatch]); + + const layoutAsidePadding = 'xl:pl-60'; + + return ( +
    +
    + + setIsAsideMobileExpanded(!isAsideMobileExpanded)} + > + + + setIsAsideLgActive(true)} + > + + + + + + + setIsAsideLgActive(false)} + /> + {children} + Hand-crafted & Made with ❤️ +
    +
    + ); +} diff --git a/frontend/src/layouts/Guest.tsx b/frontend/src/layouts/Guest.tsx new file mode 100644 index 0000000..49ac1b0 --- /dev/null +++ b/frontend/src/layouts/Guest.tsx @@ -0,0 +1,19 @@ +import React, { ReactNode } from 'react'; +import { useAppSelector } from '../stores/hooks'; + +type Props = { + children: ReactNode; +}; + +export default function LayoutGuest({ children }: Props) { + const darkMode = useAppSelector((state) => state.style.darkMode); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + + return ( +
    +
    + {children} +
    +
    + ); +} diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts new file mode 100644 index 0000000..666b930 --- /dev/null +++ b/frontend/src/menuAside.ts @@ -0,0 +1,84 @@ +import * as icon from '@mdi/js'; +import { MenuAsideItem } from './interfaces'; + +const menuAside: MenuAsideItem[] = [ + { + href: '/dashboard', + icon: icon.mdiViewDashboardOutline, + label: 'Dashboard', + }, + + { + href: '/users/users-list', + label: 'Users', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiAccountGroup ? icon.mdiAccountGroup : icon.mdiTable, + permissions: 'READ_USERS', + }, + { + href: '/access_logs/access_logs-list', + label: 'Access logs', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiHistory ? icon.mdiHistory : icon.mdiTable, + permissions: 'READ_ACCESS_LOGS', + }, + { + href: '/qr_codes/qr_codes-list', + label: 'Qr codes', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiQrcode ? icon.mdiQrcode : icon.mdiTable, + permissions: 'READ_QR_CODES', + }, + { + href: '/videos/videos-list', + label: 'Videos', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiVideo ? icon.mdiVideo : icon.mdiTable, + permissions: 'READ_VIDEOS', + }, + { + href: '/roles/roles-list', + label: 'Roles', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiShieldAccountVariantOutline + ? icon.mdiShieldAccountVariantOutline + : icon.mdiTable, + permissions: 'READ_ROLES', + }, + { + href: '/permissions/permissions-list', + label: 'Permissions', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiShieldAccountOutline + ? icon.mdiShieldAccountOutline + : icon.mdiTable, + permissions: 'READ_PERMISSIONS', + }, + { + href: '/profile', + label: 'Profile', + icon: icon.mdiAccountCircle, + }, + + { + href: '/home', + label: 'Home page', + icon: icon.mdiHome, + withDevider: true, + }, + { + href: '/api-docs', + target: '_blank', + label: 'Swagger API', + icon: icon.mdiFileCode, + permissions: 'READ_API_DOCS', + }, +]; + +export default menuAside; diff --git a/frontend/src/menuNavBar.ts b/frontend/src/menuNavBar.ts new file mode 100644 index 0000000..f24b3b0 --- /dev/null +++ b/frontend/src/menuNavBar.ts @@ -0,0 +1,49 @@ +import { + mdiMenu, + mdiClockOutline, + mdiCloud, + mdiCrop, + mdiAccount, + mdiCogOutline, + mdiEmail, + mdiLogout, + mdiThemeLightDark, + mdiGithub, + mdiVuejs, +} from '@mdi/js'; +import { MenuNavBarItem } from './interfaces'; + +const menuNavBar: MenuNavBarItem[] = [ + { + isCurrentUser: true, + menu: [ + { + icon: mdiAccount, + label: 'My Profile', + href: '/profile', + }, + { + isDivider: true, + }, + { + icon: mdiLogout, + label: 'Log Out', + isLogout: true, + }, + ], + }, + { + icon: mdiThemeLightDark, + label: 'Light/Dark', + isDesktopNoLabel: true, + isToggleLightDark: true, + }, + { + icon: mdiLogout, + label: 'Log out', + isDesktopNoLabel: true, + isLogout: true, + }, +]; + +export default menuNavBar; diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx new file mode 100644 index 0000000..3cb6922 --- /dev/null +++ b/frontend/src/pages/_app.tsx @@ -0,0 +1,174 @@ +import React from 'react'; +import type { AppProps } from 'next/app'; +import type { ReactElement, ReactNode } from 'react'; +import type { NextPage } from 'next'; +import Head from 'next/head'; +import { store } from '../stores/store'; +import { Provider } from 'react-redux'; +import '../css/main.css'; +import axios from 'axios'; +import { baseURLApi } from '../config'; +import { useRouter } from 'next/router'; +import 'intro.js/introjs.css'; +import IntroGuide from '../components/IntroGuide'; +import { + appSteps, + landingSteps, + loginSteps, + usersSteps, + rolesSteps, +} from '../stores/introSteps'; + +export type NextPageWithLayout

    , IP = P> = NextPage< + P, + IP +> & { + getLayout?: (page: ReactElement) => ReactNode; +}; + +type AppPropsWithLayout = AppProps & { + Component: NextPageWithLayout; +}; + +function MyApp({ Component, pageProps }: AppPropsWithLayout) { + // Use the layout defined at the page level, if available + const getLayout = Component.getLayout || ((page) => page); + + if (typeof window !== 'undefined') { + // Perform localStorage action + console.log( + 'process.env.NEXT_PUBLIC_BACK_API', + process.env.NEXT_PUBLIC_BACK_API, + ); + axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API + ? process.env.NEXT_PUBLIC_BACK_API + : baseURLApi; + axios.defaults.headers.common['Content-Type'] = 'application/json'; + const token = localStorage.getItem('token'); + if (token) { + axios.defaults.headers.common['Authorization'] = 'Bearer ' + token; + } + } + + React.useEffect(() => { + if (typeof window !== 'undefined') { + const handleMessage = (event) => { + if (event.data === 'getLocation') { + event.source.postMessage( + { iframeLocation: window.location.pathname }, + event.origin, + ); + } + }; + + window.addEventListener('message', handleMessage); + + // Cleanup listener on unmount + return () => { + window.removeEventListener('message', handleMessage); + }; + } + }, []); + + const title = 'sev'; + + const description = 'sev generated by Flatlogic'; + + const url = 'https://flatlogic.com/'; + + const image = `https://flatlogic.com/logo.svg`; + + const imageWidth = '1920'; + + const imageHeight = '960'; + + const [stepsEnabled, setStepsEnabled] = React.useState(true); + const [stepName, setStepName] = React.useState(''); + const [steps, setSteps] = React.useState([]); + const router = useRouter(); + React.useEffect(() => { + const isCompleted = (stepKey: string) => { + return localStorage.getItem(`completed_${stepKey}`) === 'true'; + }; + if (router.pathname === '/login' && !isCompleted('loginSteps')) { + setSteps(loginSteps); + setStepName('loginSteps'); + setStepsEnabled(true); + } else if (router.pathname === '/' && !isCompleted('landingSteps')) { + setSteps(landingSteps); + setStepName('landingSteps'); + setStepsEnabled(true); + } else if (router.pathname === '/dashboard' && !isCompleted('appSteps')) { + setTimeout(() => { + setSteps(appSteps); + setStepName('appSteps'); + setStepsEnabled(true); + }, 1000); + } else if ( + router.pathname === '/users/users-list' && + !isCompleted('usersSteps') + ) { + setTimeout(() => { + setSteps(usersSteps); + setStepName('usersSteps'); + setStepsEnabled(true); + }, 1000); + } else if ( + router.pathname === '/roles/roles-list' && + !isCompleted('rolesSteps') + ) { + setTimeout(() => { + setSteps(rolesSteps); + setStepName('rolesSteps'); + setStepsEnabled(true); + }, 1000); + } else { + setSteps([]); + setStepsEnabled(false); + } + }, [router.pathname]); + + const handleExit = () => { + setStepsEnabled(false); + }; + + return ( + + {getLayout( + <> + + + + + + + + + + + + + + + + + + + + + + + + + , + )} + + ); +} + +export default MyApp; diff --git a/frontend/src/pages/access_logs/[access_logsId].tsx b/frontend/src/pages/access_logs/[access_logsId].tsx new file mode 100644 index 0000000..1728fc1 --- /dev/null +++ b/frontend/src/pages/access_logs/[access_logsId].tsx @@ -0,0 +1,167 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/access_logs/access_logsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditAccess_logs = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + user: null, + + qr_code: null, + + access_time: new Date(), + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { access_logs } = useAppSelector((state) => state.access_logs); + + const { access_logsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: access_logsId })); + }, [access_logsId]); + + useEffect(() => { + if (typeof access_logs === 'object') { + setInitialValues(access_logs); + } + }, [access_logs]); + + useEffect(() => { + if (typeof access_logs === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = access_logs[el]), + ); + + setInitialValues(newInitialVal); + } + }, [access_logs]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: access_logsId, data })); + await router.push('/access_logs/access_logs-list'); + }; + + return ( + <> + + {getPageTitle('Edit access_logs')} + + + + {''} + + + handleSubmit(values)} + > +

    + + + + + + + + + + + setInitialValues({ ...initialValues, access_time: date }) + } + /> + + + + + + + router.push('/access_logs/access_logs-list')} + /> + + + + + + + ); +}; + +EditAccess_logs.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditAccess_logs; diff --git a/frontend/src/pages/access_logs/access_logs-edit.tsx b/frontend/src/pages/access_logs/access_logs-edit.tsx new file mode 100644 index 0000000..f23e93c --- /dev/null +++ b/frontend/src/pages/access_logs/access_logs-edit.tsx @@ -0,0 +1,165 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/access_logs/access_logsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditAccess_logsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + user: null, + + qr_code: null, + + access_time: new Date(), + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { access_logs } = useAppSelector((state) => state.access_logs); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof access_logs === 'object') { + setInitialValues(access_logs); + } + }, [access_logs]); + + useEffect(() => { + if (typeof access_logs === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = access_logs[el]), + ); + setInitialValues(newInitialVal); + } + }, [access_logs]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/access_logs/access_logs-list'); + }; + + return ( + <> + + {getPageTitle('Edit access_logs')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + setInitialValues({ ...initialValues, access_time: date }) + } + /> + + + + + + + router.push('/access_logs/access_logs-list')} + /> + + +
    +
    +
    + + ); +}; + +EditAccess_logsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditAccess_logsPage; diff --git a/frontend/src/pages/access_logs/access_logs-list.tsx b/frontend/src/pages/access_logs/access_logs-list.tsx new file mode 100644 index 0000000..90c6c12 --- /dev/null +++ b/frontend/src/pages/access_logs/access_logs-list.tsx @@ -0,0 +1,175 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableAccess_logs from '../../components/Access_logs/TableAccess_logs'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/access_logs/access_logsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Access_logsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'AccessTime', title: 'access_time', date: 'true' }, + + { label: 'User', title: 'user' }, + + { label: 'QRCode', title: 'qr_code' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_ACCESS_LOGS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getAccess_logsCSV = async () => { + const response = await axios({ + url: '/access_logs?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'access_logsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Access_logs')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + Switch to Table +
    +
    + + + + +
    + + + + + ); +}; + +Access_logsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Access_logsTablesPage; diff --git a/frontend/src/pages/access_logs/access_logs-new.tsx b/frontend/src/pages/access_logs/access_logs-new.tsx new file mode 100644 index 0000000..49b60dc --- /dev/null +++ b/frontend/src/pages/access_logs/access_logs-new.tsx @@ -0,0 +1,126 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/access_logs/access_logsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + user: '', + + qr_code: '', + + access_time: '', +}; + +const Access_logsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/access_logs/access_logs-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + router.push('/access_logs/access_logs-list')} + /> + + +
    +
    +
    + + ); +}; + +Access_logsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Access_logsNew; diff --git a/frontend/src/pages/access_logs/access_logs-table.tsx b/frontend/src/pages/access_logs/access_logs-table.tsx new file mode 100644 index 0000000..eda3cfa --- /dev/null +++ b/frontend/src/pages/access_logs/access_logs-table.tsx @@ -0,0 +1,174 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableAccess_logs from '../../components/Access_logs/TableAccess_logs'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/access_logs/access_logsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Access_logsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'AccessTime', title: 'access_time', date: 'true' }, + + { label: 'User', title: 'user' }, + + { label: 'QRCode', title: 'qr_code' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_ACCESS_LOGS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getAccess_logsCSV = async () => { + const response = await axios({ + url: '/access_logs?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'access_logsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Access_logs')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to list + +
    +
    + + + +
    + + + + + ); +}; + +Access_logsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Access_logsTablesPage; diff --git a/frontend/src/pages/access_logs/access_logs-view.tsx b/frontend/src/pages/access_logs/access_logs-view.tsx new file mode 100644 index 0000000..440ca99 --- /dev/null +++ b/frontend/src/pages/access_logs/access_logs-view.tsx @@ -0,0 +1,111 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/access_logs/access_logsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +const Access_logsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { access_logs } = useAppSelector((state) => state.access_logs); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View access_logs')} + + + + + + +
    +

    User

    + +

    {access_logs?.user?.firstName ?? 'No data'}

    +
    + +
    +

    QRCode

    + +

    {access_logs?.qr_code?.code ?? 'No data'}

    +
    + + + {access_logs.access_time ? ( + + ) : ( +

    No AccessTime

    + )} +
    + + + + router.push('/access_logs/access_logs-list')} + /> +
    +
    + + ); +}; + +Access_logsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Access_logsView; diff --git a/frontend/src/pages/api/hello.js b/frontend/src/pages/api/hello.js new file mode 100644 index 0000000..1c39e1f --- /dev/null +++ b/frontend/src/pages/api/hello.js @@ -0,0 +1,5 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction + +export default function helloAPI(req, res) { + res.status(200).json({ name: 'John Doe' }); +} diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx new file mode 100644 index 0000000..173fe57 --- /dev/null +++ b/frontend/src/pages/dashboard.tsx @@ -0,0 +1,365 @@ +import * as icon from '@mdi/js'; +import Head from 'next/head'; +import React from 'react'; +import axios from 'axios'; +import type { ReactElement } from 'react'; +import LayoutAuthenticated from '../layouts/Authenticated'; +import SectionMain from '../components/SectionMain'; +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; +import BaseIcon from '../components/BaseIcon'; +import { getPageTitle } from '../config'; +import Link from 'next/link'; + +import { hasPermission } from '../helpers/userPermissions'; +import { fetchWidgets } from '../stores/roles/rolesSlice'; +import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator'; +import { SmartWidget } from '../components/SmartWidget/SmartWidget'; + +import { useAppDispatch, useAppSelector } from '../stores/hooks'; +const Dashboard = () => { + const dispatch = useAppDispatch(); + const iconsColor = useAppSelector((state) => state.style.iconsColor); + const corners = useAppSelector((state) => state.style.corners); + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + + const [users, setUsers] = React.useState('Loading...'); + const [access_logs, setAccess_logs] = React.useState('Loading...'); + const [qr_codes, setQr_codes] = React.useState('Loading...'); + const [videos, setVideos] = React.useState('Loading...'); + const [roles, setRoles] = React.useState('Loading...'); + const [permissions, setPermissions] = React.useState('Loading...'); + + const [widgetsRole, setWidgetsRole] = React.useState({ + role: { value: '', label: '' }, + }); + const { currentUser } = useAppSelector((state) => state.auth); + const { isFetchingQuery } = useAppSelector((state) => state.openAi); + + const { rolesWidgets, loading } = useAppSelector((state) => state.roles); + + async function loadData() { + const entities = [ + 'users', + 'access_logs', + 'qr_codes', + 'videos', + 'roles', + 'permissions', + ]; + const fns = [ + setUsers, + setAccess_logs, + setQr_codes, + setVideos, + setRoles, + setPermissions, + ]; + + const requests = entities.map((entity, index) => { + if (hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) { + return axios.get(`/${entity.toLowerCase()}/count`); + } else { + fns[index](null); + return Promise.resolve({ data: { count: null } }); + } + }); + + Promise.allSettled(requests).then((results) => { + results.forEach((result, i) => { + if (result.status === 'fulfilled') { + fns[i](result.value.data.count); + } else { + fns[i](result.reason.message); + } + }); + }); + } + + async function getWidgets(roleId) { + await dispatch(fetchWidgets(roleId)); + } + React.useEffect(() => { + if (!currentUser) return; + loadData().then(); + setWidgetsRole({ + role: { + value: currentUser?.app_role?.id, + label: currentUser?.app_role?.name, + }, + }); + }, [currentUser]); + + React.useEffect(() => { + if (!currentUser || !widgetsRole?.role?.value) return; + getWidgets(widgetsRole?.role?.value || '').then(); + }, [widgetsRole?.role?.value]); + + return ( + <> + + {getPageTitle('Dashboard')} + + + + {''} + + + {hasPermission(currentUser, 'CREATE_ROLES') && ( + + )} + {!!rolesWidgets.length && + hasPermission(currentUser, 'CREATE_ROLES') && ( +

    + {`${widgetsRole?.role?.label || 'Users'}'s widgets`} +

    + )} + +
    + {(isFetchingQuery || loading) && ( +
    + {' '} + Loading widgets... +
    + )} + + {rolesWidgets && + rolesWidgets.map((widget) => ( + + ))} +
    + + {!!rolesWidgets.length &&
    } + +
    + {hasPermission(currentUser, 'READ_USERS') && ( + +
    +
    +
    +
    + Users +
    +
    + {users} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_ACCESS_LOGS') && ( + +
    +
    +
    +
    + Access logs +
    +
    + {access_logs} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_QR_CODES') && ( + +
    +
    +
    +
    + Qr codes +
    +
    + {qr_codes} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_VIDEOS') && ( + +
    +
    +
    +
    + Videos +
    +
    + {videos} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_ROLES') && ( + +
    +
    +
    +
    + Roles +
    +
    + {roles} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_PERMISSIONS') && ( + +
    +
    +
    +
    + Permissions +
    +
    + {permissions} +
    +
    +
    + +
    +
    +
    + + )} +
    +
    + + ); +}; + +Dashboard.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default Dashboard; diff --git a/frontend/src/pages/error.tsx b/frontend/src/pages/error.tsx new file mode 100644 index 0000000..0e80c27 --- /dev/null +++ b/frontend/src/pages/error.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import BaseButton from '../components/BaseButton'; +import CardBox from '../components/CardBox'; +import SectionFullScreen from '../components/SectionFullScreen'; +import LayoutGuest from '../layouts/Guest'; +import { getPageTitle } from '../config'; + +export default function Error() { + return ( + <> + + {getPageTitle('Error')} + + + + } + > +
    +

    Unhandled exception

    + +

    An Error Occurred

    +
    +
    +
    + + ); +} + +Error.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/forgot.tsx b/frontend/src/pages/forgot.tsx new file mode 100644 index 0000000..071239b --- /dev/null +++ b/frontend/src/pages/forgot.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import type { ReactElement } from 'react'; +import { ToastContainer, toast } from 'react-toastify'; +import Head from 'next/head'; +import BaseButton from '../components/BaseButton'; +import CardBox from '../components/CardBox'; +import SectionFullScreen from '../components/SectionFullScreen'; +import LayoutGuest from '../layouts/Guest'; +import { Field, Form, Formik } from 'formik'; +import FormField from '../components/FormField'; +import BaseDivider from '../components/BaseDivider'; +import BaseButtons from '../components/BaseButtons'; +import { useRouter } from 'next/router'; +import { getPageTitle } from '../config'; +import axios from 'axios'; + +export default function Forgot() { + const [loading, setLoading] = React.useState(false); + const router = useRouter(); + const notify = (type, msg) => toast(msg, { type }); + + const handleSubmit = async (value) => { + setLoading(true); + try { + const { data: response } = await axios.post( + '/auth/send-password-reset-email', + value, + ); + setLoading(false); + notify('success', 'Please check your email for verification link'); + setTimeout(async () => { + await router.push('/login'); + }, 3000); + } catch (error) { + setLoading(false); + console.log('error: ', error); + notify('error', 'Something was wrong. Try again'); + } + }; + + return ( + <> + + {getPageTitle('Login')} + + + + + handleSubmit(values)} + > +
    + + + + + + + + + + + +
    +
    +
    + + + ); +} + +Forgot.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/forms.tsx b/frontend/src/pages/forms.tsx new file mode 100644 index 0000000..45de29b --- /dev/null +++ b/frontend/src/pages/forms.tsx @@ -0,0 +1,162 @@ +import { + mdiAccount, + mdiBallotOutline, + mdiGithub, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import { Field, Form, Formik } from 'formik'; +import Head from 'next/head'; +import { ReactElement } from 'react'; +import BaseButton from '../components/BaseButton'; +import BaseButtons from '../components/BaseButtons'; +import BaseDivider from '../components/BaseDivider'; +import CardBox from '../components/CardBox'; +import FormCheckRadio from '../components/FormCheckRadio'; +import FormCheckRadioGroup from '../components/FormCheckRadioGroup'; +import FormField from '../components/FormField'; +import LayoutAuthenticated from '../layouts/Authenticated'; +import SectionMain from '../components/SectionMain'; +import SectionTitle from '../components/SectionTitle'; +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../config'; + +const FormsPage = () => { + return ( + <> + + {getPageTitle('Forms')} + + + + + {''} + + + + alert(JSON.stringify(values, null, 2))} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + + Custom elements + + + + null} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +
    + + ); +}; + +FormsPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default FormsPage; diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx new file mode 100644 index 0000000..085465d --- /dev/null +++ b/frontend/src/pages/index.tsx @@ -0,0 +1,132 @@ +import React, { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import Link from 'next/link'; +import { useAppSelector } from '../stores/hooks'; +import LayoutGuest from '../layouts/Guest'; +import WebSiteHeader from '../components/WebPageComponents/Header'; +import WebSiteFooter from '../components/WebPageComponents/Footer'; +import { + ContactFormDesigns, + HeroDesigns, + FeaturesDesigns, + AboutUsDesigns, +} from '../components/WebPageComponents/designs'; + +import ContactFormSection from '../components/WebPageComponents/ContactFormComponent'; + +import HeroSection from '../components/WebPageComponents/HeroComponent'; + +import FeaturesSection from '../components/WebPageComponents/FeaturesComponent'; + +import AboutUsSection from '../components/WebPageComponents/AboutUsComponent'; + +export default function WebSite() { + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const projectName = 'sev'; + + useEffect(() => { + const darkElement = document.querySelector('body .dark'); + if (darkElement) { + darkElement.classList.remove('dark'); + } + }, []); + const pages = [ + { + href: '/home', + label: 'home', + }, + + { + href: '/about', + label: 'about', + }, + + { + href: '/contact', + label: 'contact', + }, + + { + href: '/faq', + label: 'FAQ', + }, + ]; + + const features_points = [ + { + name: 'Seamless QR Scanning', + description: + 'Effortlessly scan QR codes to unlock exclusive video content. Our system ensures quick and accurate scanning for a smooth user experience.', + icon: 'mdiQrcodeScan', + }, + { + name: 'Timed Video Access', + description: + 'Enjoy video content for a limited time after scanning. Our custom timer ensures you make the most of your viewing session.', + icon: 'mdiTimer', + }, + { + name: 'Admin Control Panel', + description: + 'Manage video content and QR settings with ease. The admin panel provides full control over user access and content management.', + icon: 'mdiAccountCog', + }, + ]; + + return ( +
    + + {`Welcome to Sevastopol QR Verification`} + + + +
    + + + + + + + +
    + +
    + ); +} + +WebSite.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx new file mode 100644 index 0000000..dd65582 --- /dev/null +++ b/frontend/src/pages/login.tsx @@ -0,0 +1,299 @@ +import React, { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import BaseButton from '../components/BaseButton'; +import CardBox from '../components/CardBox'; +import BaseIcon from '../components/BaseIcon'; +import { mdiInformation } from '@mdi/js'; +import SectionFullScreen from '../components/SectionFullScreen'; +import LayoutGuest from '../layouts/Guest'; +import { Field, Form, Formik } from 'formik'; +import FormField from '../components/FormField'; +import FormCheckRadio from '../components/FormCheckRadio'; +import BaseDivider from '../components/BaseDivider'; +import BaseButtons from '../components/BaseButtons'; +import { useRouter } from 'next/router'; +import { getPageTitle } from '../config'; +import { findMe, loginUser, resetAction } from '../stores/authSlice'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import Link from 'next/link'; +import { toast, ToastContainer } from 'react-toastify'; +import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; + +export default function Login() { + const router = useRouter(); + const dispatch = useAppDispatch(); + const textColor = useAppSelector((state) => state.style.linkColor); + const iconsColor = useAppSelector((state) => state.style.iconsColor); + const notify = (type, msg) => toast(msg, { type }); + const [illustrationImage, setIllustrationImage] = useState({ + src: undefined, + photographer: undefined, + photographer_url: undefined, + }); + const [illustrationVideo, setIllustrationVideo] = useState({ + video_files: [], + }); + const [contentType, setContentType] = useState('video'); + const [contentPosition, setContentPosition] = useState('right'); + + const { + currentUser, + isFetching, + errorMessage, + token, + notify: notifyState, + } = useAppSelector((state) => state.auth); + const [initialValues, setInitialValues] = React.useState({ + email: 'admin@flatlogic.com', + password: 'password', + remember: true, + }); + + const title = 'sev'; + + // Fetch Pexels image/video + useEffect(() => { + async function fetchData() { + const image = await getPexelsImage(); + const video = await getPexelsVideo(); + setIllustrationImage(image); + setIllustrationVideo(video); + } + fetchData(); + }, []); + // Fetch user data + useEffect(() => { + if (token) { + dispatch(findMe()); + } + }, [token, dispatch]); + // Redirect to dashboard if user is logged in + useEffect(() => { + if (currentUser?.id) { + router.push('/dashboard'); + } + }, [currentUser?.id, router]); + // Show error message if there is one + useEffect(() => { + if (errorMessage) { + notify('error', errorMessage); + } + }, [errorMessage]); + // Show notification if there is one + useEffect(() => { + if (notifyState?.showNotification) { + notify('success', notifyState?.textNotification); + dispatch(resetAction()); + } + }, [notifyState?.showNotification]); + + const handleSubmit = async (value) => { + const { remember, ...rest } = value; + await dispatch(loginUser(rest)); + }; + + const setLogin = (target) => { + const email = target?.innerText; + setInitialValues((prev) => { + return { ...prev, email, password: 'password' }; + }); + }; + + const imageBlock = (image) => ( + + ); + + const videoBlock = (video) => { + if (video?.video_files?.length > 0) { + return ( +
    + + +
    + ); + } + }; + + return ( +
    + + {getPageTitle('Login')} + + + +
    + {contentType === 'image' && contentPosition !== 'background' + ? imageBlock(illustrationImage) + : null} + {contentType === 'video' && contentPosition !== 'background' + ? videoBlock(illustrationVideo) + : null} +
    + + +

    sev

    + + +
    +
    +

    + Use{' '} + setLogin(e.target)} + > + admin@flatlogic.com + {' '} + to login as Admin +

    +

    + Use{' '} + setLogin(e.target)} + > + client@hello.com + {' '} + to login as User +

    +
    +
    + +
    +
    +
    + + + handleSubmit(values)} + > +
    + + + + + + + + +
    + + + + + + Forgot password? + +
    + + + + + + +
    +

    + Don’t have account yet?{' '} + + New Account + +

    + +
    +
    +
    +
    +
    +
    +

    + © 2024 {title}. All rights reserved +

    + + Privacy Policy + +
    + +
    + ); +} + +Login.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/password-reset.tsx b/frontend/src/pages/password-reset.tsx new file mode 100644 index 0000000..e75c2f4 --- /dev/null +++ b/frontend/src/pages/password-reset.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import type { ReactElement } from 'react'; +import LayoutGuest from '../layouts/Guest'; +import PasswordSetOrReset from '../components/PasswordSetOrReset'; + +export default function Reset() { + return ; +} + +Reset.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/permissions/[permissionsId].tsx b/frontend/src/pages/permissions/[permissionsId].tsx new file mode 100644 index 0000000..c3e482c --- /dev/null +++ b/frontend/src/pages/permissions/[permissionsId].tsx @@ -0,0 +1,126 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/permissions/permissionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditPermissions = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { permissions } = useAppSelector((state) => state.permissions); + + const { permissionsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: permissionsId })); + }, [permissionsId]); + + useEffect(() => { + if (typeof permissions === 'object') { + setInitialValues(permissions); + } + }, [permissions]); + + useEffect(() => { + if (typeof permissions === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = permissions[el]), + ); + + setInitialValues(newInitialVal); + } + }, [permissions]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: permissionsId, data })); + await router.push('/permissions/permissions-list'); + }; + + return ( + <> + + {getPageTitle('Edit permissions')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + router.push('/permissions/permissions-list')} + /> + + +
    +
    +
    + + ); +}; + +EditPermissions.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditPermissions; diff --git a/frontend/src/pages/permissions/permissions-edit.tsx b/frontend/src/pages/permissions/permissions-edit.tsx new file mode 100644 index 0000000..17c0586 --- /dev/null +++ b/frontend/src/pages/permissions/permissions-edit.tsx @@ -0,0 +1,124 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/permissions/permissionsSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditPermissionsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { permissions } = useAppSelector((state) => state.permissions); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof permissions === 'object') { + setInitialValues(permissions); + } + }, [permissions]); + + useEffect(() => { + if (typeof permissions === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = permissions[el]), + ); + setInitialValues(newInitialVal); + } + }, [permissions]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/permissions/permissions-list'); + }; + + return ( + <> + + {getPageTitle('Edit permissions')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + router.push('/permissions/permissions-list')} + /> + + +
    +
    +
    + + ); +}; + +EditPermissionsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditPermissionsPage; diff --git a/frontend/src/pages/permissions/permissions-list.tsx b/frontend/src/pages/permissions/permissions-list.tsx new file mode 100644 index 0000000..97955e4 --- /dev/null +++ b/frontend/src/pages/permissions/permissions-list.tsx @@ -0,0 +1,165 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TablePermissions from '../../components/Permissions/TablePermissions'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/permissions/permissionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const PermissionsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{ label: 'Name', title: 'name' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_PERMISSIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getPermissionsCSV = async () => { + const response = await axios({ + url: '/permissions?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'permissionsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Permissions')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    +
    + + + + +
    + + + + + ); +}; + +PermissionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default PermissionsTablesPage; diff --git a/frontend/src/pages/permissions/permissions-new.tsx b/frontend/src/pages/permissions/permissions-new.tsx new file mode 100644 index 0000000..e5a9eb0 --- /dev/null +++ b/frontend/src/pages/permissions/permissions-new.tsx @@ -0,0 +1,98 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/permissions/permissionsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + name: '', +}; + +const PermissionsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/permissions/permissions-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + router.push('/permissions/permissions-list')} + /> + + +
    +
    +
    + + ); +}; + +PermissionsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default PermissionsNew; diff --git a/frontend/src/pages/permissions/permissions-table.tsx b/frontend/src/pages/permissions/permissions-table.tsx new file mode 100644 index 0000000..4f54815 --- /dev/null +++ b/frontend/src/pages/permissions/permissions-table.tsx @@ -0,0 +1,164 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TablePermissions from '../../components/Permissions/TablePermissions'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/permissions/permissionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const PermissionsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{ label: 'Name', title: 'name' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_PERMISSIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getPermissionsCSV = async () => { + const response = await axios({ + url: '/permissions?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'permissionsCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Permissions')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    +
    + + + +
    + + + + + ); +}; + +PermissionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default PermissionsTablesPage; diff --git a/frontend/src/pages/permissions/permissions-view.tsx b/frontend/src/pages/permissions/permissions-view.tsx new file mode 100644 index 0000000..73dde19 --- /dev/null +++ b/frontend/src/pages/permissions/permissions-view.tsx @@ -0,0 +1,83 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/permissions/permissionsSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +const PermissionsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { permissions } = useAppSelector((state) => state.permissions); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View permissions')} + + + + + + +
    +

    Name

    +

    {permissions?.name}

    +
    + + + + router.push('/permissions/permissions-list')} + /> +
    +
    + + ); +}; + +PermissionsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default PermissionsView; diff --git a/frontend/src/pages/privacy-policy.tsx b/frontend/src/pages/privacy-policy.tsx new file mode 100644 index 0000000..bb91254 --- /dev/null +++ b/frontend/src/pages/privacy-policy.tsx @@ -0,0 +1,292 @@ +import React, { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import LayoutGuest from '../layouts/Guest'; +import { getPageTitle } from '../config'; + +export default function PrivacyPolicy() { + const title = 'sev'; + const [projectUrl, setProjectUrl] = useState(''); + + useEffect(() => { + setProjectUrl(location.origin); + }, []); + + const Introduction = () => { + return ( + <> +

    1. Introduction

    +

    + {/* eslint-disable-next-line react/no-unescaped-entities */} + We at {title} ("we", "us", "our") are committed to + protecting your privacy. This Privacy Policy explains how we collect, + use, disclose, and safeguard your information when you visit our + website {projectUrl}, use our services, or + interact with us in other ways. By using our services, you agree to + the collection and use of information in accordance with this policy. +

    + + ); + }; + + const Information = () => { + return ( + <> +

    2. Information We Collect

    +
    +

    2.1 Personal Identification Information

    +

    + We collect various types of personal information in connection with + the services we provide, including: +

    +
      +
    • + Contact Information: Name, email address, phone number, mailing + address. +
    • +
    • Account Information: Username, password, profile picture.
    • +
    • Payment Information: Credit card details, billing address.
    • +
    • Demographic Information: Age, gender, interests.
    • +
    +

    2.2 Technical Data

    +

    + We automatically collect certain information when you visit, use, or + navigate our services. This information may include: +

    +
      +
    • + Device Information: IP address, browser type, operating system, + device type. +
    • +
    • + Usage Data: Pages visited, time spent on each page, links clicked, + and other actions taken on our site. +
    • +
    +

    2.3 Cookies and Tracking Technologies

    +

    + We use cookies and similar tracking technologies to track the + activity on our service and hold certain information. You can + instruct your browser to refuse all cookies or to indicate when a + cookie is being sent. +

    +
    + + ); + }; + + const HowToUser = () => { + return ( + <> +

    3. How We Use Your Information

    +

    We use the information we collect in various ways, including to:

    +
      +
    • Provide, operate, and maintain our website and services.
    • +
    • Improve, personalize, and expand our website and services.
    • +
    • Understand and analyze how you use our website and services.
    • +
    • Develop new products, services, features, and functionality.
    • +
    • + Communicate with you, either directly or through one of our + partners, including for customer service, to provide you with + updates and other information relating to the website, and for + marketing and promotional purposes. +
    • +
    • + Process your transactions and send you related information, + including purchase confirmations and invoices. +
    • +
    • Find and prevent fraud.
    • +
    • Comply with legal obligations.
    • +
    + + ); + }; + + const DataProtection = () => { + return ( + <> +

    4. Data Protection and Security

    +

    + We implement a variety of security measures to maintain the safety of + your personal information. These measures include: +

    +
      +
    • + Encryption: We use encryption to protect sensitive information + transmitted online. Access Controls: We restrict access to your + personal data to authorized personnel only. Regular Security Audits: + We conduct regular audits to identify and address potential security + vulnerabilities. +
    • +
    + + ); + }; + + const Sharing = () => { + return ( + <> +

    5. Sharing Your Information

    +

    + We do not sell, trade, or otherwise transfer your Personally + Identifiable Information to outside parties without your consent, + except in the following cases: +

    +
      +
    • + Service Providers: We may share your information with third-party + service providers who perform services on our behalf, such as + payment processing, data analysis, email delivery, hosting services, + customer service, and marketing assistance. +
    • +
    • + Business Transfers: In the event of a merger, acquisition, or sale + of all or a portion of our assets, your information may be + transferred as part of that transaction. +
    • +
    • + Legal Requirements: We may disclose your information if required to + do so by law or in response to valid requests by public authorities + (e.g., a court or a government agency). +
    • +
    + + ); + }; + + const ProtectionRights = () => { + return ( + <> +

    6. Your Data Protection Rights

    +

    + Depending on your location, you may have the following rights + regarding your personal data: +

    +
      +
    • + The Right to Access: You have the right to request copies of your + personal data. +
    • +
    • + The Right to Rectification: You have the right to request that we + correct any information you believe is inaccurate or complete + information you believe is incomplete. +
    • +
    • + The Right to Erasure: You have the right to request that we erase + your personal data, under certain conditions. +
    • +
    • + The Right to Restrict Processing: You have the right to request that + we restrict the processing of your personal data, under certain + conditions. +
    • +
    • + The Right to Object to Processing: You have the right to object to + our processing of your personal data, under certain conditions. +
    • +
    • + The Right to Data Portability: You have the right to request that we + transfer the data that we have collected to another organization, or + directly to you, under certain conditions. +
    • +
    + + ); + }; + + const DataTransfers = () => { + return ( + <> +

    7. International Data Transfers

    +

    + Your information, including personal data, may be transferred to — and + maintained on — computers located outside of your state, province, + country, or other governmental jurisdiction where the data protection + laws may differ from those of your jurisdiction. We will take all + steps reasonably necessary to ensure that your data is treated + securely and in accordance with this Privacy Policy. +

    + + ); + }; + + const RetentionOfData = () => { + return ( + <> +

    8. Retention of Data

    +

    + We will retain your personal data only for as long as is necessary for + the purposes set out in this Privacy Policy. We will retain and use + your personal data to the extent necessary to comply with our legal + obligations, resolve disputes, and enforce our policies. +

    + + ); + }; + + const ChangePrivacy = () => { + return ( + <> +

    9. Changes to This Privacy Policy

    +

    + We may update our Privacy Policy from time to time. We will notify you + of any changes by posting the new Privacy Policy on this page. You are + advised to review this Privacy Policy periodically for any changes. + Changes to this Privacy Policy are effective when they are posted on + this page. +

    + + ); + }; + + const ContactUs = () => { + return ( + <> +

    10. Contact Us

    +

    + If you have any questions about this Privacy Policy, please contact + us: +

    +
    + By email:{' '} + [support@flatlogic.com] +
    +
    + By visiting this page on our website:{' '} + Contact Us +
    + + ); + }; + + return ( +
    + + {getPageTitle('Privacy Policy')} + + +
    +
    +
    +

    Privacy Policy

    + + + + + + + + + + +
    +
    +
    +
    + ); +} + +PrivacyPolicy.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/profile.tsx b/frontend/src/pages/profile.tsx new file mode 100644 index 0000000..efc9070 --- /dev/null +++ b/frontend/src/pages/profile.tsx @@ -0,0 +1,178 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import { ToastContainer, toast } from 'react-toastify'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; + +import CardBox from '../components/CardBox'; +import LayoutAuthenticated from '../layouts/Authenticated'; +import SectionMain from '../components/SectionMain'; +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../components/FormField'; +import BaseDivider from '../components/BaseDivider'; +import BaseButtons from '../components/BaseButtons'; +import BaseButton from '../components/BaseButton'; +import FormCheckRadio from '../components/FormCheckRadio'; +import FormCheckRadioGroup from '../components/FormCheckRadioGroup'; +import FormImagePicker from '../components/FormImagePicker'; +import { SwitchField } from '../components/SwitchField'; +import { SelectField } from '../components/SelectField'; + +import { update, fetch } from '../stores/users/usersSlice'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import { useRouter } from 'next/router'; +import { findMe } from '../stores/authSlice'; + +const EditUsers = () => { + const { currentUser, isFetching, token } = useAppSelector( + (state) => state.auth, + ); + const router = useRouter(); + const dispatch = useAppDispatch(); + const notify = (type, msg) => toast(msg, { type }); + const initVals = { + firstName: '', + lastName: '', + phoneNumber: '', + email: '', + app_role: '', + disabled: false, + avatar: [], + password: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + useEffect(() => { + if (currentUser?.id && typeof currentUser === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = currentUser[el]), + ); + + setInitialValues(newInitialVal); + } + }, [currentUser]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: currentUser.id, data })); + await dispatch(findMe()); + await router.push('/users/users-list'); + notify('success', 'Profile was updated!'); + }; + + return ( + <> + + {getPageTitle('Edit profile')} + + + + {''} + + + {currentUser?.avatar[0]?.publicUrl && ( +
    +
    + Avatar +
    +
    + )} + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/users/users-list')} + /> + + +
    +
    +
    + + ); +}; + +EditUsers.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default EditUsers; diff --git a/frontend/src/pages/qr_codes/[qr_codesId].tsx b/frontend/src/pages/qr_codes/[qr_codesId].tsx new file mode 100644 index 0000000..80eb646 --- /dev/null +++ b/frontend/src/pages/qr_codes/[qr_codesId].tsx @@ -0,0 +1,179 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/qr_codes/qr_codesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditQr_codes = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + code: '', + + video: null, + + valid_from: new Date(), + + valid_until: new Date(), + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { qr_codes } = useAppSelector((state) => state.qr_codes); + + const { qr_codesId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: qr_codesId })); + }, [qr_codesId]); + + useEffect(() => { + if (typeof qr_codes === 'object') { + setInitialValues(qr_codes); + } + }, [qr_codes]); + + useEffect(() => { + if (typeof qr_codes === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = qr_codes[el])); + + setInitialValues(newInitialVal); + } + }, [qr_codes]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: qr_codesId, data })); + await router.push('/qr_codes/qr_codes-list'); + }; + + return ( + <> + + {getPageTitle('Edit qr_codes')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + setInitialValues({ ...initialValues, valid_from: date }) + } + /> + + + + + setInitialValues({ ...initialValues, valid_until: date }) + } + /> + + + + + + + router.push('/qr_codes/qr_codes-list')} + /> + + +
    +
    +
    + + ); +}; + +EditQr_codes.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditQr_codes; diff --git a/frontend/src/pages/qr_codes/qr_codes-edit.tsx b/frontend/src/pages/qr_codes/qr_codes-edit.tsx new file mode 100644 index 0000000..163727b --- /dev/null +++ b/frontend/src/pages/qr_codes/qr_codes-edit.tsx @@ -0,0 +1,177 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/qr_codes/qr_codesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditQr_codesPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + code: '', + + video: null, + + valid_from: new Date(), + + valid_until: new Date(), + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { qr_codes } = useAppSelector((state) => state.qr_codes); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof qr_codes === 'object') { + setInitialValues(qr_codes); + } + }, [qr_codes]); + + useEffect(() => { + if (typeof qr_codes === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = qr_codes[el])); + setInitialValues(newInitialVal); + } + }, [qr_codes]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/qr_codes/qr_codes-list'); + }; + + return ( + <> + + {getPageTitle('Edit qr_codes')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + setInitialValues({ ...initialValues, valid_from: date }) + } + /> + + + + + setInitialValues({ ...initialValues, valid_until: date }) + } + /> + + + + + + + router.push('/qr_codes/qr_codes-list')} + /> + + +
    +
    +
    + + ); +}; + +EditQr_codesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditQr_codesPage; diff --git a/frontend/src/pages/qr_codes/qr_codes-list.tsx b/frontend/src/pages/qr_codes/qr_codes-list.tsx new file mode 100644 index 0000000..90e8173 --- /dev/null +++ b/frontend/src/pages/qr_codes/qr_codes-list.tsx @@ -0,0 +1,173 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableQr_codes from '../../components/Qr_codes/TableQr_codes'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/qr_codes/qr_codesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Qr_codesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'QRCode', title: 'code' }, + + { label: 'ValidFrom', title: 'valid_from', date: 'true' }, + { label: 'ValidUntil', title: 'valid_until', date: 'true' }, + + { label: 'Video', title: 'video' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_QR_CODES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getQr_codesCSV = async () => { + const response = await axios({ + url: '/qr_codes?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'qr_codesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Qr_codes')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    + +
    + Switch to Table +
    +
    + + + + +
    + + + + + ); +}; + +Qr_codesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Qr_codesTablesPage; diff --git a/frontend/src/pages/qr_codes/qr_codes-new.tsx b/frontend/src/pages/qr_codes/qr_codes-new.tsx new file mode 100644 index 0000000..af4596e --- /dev/null +++ b/frontend/src/pages/qr_codes/qr_codes-new.tsx @@ -0,0 +1,143 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/qr_codes/qr_codesSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + code: '', + + video: '', + + valid_from: '', + + valid_until: '', +}; + +const Qr_codesNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + // get from url params + const { dateRangeStart, dateRangeEnd } = router.query; + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/qr_codes/qr_codes-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + router.push('/qr_codes/qr_codes-list')} + /> + + +
    +
    +
    + + ); +}; + +Qr_codesNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Qr_codesNew; diff --git a/frontend/src/pages/qr_codes/qr_codes-table.tsx b/frontend/src/pages/qr_codes/qr_codes-table.tsx new file mode 100644 index 0000000..373c009 --- /dev/null +++ b/frontend/src/pages/qr_codes/qr_codes-table.tsx @@ -0,0 +1,172 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableQr_codes from '../../components/Qr_codes/TableQr_codes'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/qr_codes/qr_codesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Qr_codesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'QRCode', title: 'code' }, + + { label: 'ValidFrom', title: 'valid_from', date: 'true' }, + { label: 'ValidUntil', title: 'valid_until', date: 'true' }, + + { label: 'Video', title: 'video' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_QR_CODES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getQr_codesCSV = async () => { + const response = await axios({ + url: '/qr_codes?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'qr_codesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Qr_codes')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to calendar + +
    +
    + + + +
    + + + + + ); +}; + +Qr_codesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Qr_codesTablesPage; diff --git a/frontend/src/pages/qr_codes/qr_codes-view.tsx b/frontend/src/pages/qr_codes/qr_codes-view.tsx new file mode 100644 index 0000000..cb71155 --- /dev/null +++ b/frontend/src/pages/qr_codes/qr_codes-view.tsx @@ -0,0 +1,166 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/qr_codes/qr_codesSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +const Qr_codesView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { qr_codes } = useAppSelector((state) => state.qr_codes); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View qr_codes')} + + + + + + +
    +

    QRCode

    +

    {qr_codes?.code}

    +
    + +
    +

    Video

    + +

    {qr_codes?.video?.title ?? 'No data'}

    +
    + + + {qr_codes.valid_from ? ( + + ) : ( +

    No ValidFrom

    + )} +
    + + + {qr_codes.valid_until ? ( + + ) : ( +

    No ValidUntil

    + )} +
    + + <> +

    Access_logs QRCode

    + +
    + + + + + + + + {qr_codes.access_logs_qr_code && + Array.isArray(qr_codes.access_logs_qr_code) && + qr_codes.access_logs_qr_code.map((item: any) => ( + + router.push( + `/access_logs/access_logs-view/?id=${item.id}`, + ) + } + > + + + ))} + +
    AccessTime
    + {dataFormatter.dateTimeFormatter(item.access_time)} +
    +
    + {!qr_codes?.access_logs_qr_code?.length && ( +
    No data
    + )} +
    + + + + + router.push('/qr_codes/qr_codes-list')} + /> +
    +
    + + ); +}; + +Qr_codesView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Qr_codesView; diff --git a/frontend/src/pages/register.tsx b/frontend/src/pages/register.tsx new file mode 100644 index 0000000..40ab874 --- /dev/null +++ b/frontend/src/pages/register.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import type { ReactElement } from 'react'; +import { ToastContainer, toast } from 'react-toastify'; +import Head from 'next/head'; +import BaseButton from '../components/BaseButton'; +import CardBox from '../components/CardBox'; +import SectionFullScreen from '../components/SectionFullScreen'; +import LayoutGuest from '../layouts/Guest'; +import { Field, Form, Formik } from 'formik'; +import FormField from '../components/FormField'; +import BaseDivider from '../components/BaseDivider'; +import BaseButtons from '../components/BaseButtons'; +import { useRouter } from 'next/router'; +import { getPageTitle } from '../config'; + +import axios from 'axios'; + +export default function Register() { + const [loading, setLoading] = React.useState(false); + const router = useRouter(); + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const handleSubmit = async (value) => { + setLoading(true); + try { + const { data: response } = await axios.post('/auth/signup', value); + await router.push('/login'); + setLoading(false); + notify('success', 'Please check your email for verification link'); + } catch (error) { + setLoading(false); + console.log('error: ', error); + notify('error', 'Something was wrong. Try again'); + } + }; + + return ( + <> + + {getPageTitle('Login')} + + + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + +
    +
    +
    + + + ); +} + +Register.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/roles/[rolesId].tsx b/frontend/src/pages/roles/[rolesId].tsx new file mode 100644 index 0000000..3918191 --- /dev/null +++ b/frontend/src/pages/roles/[rolesId].tsx @@ -0,0 +1,137 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/roles/rolesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditRoles = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + + permissions: [], + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { roles } = useAppSelector((state) => state.roles); + + const { rolesId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: rolesId })); + }, [rolesId]); + + useEffect(() => { + if (typeof roles === 'object') { + setInitialValues(roles); + } + }, [roles]); + + useEffect(() => { + if (typeof roles === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = roles[el])); + + setInitialValues(newInitialVal); + } + }, [roles]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: rolesId, data })); + await router.push('/roles/roles-list'); + }; + + return ( + <> + + {getPageTitle('Edit roles')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + router.push('/roles/roles-list')} + /> + + +
    +
    +
    + + ); +}; + +EditRoles.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditRoles; diff --git a/frontend/src/pages/roles/roles-edit.tsx b/frontend/src/pages/roles/roles-edit.tsx new file mode 100644 index 0000000..88500cd --- /dev/null +++ b/frontend/src/pages/roles/roles-edit.tsx @@ -0,0 +1,135 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/roles/rolesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditRolesPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + + permissions: [], + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { roles } = useAppSelector((state) => state.roles); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof roles === 'object') { + setInitialValues(roles); + } + }, [roles]); + + useEffect(() => { + if (typeof roles === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = roles[el])); + setInitialValues(newInitialVal); + } + }, [roles]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/roles/roles-list'); + }; + + return ( + <> + + {getPageTitle('Edit roles')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + router.push('/roles/roles-list')} + /> + + +
    +
    +
    + + ); +}; + +EditRolesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditRolesPage; diff --git a/frontend/src/pages/roles/roles-list.tsx b/frontend/src/pages/roles/roles-list.tsx new file mode 100644 index 0000000..5c07566 --- /dev/null +++ b/frontend/src/pages/roles/roles-list.tsx @@ -0,0 +1,164 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableRoles from '../../components/Roles/TableRoles'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/roles/rolesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const RolesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + + { label: 'Permissions', title: 'permissions' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_ROLES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getRolesCSV = async () => { + const response = await axios({ + url: '/roles?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'rolesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Roles')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    +
    + + + + +
    + + + + + ); +}; + +RolesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default RolesTablesPage; diff --git a/frontend/src/pages/roles/roles-new.tsx b/frontend/src/pages/roles/roles-new.tsx new file mode 100644 index 0000000..1c068c1 --- /dev/null +++ b/frontend/src/pages/roles/roles-new.tsx @@ -0,0 +1,110 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/roles/rolesSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + name: '', + + permissions: [], +}; + +const RolesNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/roles/roles-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + router.push('/roles/roles-list')} + /> + + +
    +
    +
    + + ); +}; + +RolesNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default RolesNew; diff --git a/frontend/src/pages/roles/roles-table.tsx b/frontend/src/pages/roles/roles-table.tsx new file mode 100644 index 0000000..380cca9 --- /dev/null +++ b/frontend/src/pages/roles/roles-table.tsx @@ -0,0 +1,163 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableRoles from '../../components/Roles/TableRoles'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/roles/rolesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const RolesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + + { label: 'Permissions', title: 'permissions' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_ROLES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getRolesCSV = async () => { + const response = await axios({ + url: '/roles?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'rolesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Roles')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    +
    + + + +
    + + + + + ); +}; + +RolesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default RolesTablesPage; diff --git a/frontend/src/pages/roles/roles-view.tsx b/frontend/src/pages/roles/roles-view.tsx new file mode 100644 index 0000000..b7368ff --- /dev/null +++ b/frontend/src/pages/roles/roles-view.tsx @@ -0,0 +1,171 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/roles/rolesSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +const RolesView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { roles } = useAppSelector((state) => state.roles); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View roles')} + + + + + + +
    +

    Name

    +

    {roles?.name}

    +
    + + <> +

    Permissions

    + +
    + + + + + + + + {roles.permissions && + Array.isArray(roles.permissions) && + roles.permissions.map((item: any) => ( + + router.push( + `/permissions/permissions-view/?id=${item.id}`, + ) + } + > + + + ))} + +
    Name
    {item.name}
    +
    + {!roles?.permissions?.length && ( +
    No data
    + )} +
    + + + <> +

    Users App Role

    + +
    + + + + + + + + + + + + + + + + {roles.users_app_role && + Array.isArray(roles.users_app_role) && + roles.users_app_role.map((item: any) => ( + + router.push(`/users/users-view/?id=${item.id}`) + } + > + + + + + + + + + + + ))} + +
    First NameLast NamePhone NumberE-MailDisabled
    {item.firstName}{item.lastName}{item.phoneNumber}{item.email} + {dataFormatter.booleanFormatter(item.disabled)} +
    +
    + {!roles?.users_app_role?.length && ( +
    No data
    + )} +
    + + + + + router.push('/roles/roles-list')} + /> +
    +
    + + ); +}; + +RolesView.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default RolesView; diff --git a/frontend/src/pages/search.tsx b/frontend/src/pages/search.tsx new file mode 100644 index 0000000..f64cd5c --- /dev/null +++ b/frontend/src/pages/search.tsx @@ -0,0 +1,93 @@ +import React, { ReactElement, useEffect, useState } from 'react'; +import Head from 'next/head'; +import 'react-datepicker/dist/react-datepicker.css'; +import { useAppDispatch } from '../stores/hooks'; + +import { useRouter } from 'next/router'; +import LayoutAuthenticated from '../layouts/Authenticated'; +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; +import SectionMain from '../components/SectionMain'; +import CardBox from '../components/CardBox'; +import SearchResults from '../components/SearchResults'; +import LoadingSpinner from '../components/LoadingSpinner'; +import BaseButton from '../components/BaseButton'; +import BaseDivider from '../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import axios from 'axios'; + +const SearchView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const searchQuery = router.query.query; + const [loading, setLoading] = useState(false); + const [searchResults, setSearchResults] = useState([]); + + useEffect(() => { + dispatch(fetchData()); + }, [dispatch, searchQuery]); + + const fetchData = createAsyncThunk('/search', async () => { + setLoading(true); + try { + const response = await axios.post('/search', { searchQuery }); + setSearchResults(response.data); + setLoading(false); + return response.data; + } catch (error) { + console.error(error.response); + setLoading(false); + throw error; + } + }); + + const groupedResults = searchResults.reduce((acc, item) => { + const { tableName } = item; + acc[tableName] = acc[tableName] || []; + acc[tableName].push(item); + return acc; + }, {}); + + return ( + <> + + Search Result + + + + {''} + + + {loading ? ( + + ) : ( + + )} + + router.push('/dashboard')} + /> + + + + ); +}; + +SearchView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default SearchView; diff --git a/frontend/src/pages/tables.tsx b/frontend/src/pages/tables.tsx new file mode 100644 index 0000000..626dc0e --- /dev/null +++ b/frontend/src/pages/tables.tsx @@ -0,0 +1,37 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../components/CardBox'; +import LayoutAuthenticated from '../layouts/Authenticated'; +import SectionMain from '../components/SectionMain'; +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; +import TableSampleClients from '../components/TableSampleClients'; +import { getPageTitle } from '../config'; + +const TablesPage = () => { + return ( + <> + + {getPageTitle('Tables')} + + + + {''} + + + + + + + ); +}; + +TablesPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default TablesPage; diff --git a/frontend/src/pages/terms-of-use.tsx b/frontend/src/pages/terms-of-use.tsx new file mode 100644 index 0000000..f112040 --- /dev/null +++ b/frontend/src/pages/terms-of-use.tsx @@ -0,0 +1,205 @@ +import React, { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import LayoutGuest from '../layouts/Guest'; +import { getPageTitle } from '../config'; + +export default function PrivacyPolicy() { + const title = 'sev'; + const [projectUrl, setProjectUrl] = useState(''); + + useEffect(() => { + setProjectUrl(location.origin); + }, []); + + const Information = () => { + return ( + <> +

    1. Acceptance of Terms

    +
    +

    + By accessing and using our application, you agree to comply with and + be bound by these Terms of Use. If you do not agree to these terms, + please do not use the application. +

    +
    + + ); + }; + + const ChangesTerms = () => { + return ( + <> +

    2. Changes to Terms

    +

    + We reserve the right to modify these Terms of Use at any time. Any + changes will be effective immediately upon posting. Your continued use + of the application after any such changes constitutes your acceptance + of the new terms. +

    + + ); + }; + + const UseApplication = () => { + return ( + <> +

    3. Use of the Application

    +

    + You agree to use the application only for lawful purposes and in a way + that does not infringe the rights of, restrict, or inhibit anyone + else’s use and enjoyment of the application. Prohibited behavior + includes harassing or causing distress or inconvenience to any other + user, transmitting obscene or offensive content, or disrupting the + normal flow of dialogue within the application. +

    + + ); + }; + + const IntellectualProperty = () => { + return ( + <> +

    4. Intellectual Property

    +

    + All content included on the application, such as text, graphics, + logos, images, and software, is the property of {title} or its content + suppliers and protected by international copyright laws. Unauthorized + use of the content may violate copyright, trademark, and other laws. +

    + + ); + }; + + const UserContent = () => { + return ( + <> +

    5. User Content

    +

    + You are responsible for any content you upload, post, or otherwise + make available through the application. You grant {title}a worldwide, + irrevocable, non-exclusive, royalty-free license to use, reproduce, + modify, publish, and distribute such content for any purpose. +

    + + ); + }; + + const Privacy = () => { + return ( + <> +

    6. Privacy

    +

    + Your privacy is important to us. Please review our Privacy Policy to + understand our practices regarding the collection, use, and disclosure + of your personal information. +

    + + ); + }; + + const Liability = () => { + return ( + <> +

    7. Limitation of Liability

    +

    + The application is provided “as is” and “as available” without any + warranties of any kind, either express or implied. {title} + does not warrant that the application will be uninterrupted or + error-free. In no event shall {title} be liable for any damages + arising out of your use of the application. +

    + + ); + }; + + const Indemnification = () => { + return ( + <> +

    8. Indemnification

    +

    + You agree to indemnify, defend, and hold harmless {title}, its + officers, directors, employees, and agents from and against any + claims, liabilities, damages, losses, and expenses, including without + limitation reasonable legal and accounting fees, arising out of or in + any way connected with your access to or use of the application or + your violation of these Terms of Use. +

    + + ); + }; + + const Termination = () => { + return ( + <> +

    9. Termination

    +

    + We reserve the right to terminate or suspend your access to the + application at our sole discretion, without notice and without + liability, for any reason, including if we believe you have violated + these Terms of Use. +

    + + ); + }; + + const GoverningLaw = () => { + return ( + <> +

    10. Governing Law

    +

    + These Terms of Use are governed by and interpreted in accordance with + applicable laws, without regard to any conflict of law principles. You + agree to submit to the exclusive jurisdiction of the courts that have + authority to resolve any dispute arising from the use of the + application. +

    + + ); + }; + + const ContactUs = () => { + return ( + <> +

    11. Contact Information

    +

    + If you have any questions about these Terms of Use, please contact us + at:{' '} + [support@flatlogic.com] +

    + + ); + }; + + return ( +
    + + {getPageTitle('Terms of Use')} + + +
    +
    +
    +

    Terms of Use

    + + + + + + + + + + + + +
    +
    +
    +
    + ); +} + +PrivacyPolicy.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/users/[usersId].tsx b/frontend/src/pages/users/[usersId].tsx new file mode 100644 index 0000000..e16077f --- /dev/null +++ b/frontend/src/pages/users/[usersId].tsx @@ -0,0 +1,205 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/users/usersSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditUsers = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + firstName: '', + + lastName: '', + + phoneNumber: '', + + email: '', + + disabled: false, + + avatar: [], + + app_role: null, + + custom_permissions: [], + + password: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { users } = useAppSelector((state) => state.users); + + const { usersId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: usersId })); + }, [usersId]); + + useEffect(() => { + if (typeof users === 'object') { + setInitialValues(users); + } + }, [users]); + + useEffect(() => { + if (typeof users === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = users[el])); + + setInitialValues(newInitialVal); + } + }, [users]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: usersId, data })); + await router.push('/users/users-list'); + }; + + return ( + <> + + {getPageTitle('Edit users')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/users/users-list')} + /> + + +
    +
    +
    + + ); +}; + +EditUsers.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditUsers; diff --git a/frontend/src/pages/users/users-edit.tsx b/frontend/src/pages/users/users-edit.tsx new file mode 100644 index 0000000..87d7090 --- /dev/null +++ b/frontend/src/pages/users/users-edit.tsx @@ -0,0 +1,203 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/users/usersSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditUsersPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + firstName: '', + + lastName: '', + + phoneNumber: '', + + email: '', + + disabled: false, + + avatar: [], + + app_role: null, + + custom_permissions: [], + + password: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { users } = useAppSelector((state) => state.users); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof users === 'object') { + setInitialValues(users); + } + }, [users]); + + useEffect(() => { + if (typeof users === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = users[el])); + setInitialValues(newInitialVal); + } + }, [users]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/users/users-list'); + }; + + return ( + <> + + {getPageTitle('Edit users')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/users/users-list')} + /> + + +
    +
    +
    + + ); +}; + +EditUsersPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditUsersPage; diff --git a/frontend/src/pages/users/users-list.tsx b/frontend/src/pages/users/users-list.tsx new file mode 100644 index 0000000..6d03ebc --- /dev/null +++ b/frontend/src/pages/users/users-list.tsx @@ -0,0 +1,173 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableUsers from '../../components/Users/TableUsers'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/users/usersSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const UsersTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'First Name', title: 'firstName' }, + { label: 'Last Name', title: 'lastName' }, + { label: 'Phone Number', title: 'phoneNumber' }, + { label: 'E-Mail', title: 'email' }, + + { label: 'App Role', title: 'app_role' }, + + { label: 'Custom Permissions', title: 'custom_permissions' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_USERS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getUsersCSV = async () => { + const response = await axios({ + url: '/users?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'usersCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Users')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    +
    + + + + +
    + + + + + ); +}; + +UsersTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default UsersTablesPage; diff --git a/frontend/src/pages/users/users-new.tsx b/frontend/src/pages/users/users-new.tsx new file mode 100644 index 0000000..4f3fa45 --- /dev/null +++ b/frontend/src/pages/users/users-new.tsx @@ -0,0 +1,171 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/users/usersSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + firstName: '', + + lastName: '', + + phoneNumber: '', + + email: '', + + disabled: false, + + avatar: [], + + app_role: '', + + custom_permissions: [], +}; + +const UsersNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/users/users-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/users/users-list')} + /> + + +
    +
    +
    + + ); +}; + +UsersNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default UsersNew; diff --git a/frontend/src/pages/users/users-table.tsx b/frontend/src/pages/users/users-table.tsx new file mode 100644 index 0000000..a408cd6 --- /dev/null +++ b/frontend/src/pages/users/users-table.tsx @@ -0,0 +1,168 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableUsers from '../../components/Users/TableUsers'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/users/usersSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const UsersTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'First Name', title: 'firstName' }, + { label: 'Last Name', title: 'lastName' }, + { label: 'Phone Number', title: 'phoneNumber' }, + { label: 'E-Mail', title: 'email' }, + + { label: 'App Role', title: 'app_role' }, + + { label: 'Custom Permissions', title: 'custom_permissions' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_USERS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getUsersCSV = async () => { + const response = await axios({ + url: '/users?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'usersCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Users')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    +
    + + + +
    + + + + + ); +}; + +UsersTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default UsersTablesPage; diff --git a/frontend/src/pages/users/users-view.tsx b/frontend/src/pages/users/users-view.tsx new file mode 100644 index 0000000..5a5906e --- /dev/null +++ b/frontend/src/pages/users/users-view.tsx @@ -0,0 +1,199 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/users/usersSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +const UsersView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { users } = useAppSelector((state) => state.users); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View users')} + + + + + + +
    +

    First Name

    +

    {users?.firstName}

    +
    + +
    +

    Last Name

    +

    {users?.lastName}

    +
    + +
    +

    Phone Number

    +

    {users?.phoneNumber}

    +
    + +
    +

    E-Mail

    +

    {users?.email}

    +
    + + + null }} + disabled + /> + + +
    +

    Avatar

    + {users?.avatar?.length ? ( + + ) : ( +

    No Avatar

    + )} +
    + +
    +

    App Role

    + +

    {users?.app_role?.name ?? 'No data'}

    +
    + + <> +

    Custom Permissions

    + +
    + + + + + + + + {users.custom_permissions && + Array.isArray(users.custom_permissions) && + users.custom_permissions.map((item: any) => ( + + router.push( + `/permissions/permissions-view/?id=${item.id}`, + ) + } + > + + + ))} + +
    Name
    {item.name}
    +
    + {!users?.custom_permissions?.length && ( +
    No data
    + )} +
    + + + <> +

    Access_logs User

    + +
    + + + + + + + + {users.access_logs_user && + Array.isArray(users.access_logs_user) && + users.access_logs_user.map((item: any) => ( + + router.push( + `/access_logs/access_logs-view/?id=${item.id}`, + ) + } + > + + + ))} + +
    AccessTime
    + {dataFormatter.dateTimeFormatter(item.access_time)} +
    +
    + {!users?.access_logs_user?.length && ( +
    No data
    + )} +
    + + + + + router.push('/users/users-list')} + /> +
    +
    + + ); +}; + +UsersView.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default UsersView; diff --git a/frontend/src/pages/verify-email.tsx b/frontend/src/pages/verify-email.tsx new file mode 100644 index 0000000..a088720 --- /dev/null +++ b/frontend/src/pages/verify-email.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import type { ReactElement } from 'react'; +import { ToastContainer, toast } from 'react-toastify'; +import Head from 'next/head'; +import CardBox from '../components/CardBox'; +import SectionFullScreen from '../components/SectionFullScreen'; +import LayoutGuest from '../layouts/Guest'; +import { useRouter } from 'next/router'; +import { getPageTitle } from '../config'; +import axios from 'axios'; + +export default function Verify() { + const [loading, setLoading] = React.useState(false); + const router = useRouter(); + const { token } = router.query; + const notify = (type, msg) => toast(msg, { type }); + + React.useEffect(() => { + if (!token) { + router.push('/login'); + return; + } + const handleSubmit = async () => { + setLoading(true); + await axios + .put('/auth/verify-email', { + token, + }) + .then((verified) => { + if (verified) { + setLoading(false); + notify('success', 'Your email was verified'); + } + }) + .catch((error) => { + setLoading(false); + console.log('error: ', error); + notify('error', error.response); + }) + .finally(async () => { + await router.push('/login'); + }); + }; + handleSubmit().then(); + }, [token]); + + return ( + <> + + {getPageTitle('Verify Email')} + + + +

    {loading ? 'Loading...' : ''}

    +
    +
    + + + + ); +} + +Verify.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/videos/[videosId].tsx b/frontend/src/pages/videos/[videosId].tsx new file mode 100644 index 0000000..e823315 --- /dev/null +++ b/frontend/src/pages/videos/[videosId].tsx @@ -0,0 +1,140 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/videos/videosSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditVideos = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + title: '', + + url: '', + + status: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { videos } = useAppSelector((state) => state.videos); + + const { videosId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: videosId })); + }, [videosId]); + + useEffect(() => { + if (typeof videos === 'object') { + setInitialValues(videos); + } + }, [videos]); + + useEffect(() => { + if (typeof videos === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = videos[el])); + + setInitialValues(newInitialVal); + } + }, [videos]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: videosId, data })); + await router.push('/videos/videos-list'); + }; + + return ( + <> + + {getPageTitle('Edit videos')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + router.push('/videos/videos-list')} + /> + + +
    +
    +
    + + ); +}; + +EditVideos.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditVideos; diff --git a/frontend/src/pages/videos/videos-edit.tsx b/frontend/src/pages/videos/videos-edit.tsx new file mode 100644 index 0000000..1518afd --- /dev/null +++ b/frontend/src/pages/videos/videos-edit.tsx @@ -0,0 +1,138 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/videos/videosSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditVideosPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + title: '', + + url: '', + + status: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { videos } = useAppSelector((state) => state.videos); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof videos === 'object') { + setInitialValues(videos); + } + }, [videos]); + + useEffect(() => { + if (typeof videos === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = videos[el])); + setInitialValues(newInitialVal); + } + }, [videos]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/videos/videos-list'); + }; + + return ( + <> + + {getPageTitle('Edit videos')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + router.push('/videos/videos-list')} + /> + + +
    +
    +
    + + ); +}; + +EditVideosPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditVideosPage; diff --git a/frontend/src/pages/videos/videos-list.tsx b/frontend/src/pages/videos/videos-list.tsx new file mode 100644 index 0000000..3bf546a --- /dev/null +++ b/frontend/src/pages/videos/videos-list.tsx @@ -0,0 +1,170 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableVideos from '../../components/Videos/TableVideos'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/videos/videosSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const VideosTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Title', title: 'title' }, + { label: 'URL', title: 'url' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['active', 'inactive'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_VIDEOS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getVideosCSV = async () => { + const response = await axios({ + url: '/videos?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'videosCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Videos')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    +
    +
    + + + + +
    + + + + + ); +}; + +VideosTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default VideosTablesPage; diff --git a/frontend/src/pages/videos/videos-new.tsx b/frontend/src/pages/videos/videos-new.tsx new file mode 100644 index 0000000..f77028d --- /dev/null +++ b/frontend/src/pages/videos/videos-new.tsx @@ -0,0 +1,114 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/videos/videosSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + title: '', + + url: '', + + status: 'active', +}; + +const VideosNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/videos/videos-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
    + + + + + + + + + + + + + + + + + + + + + router.push('/videos/videos-list')} + /> + + +
    +
    +
    + + ); +}; + +VideosNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default VideosNew; diff --git a/frontend/src/pages/videos/videos-table.tsx b/frontend/src/pages/videos/videos-table.tsx new file mode 100644 index 0000000..7b44285 --- /dev/null +++ b/frontend/src/pages/videos/videos-table.tsx @@ -0,0 +1,173 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableVideos from '../../components/Videos/TableVideos'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/videos/videosSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const VideosTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Title', title: 'title' }, + { label: 'URL', title: 'url' }, + + { + label: 'Status', + title: 'status', + type: 'enum', + options: ['active', 'inactive'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_VIDEOS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getVideosCSV = async () => { + const response = await axios({ + url: '/videos?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'videosCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Videos')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
    +
    + + + Back to table + +
    +
    + + + +
    + + + + + ); +}; + +VideosTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default VideosTablesPage; diff --git a/frontend/src/pages/videos/videos-view.tsx b/frontend/src/pages/videos/videos-view.tsx new file mode 100644 index 0000000..f610817 --- /dev/null +++ b/frontend/src/pages/videos/videos-view.tsx @@ -0,0 +1,140 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/videos/videosSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +const VideosView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { videos } = useAppSelector((state) => state.videos); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View videos')} + + + + + + +
    +

    Title

    +

    {videos?.title}

    +
    + +
    +

    URL

    +

    {videos?.url}

    +
    + +
    +

    Status

    +

    {videos?.status ?? 'No data'}

    +
    + + <> +

    Qr_codes Video

    + +
    + + + + + + + + + + + + {videos.qr_codes_video && + Array.isArray(videos.qr_codes_video) && + videos.qr_codes_video.map((item: any) => ( + + router.push( + `/qr_codes/qr_codes-view/?id=${item.id}`, + ) + } + > + + + + + + + ))} + +
    QRCodeValidFromValidUntil
    {item.code} + {dataFormatter.dateTimeFormatter(item.valid_from)} + + {dataFormatter.dateTimeFormatter(item.valid_until)} +
    +
    + {!videos?.qr_codes_video?.length && ( +
    No data
    + )} +
    + + + + + router.push('/videos/videos-list')} + /> +
    +
    + + ); +}; + +VideosView.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default VideosView; diff --git a/frontend/src/pages/web_pages/about.tsx b/frontend/src/pages/web_pages/about.tsx new file mode 100644 index 0000000..3982531 --- /dev/null +++ b/frontend/src/pages/web_pages/about.tsx @@ -0,0 +1,121 @@ +import React, { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import Link from 'next/link'; +import { useAppSelector } from '../../stores/hooks'; +import LayoutGuest from '../../layouts/Guest'; +import WebSiteHeader from '../../components/WebPageComponents/Header'; +import WebSiteFooter from '../../components/WebPageComponents/Footer'; +import { + HeroDesigns, + AboutUsDesigns, + FeaturesDesigns, +} from '../../components/WebPageComponents/designs'; + +import HeroSection from '../../components/WebPageComponents/HeroComponent'; + +import AboutUsSection from '../../components/WebPageComponents/AboutUsComponent'; + +import FeaturesSection from '../../components/WebPageComponents/FeaturesComponent'; + +export default function WebSite() { + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const projectName = 'sev'; + + useEffect(() => { + const darkElement = document.querySelector('body .dark'); + if (darkElement) { + darkElement.classList.remove('dark'); + } + }, []); + const pages = [ + { + href: '/home', + label: 'home', + }, + + { + href: '/about', + label: 'about', + }, + + { + href: '/contact', + label: 'contact', + }, + + { + href: '/faq', + label: 'FAQ', + }, + ]; + + const features_points = [ + { + name: 'Advanced QR Technology', + description: + 'Utilize cutting-edge QR code technology to ensure secure and efficient access to video content. Our system is designed for reliability and ease of use.', + icon: 'mdiQrcode', + }, + { + name: 'User-Friendly Interface', + description: + 'Experience a seamless and intuitive interface that makes navigating and accessing content a breeze for all users, regardless of technical expertise.', + icon: 'mdiAccountCircle', + }, + { + name: 'Comprehensive Admin Tools', + description: + 'Empower administrators with robust tools to manage content, monitor access, and customize settings, ensuring a tailored experience for every user.', + icon: 'mdiCogOutline', + }, + ]; + + return ( +
    + + {`About Sevastopol QR Verification`} + + + +
    + + + + + +
    + +
    + ); +} + +WebSite.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/web_pages/contact.tsx b/frontend/src/pages/web_pages/contact.tsx new file mode 100644 index 0000000..58900f7 --- /dev/null +++ b/frontend/src/pages/web_pages/contact.tsx @@ -0,0 +1,86 @@ +import React, { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import Link from 'next/link'; +import { useAppSelector } from '../../stores/hooks'; +import LayoutGuest from '../../layouts/Guest'; +import WebSiteHeader from '../../components/WebPageComponents/Header'; +import WebSiteFooter from '../../components/WebPageComponents/Footer'; +import { + HeroDesigns, + ContactFormDesigns, +} from '../../components/WebPageComponents/designs'; + +import HeroSection from '../../components/WebPageComponents/HeroComponent'; + +import ContactFormSection from '../../components/WebPageComponents/ContactFormComponent'; + +export default function WebSite() { + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const projectName = 'sev'; + + useEffect(() => { + const darkElement = document.querySelector('body .dark'); + if (darkElement) { + darkElement.classList.remove('dark'); + } + }, []); + const pages = [ + { + href: '/home', + label: 'home', + }, + + { + href: '/about', + label: 'about', + }, + + { + href: '/contact', + label: 'contact', + }, + + { + href: '/faq', + label: 'FAQ', + }, + ]; + + return ( +
    + + {`Contact Us - Sevastopol QR Verification`} + + + +
    + + + +
    + +
    + ); +} + +WebSite.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/web_pages/faq.tsx b/frontend/src/pages/web_pages/faq.tsx new file mode 100644 index 0000000..f6f8209 --- /dev/null +++ b/frontend/src/pages/web_pages/faq.tsx @@ -0,0 +1,123 @@ +import React, { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import Link from 'next/link'; +import { useAppSelector } from '../../stores/hooks'; +import LayoutGuest from '../../layouts/Guest'; +import WebSiteHeader from '../../components/WebPageComponents/Header'; +import WebSiteFooter from '../../components/WebPageComponents/Footer'; +import { + HeroDesigns, + FaqDesigns, +} from '../../components/WebPageComponents/designs'; + +import HeroSection from '../../components/WebPageComponents/HeroComponent'; + +import FaqSection from '../../components/WebPageComponents/FaqComponent'; + +export default function WebSite() { + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const projectName = 'sev'; + + useEffect(() => { + const darkElement = document.querySelector('body .dark'); + if (darkElement) { + darkElement.classList.remove('dark'); + } + }, []); + const pages = [ + { + href: '/home', + label: 'home', + }, + + { + href: '/about', + label: 'about', + }, + + { + href: '/contact', + label: 'contact', + }, + + { + href: '/faq', + label: 'FAQ', + }, + ]; + + const faqs = [ + { + question: 'How does the QR code verification work?', + answer: + "Our QR code verification system allows users to scan a code using their device's camera to access exclusive video content. Once scanned, the system verifies the code and grants temporary access to the video.", + }, + { + question: 'What happens if the QR code is invalid?', + answer: + 'If an invalid QR code is scanned, the system will notify the user and prompt them to scan a valid code. This ensures that only authorized users can access the content.', + }, + { + question: 'How long can I view the video after scanning?', + answer: + 'After successfully scanning a QR code, users can view the video for a limited time, typically set to 2 minutes. This duration can be adjusted by the admin based on specific requirements.', + }, + { + question: 'Can I manage the video content as an admin?', + answer: + 'Yes, admins have full control over the video content and QR code settings. They can add, remove, or update videos and manage user access through the admin panel.', + }, + { + question: 'Is there a way to track user access?', + answer: + 'Yes, the system logs user access and activity, allowing admins to monitor who accessed the content and when. This helps in maintaining security and understanding user engagement.', + }, + { + question: 'What if I encounter issues with the QR scanner?', + answer: + "If you face any issues with the QR scanner, ensure that your device's camera is functioning properly and that you have granted the necessary permissions. If problems persist, contact our support team for assistance.", + }, + { + question: 'Can I use the system on multiple devices?', + answer: + 'Yes, the system is designed to be compatible with various devices, including smartphones, tablets, and computers, ensuring a seamless experience across platforms.', + }, + ]; + + return ( +
    + + {`Frequently Asked Questions - Sevastopol QR Verification`} + + + +
    + + + +
    + +
    + ); +} + +WebSite.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/pages/web_pages/home.tsx b/frontend/src/pages/web_pages/home.tsx new file mode 100644 index 0000000..45f7610 --- /dev/null +++ b/frontend/src/pages/web_pages/home.tsx @@ -0,0 +1,132 @@ +import React, { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import Link from 'next/link'; +import { useAppSelector } from '../../stores/hooks'; +import LayoutGuest from '../../layouts/Guest'; +import WebSiteHeader from '../../components/WebPageComponents/Header'; +import WebSiteFooter from '../../components/WebPageComponents/Footer'; +import { + ContactFormDesigns, + HeroDesigns, + FeaturesDesigns, + AboutUsDesigns, +} from '../../components/WebPageComponents/designs'; + +import ContactFormSection from '../../components/WebPageComponents/ContactFormComponent'; + +import HeroSection from '../../components/WebPageComponents/HeroComponent'; + +import FeaturesSection from '../../components/WebPageComponents/FeaturesComponent'; + +import AboutUsSection from '../../components/WebPageComponents/AboutUsComponent'; + +export default function WebSite() { + const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const projectName = 'sev'; + + useEffect(() => { + const darkElement = document.querySelector('body .dark'); + if (darkElement) { + darkElement.classList.remove('dark'); + } + }, []); + const pages = [ + { + href: '/home', + label: 'home', + }, + + { + href: '/about', + label: 'about', + }, + + { + href: '/contact', + label: 'contact', + }, + + { + href: '/faq', + label: 'FAQ', + }, + ]; + + const features_points = [ + { + name: 'Seamless QR Scanning', + description: + 'Effortlessly scan QR codes to unlock exclusive video content. Our system ensures quick and accurate scanning for a smooth user experience.', + icon: 'mdiQrcodeScan', + }, + { + name: 'Timed Video Access', + description: + 'Enjoy video content for a limited time after scanning. Our custom timer ensures you make the most of your viewing session.', + icon: 'mdiTimer', + }, + { + name: 'Admin Control Panel', + description: + 'Manage video content and QR settings with ease. The admin panel provides full control over user access and content management.', + icon: 'mdiAccountCog', + }, + ]; + + return ( +
    + + {`Welcome to Sevastopol QR Verification`} + + + +
    + + + + + + + +
    + +
    + ); +} + +WebSite.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; diff --git a/frontend/src/stores/access_logs/access_logsSlice.ts b/frontend/src/stores/access_logs/access_logsSlice.ts new file mode 100644 index 0000000..0c7585f --- /dev/null +++ b/frontend/src/stores/access_logs/access_logsSlice.ts @@ -0,0 +1,241 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + access_logs: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + access_logs: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk( + 'access_logs/fetch', + async (data: any) => { + const { id, query } = data; + const result = await axios.get( + `access_logs${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'access_logs/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('access_logs/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'access_logs/deleteAccess_logs', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`access_logs/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'access_logs/createAccess_logs', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('access_logs', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'access_logs/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('access_logs/bulk-import', data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const update = createAsyncThunk( + 'access_logs/updateAccess_logs', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`access_logs/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const access_logsSlice = createSlice({ + name: 'access_logs', + initialState, + reducers: { + setRefetch: (state, action: PayloadAction) => { + state.refetch = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(fetch.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(fetch.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(fetch.fulfilled, (state, action) => { + if (action.payload.rows && action.payload.count >= 0) { + state.access_logs = action.payload.rows; + state.count = action.payload.count; + } else { + state.access_logs = action.payload; + } + state.loading = false; + }); + + builder.addCase(deleteItemsByIds.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItemsByIds.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Access_logs has been deleted'); + }); + + builder.addCase(deleteItemsByIds.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Access_logs'.slice(0, -1)} has been deleted`); + }); + + builder.addCase(deleteItem.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(create.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Access_logs'.slice(0, -1)} has been created`); + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Access_logs'.slice(0, -1)} has been updated`); + }); + builder.addCase(update.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(uploadCsv.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(uploadCsv.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Access_logs has been uploaded'); + }); + builder.addCase(uploadCsv.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { setRefetch } = access_logsSlice.actions; + +export default access_logsSlice.reducer; diff --git a/frontend/src/stores/authSlice.ts b/frontend/src/stores/authSlice.ts new file mode 100644 index 0000000..1d405da --- /dev/null +++ b/frontend/src/stores/authSlice.ts @@ -0,0 +1,125 @@ +import { createSlice, createAsyncThunk, createAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import jwt from 'jsonwebtoken'; + +interface MainState { + isFetching: boolean; + errorMessage: string; + currentUser: any; + notify: any; + token: string; +} + +const initialState: MainState = { + /* User */ + isFetching: false, + errorMessage: '', + currentUser: null, + token: '', + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const resetAction = createAction('auth/passwordReset/reset'); + +export const loginUser = createAsyncThunk( + 'auth/loginUser', + async (creds: Record, { rejectWithValue }) => { + try { + const response = await axios.post('auth/signin/local', creds); + return response.data; + } catch (error) { + if (!error.response) { + throw error; + } + return rejectWithValue(error.response.data); + } + }, +); + +export const passwordReset = createAsyncThunk( + 'auth/passwordReset', + async (value: Record, { rejectWithValue }) => { + try { + const { data: response } = await axios.put('/auth/password-reset', { + token: value.token, + password: value.password, + type: value.type, + }); + + return response.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const findMe = createAsyncThunk('auth/findMe', async () => { + const response = await axios.get('auth/me'); + return response.data; +}); + +export const authSlice = createSlice({ + name: 'auth', + initialState, + reducers: { + logoutUser: (state) => { + localStorage.removeItem('token'); + localStorage.removeItem('user'); + axios.defaults.headers.common['Authorization'] = ''; + state.currentUser = null; + state.token = ''; + }, + }, + extraReducers: (builder) => { + builder.addCase(loginUser.pending, (state) => { + state.isFetching = true; + }); + builder.addCase(loginUser.fulfilled, (state, action) => { + const token = action.payload; + const user = jwt.decode(token); + + state.errorMessage = ''; + state.token = token; + localStorage.setItem('token', token); + localStorage.setItem('user', JSON.stringify(user)); + axios.defaults.headers.common['Authorization'] = 'Bearer ' + token; + }); + + builder.addCase(loginUser.rejected, (state, action) => { + state.errorMessage = + String(action.payload) || 'Something went wrong. Try again'; + state.isFetching = false; + }); + builder.addCase(findMe.pending, () => { + console.log('Pending findMe'); + }); + builder.addCase(findMe.fulfilled, (state, action) => { + state.currentUser = action.payload; + state.isFetching = false; + }); + + builder.addCase(passwordReset.fulfilled, (state, action) => { + state.notify.showNotification = true; + state.notify.textNotification = 'Password has been reset successfully'; + }); + + builder.addCase(resetAction, (state) => initialState); + + builder.addCase(passwordReset.rejected, (state) => { + state.errorMessage = 'Something was wrong. Try again'; + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { logoutUser } = authSlice.actions; + +export default authSlice.reducer; diff --git a/frontend/src/stores/hooks.ts b/frontend/src/stores/hooks.ts new file mode 100644 index 0000000..beb95c0 --- /dev/null +++ b/frontend/src/stores/hooks.ts @@ -0,0 +1,6 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import type { RootState, AppDispatch } from './store'; + +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useAppDispatch: () => AppDispatch = useDispatch; +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/frontend/src/stores/introSteps.ts b/frontend/src/stores/introSteps.ts new file mode 100644 index 0000000..bb91ae3 --- /dev/null +++ b/frontend/src/stores/introSteps.ts @@ -0,0 +1,152 @@ +interface Step { + element: string; + intro: string; + position?: string; + tooltipClass?: string; + highlightClass?: string; + disableInteraction?: boolean; +} + +interface Hint { + element: string; + hint: string; + hintPosition?: string; +} + +export const landingSteps: Step[] = [ + { + element: '#elementId1', + intro: ` +
    + Description +

    Welcome to our app tutorial! Get a sneak peek into the key functionalities and learn how to navigate seamlessly. Here's a quick overview to get you started.

    +
    + `, + position: 'auto', + tooltipClass: ' good-img', + }, + { + element: '#websiteHeader', + intro: + "You can switch between different sections of the app using this header. It's your gateway to exploring all the available pages.", + position: 'auto', + tooltipClass: ' right-0 mx-auto rounded shadow-lg', + disableInteraction: true, + }, + { + element: '#loginButton', + intro: + 'Decide whether to explore the landing page or proceed to the login. You can always return to the landing page later.', + disableInteraction: true, + }, +]; + +export const loginSteps: Step[] = [ + { + element: '#loginRoles', + intro: + 'Choose your login role to proceed. Experience the app as Admin, or User, or create your own account to get started.', + position: 'auto', + }, +]; + +export const appSteps: Step[] = [ + { + element: '#profilEdit', + intro: + "Update your profile information, including name, email, and password. Don't forget to save your changes to keep your profile current.", + position: 'auto', + disableInteraction: true, + }, + { + element: '#themeToggle', + intro: 'Switch between light and dark modes to suit your preference.', + position: 'auto', + disableInteraction: true, + }, + { + element: '#logout', + intro: 'Log out or switch users/roles with ease to manage your access.', + position: 'auto', + disableInteraction: true, + }, + { + element: '#search', + intro: + 'Quickly find specific data or items by entering your query in the search field. Navigate directly to the desired element.', + position: 'auto', + disableInteraction: true, + }, + { + element: '#widgetCreator', + intro: + 'Use Text-to-Chart and Text-to-Widget to create charts or widgets from text descriptions. Type what you need, like "Orders by Month," and customize your dashboard.', + position: 'auto', + disableInteraction: true, + }, + { + element: '#dashboard', + intro: + 'View all the entities available to your role, offering insights into the data categories and total items in each.', + position: 'auto', + disableInteraction: true, + }, + { + element: '#asideMenu', + intro: + 'Access various entities and manage your data. Find links to the landing page and Swagger API documentation for more information.', + position: 'auto', + disableInteraction: true, + }, + { + element: '#asideMenu', + intro: "Let's explore the User entity.", + position: 'auto', + disableInteraction: true, + }, +]; + +export const usersSteps: Step[] = [ + { + element: '#usersList', + intro: + 'Invite new users, filter data, and work with CSV files in this section.', + position: 'auto', + disableInteraction: true, + }, + { + element: '#usersTable', + intro: + 'View, modify, or delete items with the necessary permissions. Inline editing is available within the table.', + position: 'auto', + disableInteraction: true, + }, + { + element: '#asideMenu', + intro: "Let's explore the Roles entity.", + position: 'auto', + disableInteraction: true, + }, +]; + +export const rolesSteps: Step[] = [ + { + element: '#rolesTable', + intro: + 'Super Admin can manage roles and permissions. Adjust access levels and permissions for each role or user in the Roles and Permissions sections.', + position: 'auto', + disableInteraction: true, + }, + { + element: '#feedbackSection', + intro: ` +
    + Description +

    Thank you for completing the tour! We hope you now have a better understanding of the app.

    +

    If you have any questions, feel free to reach out to us at support@flatlogic.com.

    +
    + `, + position: 'auto', + tooltipClass: 'end-img', + }, +]; diff --git a/frontend/src/stores/mainSlice.ts b/frontend/src/stores/mainSlice.ts new file mode 100644 index 0000000..92b8935 --- /dev/null +++ b/frontend/src/stores/mainSlice.ts @@ -0,0 +1,36 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { UserPayloadObject } from '../interfaces'; + +interface MainState { + userName: string; + userEmail: null | string; + userAvatar: null | string; + isFieldFocusRegistered: boolean; +} + +const initialState: MainState = { + /* User */ + userName: '', + userEmail: null, + userAvatar: null, + + /* Field focus with ctrl+k (to register only once) */ + isFieldFocusRegistered: false, +}; + +export const mainSlice = createSlice({ + name: 'main', + initialState, + reducers: { + setUser: (state, action: PayloadAction) => { + state.userName = action.payload.name; + state.userEmail = action.payload.email; + state.userAvatar = action.payload.avatar; + }, + }, +}); + +// Action creators are generated for each case reducer function +export const { setUser } = mainSlice.actions; + +export default mainSlice.reducer; diff --git a/frontend/src/stores/openAiSlice.ts b/frontend/src/stores/openAiSlice.ts new file mode 100644 index 0000000..7299beb --- /dev/null +++ b/frontend/src/stores/openAiSlice.ts @@ -0,0 +1,79 @@ +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import axios from 'axios'; + +interface MainState { + isFetchingQuery: boolean; + errorMessage: string; + smartWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} +const initialState: MainState = { + isFetchingQuery: false, + errorMessage: '', + smartWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +const fulfilledNotify = (state, msg, type?: string) => { + state.notify.textNotification = msg; + state.notify.typeNotification = type || 'success'; + state.notify.showNotification = true; +}; + +export const aiPrompt = createAsyncThunk( + 'openai/aiPrompt', + async (data: any, { rejectWithValue }) => { + try { + return await axios.post('/openai/create_widget', data); + } catch (error) { + if (!error.response) { + throw error; + } + return rejectWithValue(error.response.data); + } + }, +); + +export const openAiSlice = createSlice({ + name: 'openAiSlice', + initialState, + reducers: { + resetNotify: (state) => { + state.notify.showNotification = false; + state.notify.typeNotification = ''; + state.notify.textNotification = ''; + }, + setErrorNotification: (state, action) => { + fulfilledNotify(state, action.payload, 'error'); + }, + }, + extraReducers: (builder) => { + builder.addCase(aiPrompt.pending, (state) => { + state.isFetchingQuery = true; + }); + builder.addCase(aiPrompt.fulfilled, (state, action: Record) => { + state.isFetchingQuery = false; + state.errorMessage = ''; + state.smartWidgets.unshift(action.payload.data); + }); + + builder.addCase(aiPrompt.rejected, (state) => { + state.errorMessage = 'Something was wrong. Try again'; + state.isFetchingQuery = false; + state.smartWidgets = null; + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { resetNotify, setErrorNotification } = openAiSlice.actions; + +export default openAiSlice.reducer; diff --git a/frontend/src/stores/permissions/permissionsSlice.ts b/frontend/src/stores/permissions/permissionsSlice.ts new file mode 100644 index 0000000..11b8224 --- /dev/null +++ b/frontend/src/stores/permissions/permissionsSlice.ts @@ -0,0 +1,241 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + permissions: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + permissions: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk( + 'permissions/fetch', + async (data: any) => { + const { id, query } = data; + const result = await axios.get( + `permissions${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'permissions/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('permissions/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'permissions/deletePermissions', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`permissions/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'permissions/createPermissions', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('permissions', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'permissions/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('permissions/bulk-import', data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const update = createAsyncThunk( + 'permissions/updatePermissions', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`permissions/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const permissionsSlice = createSlice({ + name: 'permissions', + initialState, + reducers: { + setRefetch: (state, action: PayloadAction) => { + state.refetch = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(fetch.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(fetch.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(fetch.fulfilled, (state, action) => { + if (action.payload.rows && action.payload.count >= 0) { + state.permissions = action.payload.rows; + state.count = action.payload.count; + } else { + state.permissions = action.payload; + } + state.loading = false; + }); + + builder.addCase(deleteItemsByIds.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItemsByIds.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Permissions has been deleted'); + }); + + builder.addCase(deleteItemsByIds.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Permissions'.slice(0, -1)} has been deleted`); + }); + + builder.addCase(deleteItem.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(create.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Permissions'.slice(0, -1)} has been created`); + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Permissions'.slice(0, -1)} has been updated`); + }); + builder.addCase(update.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(uploadCsv.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(uploadCsv.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Permissions has been uploaded'); + }); + builder.addCase(uploadCsv.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { setRefetch } = permissionsSlice.actions; + +export default permissionsSlice.reducer; diff --git a/frontend/src/stores/qr_codes/qr_codesSlice.ts b/frontend/src/stores/qr_codes/qr_codesSlice.ts new file mode 100644 index 0000000..9cfda73 --- /dev/null +++ b/frontend/src/stores/qr_codes/qr_codesSlice.ts @@ -0,0 +1,236 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + qr_codes: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + qr_codes: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('qr_codes/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`qr_codes${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'qr_codes/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('qr_codes/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'qr_codes/deleteQr_codes', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`qr_codes/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'qr_codes/createQr_codes', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('qr_codes', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'qr_codes/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('qr_codes/bulk-import', data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const update = createAsyncThunk( + 'qr_codes/updateQr_codes', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`qr_codes/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const qr_codesSlice = createSlice({ + name: 'qr_codes', + initialState, + reducers: { + setRefetch: (state, action: PayloadAction) => { + state.refetch = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(fetch.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(fetch.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(fetch.fulfilled, (state, action) => { + if (action.payload.rows && action.payload.count >= 0) { + state.qr_codes = action.payload.rows; + state.count = action.payload.count; + } else { + state.qr_codes = action.payload; + } + state.loading = false; + }); + + builder.addCase(deleteItemsByIds.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItemsByIds.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Qr_codes has been deleted'); + }); + + builder.addCase(deleteItemsByIds.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Qr_codes'.slice(0, -1)} has been deleted`); + }); + + builder.addCase(deleteItem.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(create.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Qr_codes'.slice(0, -1)} has been created`); + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Qr_codes'.slice(0, -1)} has been updated`); + }); + builder.addCase(update.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(uploadCsv.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(uploadCsv.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Qr_codes has been uploaded'); + }); + builder.addCase(uploadCsv.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { setRefetch } = qr_codesSlice.actions; + +export default qr_codesSlice.reducer; diff --git a/frontend/src/stores/roles/rolesSlice.ts b/frontend/src/stores/roles/rolesSlice.ts new file mode 100644 index 0000000..177e83b --- /dev/null +++ b/frontend/src/stores/roles/rolesSlice.ts @@ -0,0 +1,283 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + roles: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + roles: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('roles/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`roles${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'roles/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('roles/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'roles/deleteRoles', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`roles/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'roles/createRoles', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('roles', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'roles/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('roles/bulk-import', data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const update = createAsyncThunk( + 'roles/updateRoles', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`roles/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const removeWidget = createAsyncThunk( + 'openai/removeWidget', + async (payload: any) => { + const result = await axios.delete(`openai/roles-info/${payload.id}`, { + params: { + roleId: payload.roleId, + infoId: payload.widgetId, + key: 'widgets', + }, + }); + return result.data; + }, +); + +export const fetchWidgets = createAsyncThunk( + 'openai/fetchWidgets', + async (roleId: any) => { + const result = await axios.get( + `openai/info-by-key?key=widgets&roleId=${roleId}`, + ); + return result.data; + }, +); + +export const rolesSlice = createSlice({ + name: 'roles', + initialState, + reducers: { + setRefetch: (state, action: PayloadAction) => { + state.refetch = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(fetch.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(fetch.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(fetch.fulfilled, (state, action) => { + if (action.payload.rows && action.payload.count >= 0) { + state.roles = action.payload.rows; + state.count = action.payload.count; + } else { + state.roles = action.payload; + } + state.loading = false; + }); + + builder.addCase(deleteItemsByIds.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItemsByIds.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Roles has been deleted'); + }); + + builder.addCase(deleteItemsByIds.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Roles'.slice(0, -1)} has been deleted`); + }); + + builder.addCase(deleteItem.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(create.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Roles'.slice(0, -1)} has been created`); + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Roles'.slice(0, -1)} has been updated`); + }); + builder.addCase(update.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(uploadCsv.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(uploadCsv.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Roles has been uploaded'); + }); + builder.addCase(uploadCsv.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(removeWidget.pending, (state) => { + state.loading = true; + }); + builder.addCase(removeWidget.fulfilled, (state) => { + state.loading = false; + }); + builder.addCase(removeWidget.rejected, (state) => { + state.loading = false; + }); + + builder.addCase(fetchWidgets.pending, (state) => { + state.loading = true; + state.rolesWidgets = []; + }); + builder.addCase(fetchWidgets.fulfilled, (state, action) => { + state.loading = false; + state.rolesWidgets = action.payload; + }); + builder.addCase(fetchWidgets.rejected, (state) => { + state.loading = false; + state.rolesWidgets = []; + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { setRefetch } = rolesSlice.actions; + +export default rolesSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts new file mode 100644 index 0000000..e6b7048 --- /dev/null +++ b/frontend/src/stores/store.ts @@ -0,0 +1,33 @@ +import { configureStore } from '@reduxjs/toolkit'; +import styleReducer from './styleSlice'; +import mainReducer from './mainSlice'; +import authSlice from './authSlice'; +import openAiSlice from './openAiSlice'; + +import usersSlice from './users/usersSlice'; +import access_logsSlice from './access_logs/access_logsSlice'; +import qr_codesSlice from './qr_codes/qr_codesSlice'; +import videosSlice from './videos/videosSlice'; +import rolesSlice from './roles/rolesSlice'; +import permissionsSlice from './permissions/permissionsSlice'; + +export const store = configureStore({ + reducer: { + style: styleReducer, + main: mainReducer, + auth: authSlice, + openAi: openAiSlice, + + users: usersSlice, + access_logs: access_logsSlice, + qr_codes: qr_codesSlice, + videos: videosSlice, + roles: rolesSlice, + permissions: permissionsSlice, + }, +}); + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType; +// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} +export type AppDispatch = typeof store.dispatch; diff --git a/frontend/src/stores/styleSlice.ts b/frontend/src/stores/styleSlice.ts new file mode 100644 index 0000000..fe14744 --- /dev/null +++ b/frontend/src/stores/styleSlice.ts @@ -0,0 +1,107 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import * as styles from '../styles'; +import { localStorageDarkModeKey, localStorageStyleKey } from '../config'; +import { StyleKey } from '../interfaces'; + +interface StyleState { + asideStyle: string; + asideScrollbarsStyle: string; + asideBrandStyle: string; + asideMenuItemStyle: string; + asideMenuItemActiveStyle: string; + asideMenuDropdownStyle: string; + navBarItemLabelStyle: string; + navBarItemLabelHoverStyle: string; + navBarItemLabelActiveColorStyle: string; + overlayStyle: string; + darkMode: boolean; + bgLayoutColor: string; + iconsColor: string; + activeLinkColor: string; + cardsColor: string; + focusRingColor: string; + corners: string; + cardsStyle: string; + linkColor: string; + websiteHeder: string; + borders: string; + shadow: string; + websiteSectionStyle: string; + textSecondary: string; +} + +const initialState: StyleState = { + asideStyle: styles.white.aside, + asideScrollbarsStyle: styles.white.asideScrollbars, + asideBrandStyle: styles.white.asideBrand, + asideMenuItemStyle: styles.white.asideMenuItem, + asideMenuItemActiveStyle: styles.white.asideMenuItemActive, + asideMenuDropdownStyle: styles.white.asideMenuDropdown, + navBarItemLabelStyle: styles.white.navBarItemLabel, + navBarItemLabelHoverStyle: styles.white.navBarItemLabelHover, + navBarItemLabelActiveColorStyle: styles.white.navBarItemLabelActiveColor, + overlayStyle: styles.white.overlay, + darkMode: false, + bgLayoutColor: styles.white.bgLayoutColor, + iconsColor: styles.white.iconsColor, + activeLinkColor: styles.white.activeLinkColor, + cardsColor: styles.white.cardsColor, + focusRingColor: styles.white.focusRingColor, + corners: styles.white.corners, + cardsStyle: styles.white.cardsStyle, + linkColor: styles.white.linkColor, + websiteHeder: styles.white.websiteHeder, + borders: styles.white.borders, + shadow: styles.white.shadow, + websiteSectionStyle: styles.white.websiteSectionStyle, + textSecondary: styles.white.textSecondary, +}; + +export const styleSlice = createSlice({ + name: 'style', + initialState, + reducers: { + setDarkMode: (state, action: PayloadAction) => { + state.darkMode = + action.payload !== null ? action.payload : !state.darkMode; + + if (typeof localStorage !== 'undefined') { + localStorage.setItem( + localStorageDarkModeKey, + state.darkMode ? '1' : '0', + ); + } + + if (typeof document !== 'undefined') { + document.body.classList[state.darkMode ? 'add' : 'remove']( + 'dark-scrollbars', + ); + + document.documentElement.classList[state.darkMode ? 'add' : 'remove']( + 'dark-scrollbars-compat', + ); + } + }, + + setStyle: (state, action: PayloadAction) => { + if (!styles[action.payload]) { + return; + } + + if (typeof localStorage !== 'undefined') { + localStorage.setItem(localStorageStyleKey, action.payload); + } + + const style = styles[action.payload]; + + for (const key in style) { + state[`${key}Style`] = style[key]; + } + }, + }, +}); + +// Action creators are generated for each case reducer function +export const { setDarkMode, setStyle } = styleSlice.actions; + +export default styleSlice.reducer; diff --git a/frontend/src/stores/users/usersSlice.ts b/frontend/src/stores/users/usersSlice.ts new file mode 100644 index 0000000..f182315 --- /dev/null +++ b/frontend/src/stores/users/usersSlice.ts @@ -0,0 +1,236 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + users: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + users: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('users/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`users${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'users/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('users/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'users/deleteUsers', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`users/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'users/createUsers', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('users', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'users/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('users/bulk-import', data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const update = createAsyncThunk( + 'users/updateUsers', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`users/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const usersSlice = createSlice({ + name: 'users', + initialState, + reducers: { + setRefetch: (state, action: PayloadAction) => { + state.refetch = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(fetch.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(fetch.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(fetch.fulfilled, (state, action) => { + if (action.payload.rows && action.payload.count >= 0) { + state.users = action.payload.rows; + state.count = action.payload.count; + } else { + state.users = action.payload; + } + state.loading = false; + }); + + builder.addCase(deleteItemsByIds.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItemsByIds.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Users has been deleted'); + }); + + builder.addCase(deleteItemsByIds.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Users'.slice(0, -1)} has been deleted`); + }); + + builder.addCase(deleteItem.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(create.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Users'.slice(0, -1)} has been created`); + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Users'.slice(0, -1)} has been updated`); + }); + builder.addCase(update.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(uploadCsv.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(uploadCsv.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Users has been uploaded'); + }); + builder.addCase(uploadCsv.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { setRefetch } = usersSlice.actions; + +export default usersSlice.reducer; diff --git a/frontend/src/stores/usersSlice.ts b/frontend/src/stores/usersSlice.ts new file mode 100644 index 0000000..aa7f5be --- /dev/null +++ b/frontend/src/stores/usersSlice.ts @@ -0,0 +1,119 @@ +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import axios from 'axios'; + +interface MainState { + users: any; + loading: boolean; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + users: [], + loading: false, + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('users/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`users${query || (id ? `/${id}` : '')}`); + return id ? result.data : result.data.rows; +}); + +export const deleteItem = createAsyncThunk( + 'users/deleteUser', + async (id: string, thunkAPI) => { + try { + await axios.delete(`users/${id}`); + thunkAPI.dispatch(fetch({ id: '', query: '' })); + } catch (error) { + console.log(error); + } + + // showNotification('Users has been deleted', 'success'); + }, +); + +export const create = createAsyncThunk( + 'users/createUser', + async (data: any) => { + const result = await axios.post('users', { data }); + // showNotification('Users has been created', 'success'); + return result.data; + }, +); + +export const update = createAsyncThunk( + 'users/updateUser', + async (payload: any) => { + const result = await axios.put(`users/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + }, +); + +export const usersSlice = createSlice({ + name: 'users', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(fetch.pending, (state) => { + state.loading = true; + }); + builder.addCase(fetch.rejected, (state) => { + state.loading = false; + }); + + builder.addCase(fetch.fulfilled, (state, action) => { + state.users = action.payload; + state.loading = false; + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + }); + + builder.addCase(deleteItem.rejected, (state) => { + state.loading = false; + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + }); + builder.addCase(create.rejected, (state) => { + state.loading = false; + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + }); + builder.addCase(update.rejected, (state) => { + state.loading = false; + }); + }, +}); + +// Action creators are generated for each case reducer function +// export const { } = usersSlice.actions + +export default usersSlice.reducer; diff --git a/frontend/src/stores/videos/videosSlice.ts b/frontend/src/stores/videos/videosSlice.ts new file mode 100644 index 0000000..8a1b35f --- /dev/null +++ b/frontend/src/stores/videos/videosSlice.ts @@ -0,0 +1,236 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + videos: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + videos: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('videos/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`videos${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'videos/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('videos/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'videos/deleteVideos', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`videos/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'videos/createVideos', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('videos', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'videos/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('videos/bulk-import', data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const update = createAsyncThunk( + 'videos/updateVideos', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`videos/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const videosSlice = createSlice({ + name: 'videos', + initialState, + reducers: { + setRefetch: (state, action: PayloadAction) => { + state.refetch = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(fetch.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(fetch.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(fetch.fulfilled, (state, action) => { + if (action.payload.rows && action.payload.count >= 0) { + state.videos = action.payload.rows; + state.count = action.payload.count; + } else { + state.videos = action.payload; + } + state.loading = false; + }); + + builder.addCase(deleteItemsByIds.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItemsByIds.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Videos has been deleted'); + }); + + builder.addCase(deleteItemsByIds.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Videos'.slice(0, -1)} has been deleted`); + }); + + builder.addCase(deleteItem.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(create.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Videos'.slice(0, -1)} has been created`); + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Videos'.slice(0, -1)} has been updated`); + }); + builder.addCase(update.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(uploadCsv.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(uploadCsv.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Videos has been uploaded'); + }); + builder.addCase(uploadCsv.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { setRefetch } = videosSlice.actions; + +export default videosSlice.reducer; diff --git a/frontend/src/styles.ts b/frontend/src/styles.ts new file mode 100644 index 0000000..e1d80e3 --- /dev/null +++ b/frontend/src/styles.ts @@ -0,0 +1,106 @@ +interface StyleObject { + aside: string; + asideScrollbars: string; + asideBrand: string; + asideMenuItem: string; + asideMenuItemActive: string; + asideMenuDropdown: string; + navBarItemLabel: string; + navBarItemLabelHover: string; + navBarItemLabelActiveColor: string; + overlay: string; + activeLinkColor: string; + bgLayoutColor: string; + iconsColor: string; + cardsColor: string; + focusRingColor: string; + corners: string; + cardsStyle: string; + linkColor: string; + websiteHeder: string; + borders: string; + shadow: string; + websiteSectionStyle: string; + textSecondary: string; +} + +export const basic: StyleObject = { + aside: 'bg-gray-800 lg:rounded-2xl', + asideScrollbars: 'aside-scrollbars-gray', + asideBrand: 'bg-gray-900 text-white', + asideMenuItem: 'text-gray-300 hover:text-white', + asideMenuItemActive: 'font-bold text-white', + asideMenuDropdown: 'bg-gray-700/50', + navBarItemLabel: 'text-black', + navBarItemLabelHover: 'hover:text-blue-500', + navBarItemLabelActiveColor: 'text-blue-600', + overlay: 'from-gray-700 via-gray-900 to-gray-700', + activeLinkColor: 'bg-gray-100/70', + bgLayoutColor: 'bg-gray-50', + iconsColor: 'text-blue-500', + cardsColor: 'bg-white', + focusRingColor: + 'focus:ring focus:ring-blue-600 focus:border-blue-600 focus:outline-none dark:focus:ring-blue-600 border-gray-300 dark:focus:border-blue-600', + corners: 'rounded', + cardsStyle: 'bg-white border border-pavitra-400', + linkColor: 'text-black', + websiteHeder: '', + borders: '', + shadow: '', + websiteSectionStyle: '', + textSecondary: '', +}; + +export const white: StyleObject = { + aside: 'bg-white dark:text-white lg:rounded-2xl', + asideScrollbars: 'aside-scrollbars-light', + asideBrand: '', + asideMenuItem: + 'text-gray-700 hover:bg-gray-100/70 dark:text-dark-500 dark:hover:text-white dark:hover:bg-dark-800', + asideMenuItemActive: 'font-bold text-black dark:text-white', + asideMenuDropdown: 'bg-gray-100/75', + navBarItemLabel: 'text-blue-600', + navBarItemLabelHover: 'hover:text-black', + navBarItemLabelActiveColor: 'text-black', + overlay: 'from-white via-gray-100 to-white', + activeLinkColor: 'bg-gray-100/70', + bgLayoutColor: 'bg-gray-50', + iconsColor: 'text-blue-500', + cardsColor: 'bg-white', + focusRingColor: + 'focus:ring focus:ring-blue-600 focus:border-blue-600 focus:outline-none border-gray-300 dark:focus:ring-blue-600 dark:focus:border-blue-600', + corners: 'rounded', + cardsStyle: 'bg-white border border-pavitra-400', + linkColor: 'text-blue-600', + websiteHeder: 'border-b border-gray-200', + borders: 'border-gray-200', + shadow: '', + websiteSectionStyle: '', + textSecondary: 'text-gray-500', +}; + +export const dataGridStyles = { + '& .MuiDataGrid-cell': { + paddingX: 3, + border: 'none', + }, + '& .MuiDataGrid-columnHeader': { + paddingX: 3, + }, + '& .MuiDataGrid-columnHeaderCheckbox': { + paddingX: 0, + }, + '& .MuiDataGrid-columnHeaders': { + paddingY: 4, + borderStartStartRadius: 7, + borderStartEndRadius: 7, + }, + '& .MuiDataGrid-footerContainer': { + paddingY: 0.5, + borderEndStartRadius: 7, + borderEndEndRadius: 7, + }, + '& .MuiDataGrid-root': { + border: 'none', + }, +}; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..08055fc --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,106 @@ +/* eslint-env node */ +/* eslint-disable-next-line */ +const plugin = require('tailwindcss/plugin'); + +module.exports = { + content: ['./src/**/*.{js,ts,jsx,tsx}'], + darkMode: 'class', // or 'media' or 'class' + theme: { + asideScrollbars: { + light: 'light', + gray: 'gray', + }, + extend: { + zIndex: { + '-1': '-1', + }, + flexGrow: { + 5: '5', + }, + maxHeight: { + 'screen-menu': 'calc(100vh - 3.5rem)', + modal: 'calc(100vh - 160px)', + }, + transitionProperty: { + position: 'right, left, top, bottom, margin, padding', + textColor: 'color', + }, + keyframes: { + 'fade-out': { + from: { opacity: 1 }, + to: { opacity: 0 }, + }, + 'fade-in': { + from: { opacity: 0 }, + to: { opacity: 1 }, + }, + }, + animation: { + 'fade-out': 'fade-out 250ms ease-in-out', + 'fade-in': 'fade-in 250ms ease-in-out', + }, + colors: { + dark: { + 900: '#131618', + 800: '#21242A', + 700: '#2C2F36', + 600: '#9CA3AF', + 500: '#CBD5E1', + }, + green: { + text: '#45B26B', + }, + pavitra: { + blue: '#0162FD', + green: '#00B448', + orange: '#FFAA00', + red: '#F20041', + 900: '#14142A', + 800: '#4E4B66', + 700: '#6E7191', + 600: '#A0A3BD', + 500: '#D9DBE9', + 400: '#EFF0F6', + 300: '#F7F7FC', + }, + }, + borderRadius: { + '3xl': '2rem', + }, + }, + }, + plugins: [ + require('@tailwindcss/forms'), + require('@tailwindcss/typography'), + plugin(function ({ matchUtilities, theme }) { + matchUtilities( + { + 'aside-scrollbars': (value) => { + const track = value === 'light' ? '100' : '900'; + const thumb = value === 'light' ? '300' : '600'; + const color = value === 'light' ? 'gray' : value; + + return { + scrollbarWidth: 'thin', + scrollbarColor: `${theme(`colors.${color}.${thumb}`)} ${theme( + `colors.${color}.${track}`, + )}`, + '&::-webkit-scrollbar': { + width: '8px', + height: '8px', + }, + '&::-webkit-scrollbar-track': { + backgroundColor: theme(`colors.${color}.${track}`), + }, + '&::-webkit-scrollbar-thumb': { + borderRadius: '0.25rem', + backgroundColor: theme(`colors.${color}.${thumb}`), + }, + }; + }, + }, + { values: theme('asideScrollbars') }, + ); + }), + ], +}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..b8d5978 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock new file mode 100644 index 0000000..5c2e352 --- /dev/null +++ b/frontend/yarn.lock @@ -0,0 +1,3608 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@alloc/quick-lru@^5.2.0": + version "5.2.0" + resolved "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz" + integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/generator@^7.26.3": + version "7.26.3" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz" + integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ== + dependencies: + "@babel/parser" "^7.26.3" + "@babel/types" "^7.26.3" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + +"@babel/helper-module-imports@^7.16.7": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz" + integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/parser@^7.25.9", "@babel/parser@^7.26.3": + version "7.26.3" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz" + integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== + dependencies: + "@babel/types" "^7.26.3" + +"@babel/runtime-corejs3@^7.10.2": + version "7.19.0" + resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.19.0.tgz" + integrity sha512-JyXXoCu1N8GLuKc2ii8y5RGma5FMpFeO2nAQIe0Yzrbq+rQnN+sFj47auLblR5ka6aHNGPDgv8G/iI2Grb0ldQ== + dependencies: + core-js-pure "^3.20.2" + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.26.0" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz" + integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/traverse@^7.25.9": + version "7.26.4" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz" + integrity sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.3" + "@babel/parser" "^7.26.3" + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.3" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.25.9", "@babel/types@^7.26.3": + version "7.26.3" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz" + integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + +"@emotion/babel-plugin@^11.13.5": + version "11.13.5" + resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz" + integrity sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/serialize" "^1.3.3" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + +"@emotion/cache@^11.13.5", "@emotion/cache@^11.14.0", "@emotion/cache@^11.4.0": + version "11.14.0" + resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz" + integrity sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA== + dependencies: + "@emotion/memoize" "^0.9.0" + "@emotion/sheet" "^1.4.0" + "@emotion/utils" "^1.4.2" + "@emotion/weak-memoize" "^0.4.0" + stylis "4.2.0" + +"@emotion/hash@^0.9.2": + version "0.9.2" + resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz" + integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== + +"@emotion/is-prop-valid@^1.3.0": + version "1.3.1" + resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz" + integrity sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw== + dependencies: + "@emotion/memoize" "^0.9.0" + +"@emotion/memoize@^0.9.0": + version "0.9.0" + resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz" + integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== + +"@emotion/react@^11.11.3", "@emotion/react@^11.8.1": + version "11.14.0" + resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz" + integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.13.5" + "@emotion/cache" "^11.14.0" + "@emotion/serialize" "^1.3.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.2.0" + "@emotion/utils" "^1.4.2" + "@emotion/weak-memoize" "^0.4.0" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.3.3": + version "1.3.3" + resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz" + integrity sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA== + dependencies: + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/unitless" "^0.10.0" + "@emotion/utils" "^1.4.2" + csstype "^3.0.2" + +"@emotion/sheet@^1.4.0": + version "1.4.0" + resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz" + integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== + +"@emotion/styled@^11.11.0": + version "11.14.0" + resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz" + integrity sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.13.5" + "@emotion/is-prop-valid" "^1.3.0" + "@emotion/serialize" "^1.3.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.2.0" + "@emotion/utils" "^1.4.2" + +"@emotion/unitless@^0.10.0": + version "0.10.0" + resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz" + integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== + +"@emotion/use-insertion-effect-with-fallbacks@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz" + integrity sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg== + +"@emotion/utils@^1.4.2": + version "1.4.2" + resolved "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz" + integrity sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA== + +"@emotion/weak-memoize@^0.4.0": + version "0.4.0" + resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz" + integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== + +"@eslint/eslintrc@^1.3.2": + version "1.3.2" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz" + integrity sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.4.0" + globals "^13.15.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" + +"@floating-ui/core@^1.6.0": + version "1.6.8" + resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz" + integrity sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA== + dependencies: + "@floating-ui/utils" "^0.2.8" + +"@floating-ui/dom@^1.0.1": + version "1.6.12" + resolved "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz" + integrity sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w== + dependencies: + "@floating-ui/core" "^1.6.0" + "@floating-ui/utils" "^0.2.8" + +"@floating-ui/utils@^0.2.8": + version "0.2.8" + resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz" + integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig== + +"@humanwhocodes/config-array@^0.10.4": + version "0.10.4" + resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz" + integrity sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/gitignore-to-minimatch@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz" + integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA== + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + 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" + +"@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.8" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@kurkle/color@^0.3.0": + version "0.3.4" + resolved "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz" + integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w== + +"@mdi/js@^7.4.47": + version "7.4.47" + resolved "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz" + integrity sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ== + +"@mui/core-downloads-tracker@^5.16.13": + version "5.16.13" + resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.13.tgz" + integrity sha512-xe5RwI0Q2O709Bd2Y7l1W1NIwNmln0y+xaGk5VgX3vDJbkQEqzdfTFZ73e0CkEZgJwyiWgk5HY0l8R4nysOxjw== + +"@mui/material@^5.15.7": + version "5.16.13" + resolved "https://registry.npmjs.org/@mui/material/-/material-5.16.13.tgz" + integrity sha512-FhLDkDPYDzvrWCHFsdXzRArhS4AdYufU8d69rmLL+bwhodPcbm2C7cS8Gq5VR32PsW6aKZb58gvAgvEVaiiJbA== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/core-downloads-tracker" "^5.16.13" + "@mui/system" "^5.16.13" + "@mui/types" "^7.2.15" + "@mui/utils" "^5.16.13" + "@popperjs/core" "^2.11.8" + "@types/react-transition-group" "^4.4.10" + clsx "^2.1.0" + csstype "^3.1.3" + prop-types "^15.8.1" + react-is "^19.0.0" + react-transition-group "^4.4.5" + +"@mui/private-theming@^5.16.13": + version "5.16.13" + resolved "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.13.tgz" + integrity sha512-+s0FklvDvO7j0yBZn19DIIT3rLfub2fWvXGtMX49rG/xHfDFcP7fbWbZKHZMMP/2/IoTRDrZCbY1iP0xZlmuJA== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/utils" "^5.16.13" + prop-types "^15.8.1" + +"@mui/styled-engine@^5.16.13": + version "5.16.13" + resolved "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.13.tgz" + integrity sha512-2XNHEG8/o1ucSLhTA9J+HIIXjzlnEc0OV7kneeUQ5JukErPYT2zc6KYBDLjlKWrzQyvnQzbiffjjspgHUColZg== + dependencies: + "@babel/runtime" "^7.23.9" + "@emotion/cache" "^11.13.5" + csstype "^3.1.3" + prop-types "^15.8.1" + +"@mui/system@^5.16.13": + version "5.16.13" + resolved "https://registry.npmjs.org/@mui/system/-/system-5.16.13.tgz" + integrity sha512-JnO3VH3yNoAmgyr44/2jiS1tcNwshwAqAaG5fTEEjHQbkuZT/mvPYj2GC1cON0zEQ5V03xrCNl/D+gU9AXibpw== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/private-theming" "^5.16.13" + "@mui/styled-engine" "^5.16.13" + "@mui/types" "^7.2.15" + "@mui/utils" "^5.16.13" + clsx "^2.1.0" + csstype "^3.1.3" + prop-types "^15.8.1" + +"@mui/types@^7.2.15": + version "7.2.20" + resolved "https://registry.npmjs.org/@mui/types/-/types-7.2.20.tgz" + integrity sha512-straFHD7L8v05l/N5vcWk+y7eL9JF0C2mtph/y4BPm3gn2Eh61dDwDB65pa8DLss3WJfDXYC7Kx5yjP0EmXpgw== + +"@mui/utils@^5.14.16", "@mui/utils@^5.16.13": + version "5.16.13" + resolved "https://registry.npmjs.org/@mui/utils/-/utils-5.16.13.tgz" + integrity sha512-35kLiShnDPByk57Mz4PP66fQUodCFiOD92HfpW6dK9lc7kjhZsKHRKeYPgWuwEHeXwYsCSFtBCW4RZh/8WT+TQ== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/types" "^7.2.15" + "@types/prop-types" "^15.7.12" + clsx "^2.1.1" + prop-types "^15.8.1" + react-is "^19.0.0" + +"@mui/x-data-grid@^6.19.2": + version "6.20.4" + resolved "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-6.20.4.tgz" + integrity sha512-I0JhinVV4e25hD2dB+R6biPBtpGeFrXf8RwlMPQbr9gUggPmPmNtWKo8Kk2PtBBMlGtdMAgHWe7PqhmucUxU1w== + dependencies: + "@babel/runtime" "^7.23.2" + "@mui/utils" "^5.14.16" + clsx "^2.0.0" + prop-types "^15.8.1" + reselect "^4.1.8" + +"@next/env@14.2.22": + version "14.2.22" + resolved "https://registry.npmjs.org/@next/env/-/env-14.2.22.tgz" + integrity sha512-EQ6y1QeNQglNmNIXvwP/Bb+lf7n9WtgcWvtoFsHquVLCJUuxRs+6SfZ5EK0/EqkkLex4RrDySvKgKNN7PXip7Q== + +"@next/eslint-plugin-next@13.0.4": + version "13.0.4" + resolved "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.0.4.tgz" + integrity sha512-jZ4urKT+aO9QHm3ttihrIQscQISDSKK8isAom750+EySn9o3LCSkTdPGBtvBqY7Yku+NqhfQempR5J58DqTaVg== + dependencies: + glob "7.1.7" + +"@next/swc-darwin-arm64@14.2.22": + version "14.2.22" + resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.22.tgz" + integrity sha512-HUaLiehovgnqY4TMBZJ3pDaOsTE1spIXeR10pWgdQVPYqDGQmHJBj3h3V6yC0uuo/RoY2GC0YBFRkOX3dI9WVQ== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": + version "2.0.5" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@pkgr/utils@^2.3.1": + version "2.3.1" + resolved "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz" + integrity sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw== + dependencies: + cross-spawn "^7.0.3" + is-glob "^4.0.3" + open "^8.4.0" + picocolors "^1.0.0" + tiny-glob "^0.2.9" + tslib "^2.4.0" + +"@popperjs/core@^2.11.6", "@popperjs/core@^2.11.8": + version "2.11.8" + resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + +"@reduxjs/toolkit@^2.1.0": + version "2.5.0" + resolved "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz" + integrity sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg== + dependencies: + immer "^10.0.3" + redux "^5.0.1" + redux-thunk "^3.1.0" + reselect "^5.1.0" + +"@restart/hooks@^0.4.7": + 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" + +"@rushstack/eslint-patch@^1.1.3": + version "1.1.4" + resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.4.tgz" + integrity sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA== + +"@seznam/compose-react-refs@^1.0.6": + version "1.0.6" + resolved "https://registry.npmjs.org/@seznam/compose-react-refs/-/compose-react-refs-1.0.6.tgz" + integrity sha512-izzOXQfeQLonzrIQb8u6LQ8dk+ymz3WXTIXjvOlTXHq6sbzROg3NWU+9TTAOpEoK9Bth24/6F/XrfHJ5yR5n6Q== + +"@swc/counter@^0.1.3": + version "0.1.3" + resolved "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz" + integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== + +"@swc/helpers@0.5.5": + version "0.5.5" + resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz" + integrity sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A== + dependencies: + "@swc/counter" "^0.1.3" + tslib "^2.4.0" + +"@tailwindcss/forms@^0.5.7": + version "0.5.9" + resolved "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.9.tgz" + integrity sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg== + dependencies: + mini-svg-data-uri "^1.2.3" + +"@tailwindcss/line-clamp@^0.4.4": + version "0.4.4" + resolved "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.4.tgz" + integrity sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g== + +"@tailwindcss/typography@^0.5.13": + version "0.5.15" + resolved "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz" + integrity sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA== + dependencies: + lodash.castarray "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.merge "^4.6.2" + postcss-selector-parser "6.0.10" + +"@tinymce/tinymce-react@^4.3.2": + version "4.3.2" + resolved "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-4.3.2.tgz" + integrity sha512-wJHZhPf2Mk3yTtdVC/uIGh+kvDgKuTw/qV13uzdChTNo68JI1l7jYMrSQOpyimDyn5LHAw0E1zFByrm1WHAVeA== + dependencies: + prop-types "^15.6.2" + tinymce "^6.0.0 || ^5.5.1" + +"@types/date-arithmetic@*": + version "4.1.4" + resolved "https://registry.npmjs.org/@types/date-arithmetic/-/date-arithmetic-4.1.4.tgz" + integrity sha512-p9eZ2X9B80iKiTW4ukVj8B4K6q9/+xFtQ5MGYA5HWToY9nL4EkhV9+6ftT2VHpVMEZb5Tv00Iel516bVdO+yRw== + +"@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1": + version "3.3.1" + resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + +"@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/node@18.7.16": + version "18.7.16" + resolved "https://registry.npmjs.org/@types/node/-/node-18.7.16.tgz" + integrity sha512-EQHhixfu+mkqHMZl1R2Ovuvn47PUw18azMJOTwSZr9/fhzHNGXAJ0ma0dayRVchprpCj0Kc1K1xKoWaATWF1qg== + +"@types/numeral@^2.0.2": + version "2.0.2" + resolved "https://registry.npmjs.org/@types/numeral/-/numeral-2.0.2.tgz" + integrity sha512-A8F30k2gYJ/6e07spSCPpkuZu79LCnkPTvqmIWQzNGcrzwFKpVOydG41lNt5wZXjSI149qjyzC2L1+F2PD/NUA== + +"@types/parse-json@^4.0.0": + version "4.0.2" + resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== + +"@types/prop-types@*", "@types/prop-types@^15.7.12": + version "15.7.14" + resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz" + integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== + +"@types/react-big-calendar@^1.8.8": + version "1.16.0" + resolved "https://registry.npmjs.org/@types/react-big-calendar/-/react-big-calendar-1.16.0.tgz" + integrity sha512-1w2GXAJWlGmaPZOd9J9cyWA/XBNOGRZ4MmRNypEQhwEMIIL9cfd1UdcvzSrQsnBm0qYF/scqmsISNbUzPBE1vg== + dependencies: + "@types/date-arithmetic" "*" + "@types/prop-types" "*" + "@types/react" "*" + +"@types/react-redux@^7.1.24": + version "7.1.24" + resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz" + integrity sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + +"@types/react-transition-group@^4.4.0", "@types/react-transition-group@^4.4.10": + version "4.4.12" + resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz" + integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== + +"@types/react@*", "@types/react@>=16.9.11": + version "18.0.19" + resolved "https://registry.npmjs.org/@types/react/-/react-18.0.19.tgz" + integrity sha512-BDc3Q+4Q3zsn7k9xZrKfjWyJsSlEDMs38gD1qp2eDazLCdcPqAT+vq1ND+Z8AGel/UiwzNUk8ptpywgNQcJ1MQ== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + +"@types/warning@^3.0.0": + version "3.0.3" + resolved "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz" + integrity sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q== + +"@typescript-eslint/eslint-plugin@^5.37.0": + version "5.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.37.0.tgz" + integrity sha512-Fde6W0IafXktz1UlnhGkrrmnnGpAo1kyX7dnyHHVrmwJOn72Oqm3eYtddrpOwwel2W8PAK9F3pIL5S+lfoM0og== + dependencies: + "@typescript-eslint/scope-manager" "5.37.0" + "@typescript-eslint/type-utils" "5.37.0" + "@typescript-eslint/utils" "5.37.0" + debug "^4.3.4" + functional-red-black-tree "^1.0.1" + ignore "^5.2.0" + regexpp "^3.2.0" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/parser@^5.37.0", "@typescript-eslint/parser@^5.42.0": + version "5.43.0" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.43.0.tgz" + integrity sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug== + dependencies: + "@typescript-eslint/scope-manager" "5.43.0" + "@typescript-eslint/types" "5.43.0" + "@typescript-eslint/typescript-estree" "5.43.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.37.0": + version "5.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.37.0.tgz" + integrity sha512-F67MqrmSXGd/eZnujjtkPgBQzgespu/iCZ+54Ok9X5tALb9L2v3G+QBSoWkXG0p3lcTJsL+iXz5eLUEdSiJU9Q== + dependencies: + "@typescript-eslint/types" "5.37.0" + "@typescript-eslint/visitor-keys" "5.37.0" + +"@typescript-eslint/scope-manager@5.43.0": + version "5.43.0" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.43.0.tgz" + integrity sha512-XNWnGaqAtTJsUiZaoiGIrdJYHsUOd3BZ3Qj5zKp9w6km6HsrjPk/TGZv0qMTWyWj0+1QOqpHQ2gZOLXaGA9Ekw== + dependencies: + "@typescript-eslint/types" "5.43.0" + "@typescript-eslint/visitor-keys" "5.43.0" + +"@typescript-eslint/type-utils@5.37.0": + version "5.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.37.0.tgz" + integrity sha512-BSx/O0Z0SXOF5tY0bNTBcDEKz2Ec20GVYvq/H/XNKiUorUFilH7NPbFUuiiyzWaSdN3PA8JV0OvYx0gH/5aFAQ== + dependencies: + "@typescript-eslint/typescript-estree" "5.37.0" + "@typescript-eslint/utils" "5.37.0" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.37.0": + version "5.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.37.0.tgz" + integrity sha512-3frIJiTa5+tCb2iqR/bf7XwU20lnU05r/sgPJnRpwvfZaqCJBrl8Q/mw9vr3NrNdB/XtVyMA0eppRMMBqdJ1bA== + +"@typescript-eslint/types@5.43.0": + version "5.43.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.43.0.tgz" + integrity sha512-jpsbcD0x6AUvV7tyOlyvon0aUsQpF8W+7TpJntfCUWU1qaIKu2K34pMwQKSzQH8ORgUrGYY6pVIh1Pi8TNeteg== + +"@typescript-eslint/typescript-estree@5.37.0": + version "5.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.37.0.tgz" + integrity sha512-JkFoFIt/cx59iqEDSgIGnQpCTRv96MQnXCYvJi7QhBC24uyuzbD8wVbajMB1b9x4I0octYFJ3OwjAwNqk1AjDA== + dependencies: + "@typescript-eslint/types" "5.37.0" + "@typescript-eslint/visitor-keys" "5.37.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/typescript-estree@5.43.0": + version "5.43.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.43.0.tgz" + integrity sha512-BZ1WVe+QQ+igWal2tDbNg1j2HWUkAa+CVqdU79L4HP9izQY6CNhXfkNwd1SS4+sSZAP/EthI1uiCSY/+H0pROg== + dependencies: + "@typescript-eslint/types" "5.43.0" + "@typescript-eslint/visitor-keys" "5.43.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.37.0": + version "5.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.37.0.tgz" + integrity sha512-jUEJoQrWbZhmikbcWSMDuUSxEE7ID2W/QCV/uz10WtQqfOuKZUqFGjqLJ+qhDd17rjgp+QJPqTdPIBWwoob2NQ== + dependencies: + "@types/json-schema" "^7.0.9" + "@typescript-eslint/scope-manager" "5.37.0" + "@typescript-eslint/types" "5.37.0" + "@typescript-eslint/typescript-estree" "5.37.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/visitor-keys@5.37.0": + version "5.37.0" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.37.0.tgz" + integrity sha512-Hp7rT4cENBPIzMwrlehLW/28EVCOcE9U1Z1BQTc8EA8v5qpr7GRGuG+U58V5tTY48zvUOA3KHvw3rA8tY9fbdA== + dependencies: + "@typescript-eslint/types" "5.37.0" + eslint-visitor-keys "^3.3.0" + +"@typescript-eslint/visitor-keys@5.43.0": + version "5.43.0" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.43.0.tgz" + integrity sha512-icl1jNH/d18OVHLfcwdL3bWUKsBeIiKYTGxMJCoGe7xFht+E4QgzOqoWYrU8XSLJWhVw8nTacbm03v23J/hFTg== + dependencies: + "@typescript-eslint/types" "5.43.0" + eslint-visitor-keys "^3.3.0" + +"@vtaits/use-lazy-ref@^0.1.3": + version "0.1.3" + resolved "https://registry.npmjs.org/@vtaits/use-lazy-ref/-/use-lazy-ref-0.1.3.tgz" + integrity sha512-ZTLuFBHSivPcgWrwkXe5ExVt6R3/ybD+N0yFPy4ClzCztk/9bUD/1udKQ/jd7eCal+lapSrRWXbffqI9jkpDlg== + +"@yr/monotone-cubic-spline@^1.0.3": + version "1.0.3" + resolved "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz" + integrity sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.8.0: + version "8.8.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz" + integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== + +ajv@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + 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" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +apexcharts@^3.45.2: + version "3.54.1" + resolved "https://registry.npmjs.org/apexcharts/-/apexcharts-3.54.1.tgz" + integrity sha512-E4et0h/J1U3r3EwS/WlqJCQIbepKbp6wGUmaAwJOMjHUP4Ci0gxanLa7FR3okx6p9coi4st6J853/Cb1NP0vpA== + dependencies: + "@yr/monotone-cubic-spline" "^1.0.3" + svg.draggable.js "^2.2.2" + svg.easing.js "^2.0.0" + svg.filter.js "^2.0.2" + svg.pathmorphing.js "^0.1.3" + svg.resize.js "^1.4.3" + svg.select.js "^3.0.1" + +arg@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +aria-query@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz" + integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== + dependencies: + "@babel/runtime" "^7.10.2" + "@babel/runtime-corejs3" "^7.10.2" + +array-includes@^3.1.4, array-includes@^3.1.5: + version "3.1.5" + resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz" + integrity sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + get-intrinsic "^1.1.1" + is-string "^1.0.7" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.flat@^1.2.5: + version "1.3.1" + resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz" + integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz" + integrity sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.2" + es-shim-unscopables "^1.0.0" + +ast-types-flow@^0.0.7: + version "0.0.7" + resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" + integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +autoprefixer@^10.4.0: + version "10.4.9" + resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.9.tgz" + integrity sha512-Uu67eduPEmOeA0vyJby5ghu1AAELCCNSsLAjK+lz6kYzNM5sqnBO36MqfsjhPjQF/BaJM5U/UuFYyl7PavY/wQ== + dependencies: + browserslist "^4.21.3" + caniuse-lite "^1.0.30001394" + fraction.js "^4.2.0" + normalize-range "^0.1.2" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" + +axe-core@^4.4.3: + version "4.4.3" + resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.4.3.tgz" + integrity sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w== + +axios@^1.6.7: + version "1.7.9" + resolved "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz" + integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +axobject-query@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz" + integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== + +babel-plugin-macros@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz" + integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.21.3: + version "4.21.3" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz" + integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ== + dependencies: + caniuse-lite "^1.0.30001370" + electron-to-chromium "^1.4.202" + node-releases "^2.0.6" + update-browserslist-db "^1.0.5" + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +busboy@1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + +caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001394, caniuse-lite@^1.0.30001579: + version "1.0.30001690" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz" + integrity sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chart.js@^4.4.1: + version "4.4.7" + resolved "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz" + integrity sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw== + dependencies: + "@kurkle/color" "^0.3.0" + +chokidar@^3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + 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" + optionalDependencies: + fsevents "~2.3.2" + +chroma-js@^2.4.2: + version "2.6.0" + resolved "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz" + integrity sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A== + +classnames@^2.2.6: + version "2.5.1" + resolved "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + +client-only@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + +clsx@^1.1.1: + version "1.2.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + +clsx@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + +clsx@^2.0.0, clsx@^2.1.0, clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^4.0.0: + version "4.1.1" + resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +convert-source-map@^1.5.0: + version "1.9.0" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +core-js-pure@^3.20.2: + version "3.25.1" + resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.25.1.tgz" + integrity sha512-7Fr74bliUDdeJCBMxkkIuQ4xfxn/SwrVg+HkJUAoNEXVqYLv55l6Af0dJ5Lq2YBUW9yKqSkLXaS5SYPK6MGa/A== + +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +csstype@^3.0.2, csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +damerau-levenshtein@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" + integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== + +date-arithmetic@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz" + integrity sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg== + +date-fns@^2.30.0: + version "2.30.0" + resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + +dayjs@^1.11.10, dayjs@^1.11.7: + version "1.11.13" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== + +debug@^2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +decode-uri-component@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz" + integrity sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ== + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^2.1.1: + version "2.2.1" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +define-properties@^1.1.3, define-properties@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +didyoumean@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" + integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1: + 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" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +electron-to-chromium@^1.4.202: + version "1.4.248" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.248.tgz" + integrity sha512-qShjzEYpa57NnhbW2K+g+Fl+eNoDvQ7I+2MRwWnU6Z6F0HhXekzsECCLv+y2OJUsRodjqoSfwHkIX42VUFtUzg== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +enhanced-resolve@^5.10.0: + version "5.10.0" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz" + integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.4: + version "1.20.4" + resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz" + integrity sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.3" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-weakref "^1.0.2" + object-inspect "^1.12.2" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" + string.prototype.trimend "^1.0.5" + string.prototype.trimstart "^1.0.5" + unbox-primitive "^1.0.2" + +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-next@^13.0.4: + version "13.0.4" + resolved "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.0.4.tgz" + integrity sha512-moEC7BW2TK7JKq3QfnaauqRjWzVcEf71gp5DbClpFPHM6QXE0u0uVvSTiHlmOgtCe1vyWAO+AhF87ZITd8mIDw== + dependencies: + "@next/eslint-plugin-next" "13.0.4" + "@rushstack/eslint-patch" "^1.1.3" + "@typescript-eslint/parser" "^5.42.0" + eslint-import-resolver-node "^0.3.6" + eslint-import-resolver-typescript "^3.5.2" + eslint-plugin-import "^2.26.0" + eslint-plugin-jsx-a11y "^6.5.1" + eslint-plugin-react "^7.31.7" + eslint-plugin-react-hooks "^4.5.0" + +eslint-config-prettier@^8.5.0: + version "8.5.0" + resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz" + integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== + +eslint-import-resolver-node@^0.3.6: + version "0.3.6" + resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz" + integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== + dependencies: + debug "^3.2.7" + resolve "^1.20.0" + +eslint-import-resolver-typescript@^3.5.2: + version "3.5.2" + resolved "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.2.tgz" + integrity sha512-zX4ebnnyXiykjhcBvKIf5TNvt8K7yX6bllTRZ14MiurKPjDpCAZujlszTdB8pcNXhZcOf+god4s9SjQa5GnytQ== + dependencies: + debug "^4.3.4" + enhanced-resolve "^5.10.0" + get-tsconfig "^4.2.0" + globby "^13.1.2" + is-core-module "^2.10.0" + is-glob "^4.0.3" + synckit "^0.8.4" + +eslint-module-utils@^2.7.3: + version "2.7.4" + resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz" + integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== + dependencies: + debug "^3.2.7" + +eslint-plugin-import@^2.26.0: + version "2.26.0" + resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz" + integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA== + dependencies: + array-includes "^3.1.4" + array.prototype.flat "^1.2.5" + debug "^2.6.9" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.6" + eslint-module-utils "^2.7.3" + has "^1.0.3" + is-core-module "^2.8.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.values "^1.1.5" + resolve "^1.22.0" + tsconfig-paths "^3.14.1" + +eslint-plugin-jsx-a11y@^6.5.1: + version "6.6.1" + resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz" + integrity sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q== + dependencies: + "@babel/runtime" "^7.18.9" + aria-query "^4.2.2" + array-includes "^3.1.5" + ast-types-flow "^0.0.7" + axe-core "^4.4.3" + axobject-query "^2.2.0" + damerau-levenshtein "^1.0.8" + emoji-regex "^9.2.2" + has "^1.0.3" + jsx-ast-utils "^3.3.2" + language-tags "^1.0.5" + minimatch "^3.1.2" + semver "^6.3.0" + +eslint-plugin-react-hooks@^4.5.0: + version "4.6.0" + resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz" + integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== + +eslint-plugin-react@^7.31.7: + version "7.31.8" + resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz" + integrity sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw== + dependencies: + array-includes "^3.1.5" + array.prototype.flatmap "^1.3.0" + doctrine "^2.1.0" + estraverse "^5.3.0" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.5" + object.fromentries "^2.0.5" + object.hasown "^1.1.1" + object.values "^1.1.5" + prop-types "^15.8.1" + resolve "^2.0.0-next.3" + semver "^6.3.0" + string.prototype.matchall "^4.0.7" + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@^8.23.1: + version "8.23.1" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.23.1.tgz" + integrity sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg== + dependencies: + "@eslint/eslintrc" "^1.3.2" + "@humanwhocodes/config-array" "^0.10.4" + "@humanwhocodes/gitignore-to-minimatch" "^1.0.2" + "@humanwhocodes/module-importer" "^1.0.1" + ajv "^6.10.0" + 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.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.4.0" + esquery "^1.4.0" + 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.1" + globals "^13.15.0" + globby "^11.1.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-sdsl "^4.1.4" + 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.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + +espree@^9.4.0: + version "9.4.0" + resolved "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz" + integrity sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw== + dependencies: + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.2: + version "3.3.2" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + 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" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +file-saver@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz" + integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +filter-obj@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz" + integrity sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng== + +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.7" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formik@^2.4.5: + version "2.4.6" + resolved "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz" + integrity sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g== + dependencies: + "@types/hoist-non-react-statics" "^3.3.1" + deepmerge "^2.1.1" + hoist-non-react-statics "^3.3.0" + lodash "^4.17.21" + lodash-es "^4.17.21" + react-fast-compare "^2.0.1" + tiny-warning "^1.0.2" + tslib "^2.0.0" + +fraction.js@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz" + integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +function-bind@^1.1.1, function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" + integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== + +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +get-tsconfig@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.2.0.tgz" + integrity sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg== + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.1, glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^10.3.10: + version "10.4.5" + resolved "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + 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" + +glob@^7.1.3, glob@7.1.7: + version "7.1.7" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globalize@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/globalize/-/globalize-0.1.1.tgz" + integrity sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA== + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.15.0: + version "13.17.0" + resolved "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz" + integrity sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw== + dependencies: + type-fest "^0.20.2" + +globalyzer@0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz" + integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +globby@^13.1.2: + version "13.1.2" + resolved "https://registry.npmjs.org/globby/-/globby-13.1.2.tgz" + integrity sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ== + dependencies: + dir-glob "^3.0.1" + fast-glob "^3.2.11" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^4.0.0" + +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + +graceful-fs@^4.2.11, graceful-fs@^4.2.4: + version "4.2.11" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + +immer@^10.0.3: + version "10.1.1" + resolved "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz" + integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + +intro.js-react@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/intro.js-react/-/intro.js-react-1.0.0.tgz" + integrity sha512-zR8pbTyX20RnCZpJMc0nuHBpsjcr1wFkj3ZookV6Ly4eE/LGpFTQwPsaA61Cryzwiy/tTFsusf4hPU9NpI9UOg== + +intro.js@^7.2.0: + version "7.2.0" + resolved "https://registry.npmjs.org/intro.js/-/intro.js-7.2.0.tgz" + integrity sha512-qbMfaB70rOXVBceIWNYnYTpVTiZsvQh/MIkfdQbpA9di9VBfj1GigUPfcCv3aOfsbrtPcri8vTLTA4FcEDcHSQ== + +invariant@^2.2.4: + 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" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-core-module@^2.10.0, is-core-module@^2.16.0, is-core-module@^2.8.1, is-core-module@^2.9.0: + version "2.16.1" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jiti@^1.21.6: + version "1.21.7" + resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz" + integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== + +js-sdsl@^4.1.4: + version "4.1.4" + resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz" + integrity sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +jsonwebtoken@^9.0.2: + 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" + +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2: + version "3.3.3" + resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz" + integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw== + dependencies: + array-includes "^3.1.5" + object.assign "^4.1.3" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +jwt-decode@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz" + integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== + +krustykrab@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/krustykrab/-/krustykrab-1.1.0.tgz" + integrity sha512-xpX9MPbw+nJseewe6who9Oq46RQwrBfps+dO/N4fSjJhsf2+y4XWC2kz46oBGX8yzMHyYJj35ug0X5s5yxB6tA== + +language-subtag-registry@~0.3.2: + version "0.3.22" + resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz" + integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== + +language-tags@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz" + integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ== + dependencies: + language-subtag-registry "~0.3.2" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lilconfig@^3.0.0, lilconfig@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz" + integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + +lodash.castarray@^4.4.0: + version "4.4.0" + resolved "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz" + integrity sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q== + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +loose-envify@^1.0.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +luxon@^3.2.1: + version "3.5.0" + resolved "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz" + integrity sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ== + +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4, micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mini-svg-data-uri@^1.2.3: + version "1.4.4" + resolved "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz" + integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg== + +minimatch@^3.0.4, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +moment-timezone@^0.5.40: + version "0.5.46" + resolved "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.46.tgz" + integrity sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw== + dependencies: + moment "^2.29.4" + +moment@^2.29.4: + version "2.30.1" + resolved "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + +ms@^2.1.1, ms@2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +nanoid@^3.3.6, nanoid@^3.3.7: + version "3.3.8" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +next@^14.1.0: + version "14.2.22" + resolved "https://registry.npmjs.org/next/-/next-14.2.22.tgz" + integrity sha512-Ps2caobQ9hlEhscLPiPm3J3SYhfwfpMqzsoCMZGWxt9jBRK9hoBZj2A37i8joKhsyth2EuVKDVJCTF5/H4iEDw== + dependencies: + "@next/env" "14.2.22" + "@swc/helpers" "0.5.5" + busboy "1.6.0" + caniuse-lite "^1.0.30001579" + graceful-fs "^4.2.11" + postcss "8.4.31" + styled-jsx "5.1.1" + optionalDependencies: + "@next/swc-darwin-arm64" "14.2.22" + "@next/swc-darwin-x64" "14.2.22" + "@next/swc-linux-arm64-gnu" "14.2.22" + "@next/swc-linux-arm64-musl" "14.2.22" + "@next/swc-linux-x64-gnu" "14.2.22" + "@next/swc-linux-x64-musl" "14.2.22" + "@next/swc-win32-arm64-msvc" "14.2.22" + "@next/swc-win32-ia32-msvc" "14.2.22" + "@next/swc-win32-x64-msvc" "14.2.22" + +node-releases@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz" + integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" + integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== + +numeral@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz" + integrity sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA== + +object-assign@^4.0.1, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + +object-inspect@^1.12.2, object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.3, object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.entries@^1.1.5: + version "1.1.5" + resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz" + integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +object.fromentries@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz" + integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +object.hasown@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz" + integrity sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A== + dependencies: + define-properties "^1.1.4" + es-abstract "^1.19.5" + +object.values@^1.1.5: + version "1.1.5" + resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz" + integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +open@^8.4.0: + version "8.4.0" + resolved "https://registry.npmjs.org/open/-/open-8.4.0.tgz" + integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + 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.3" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pirates@^4.0.1: + version "4.0.6" + resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + +postcss-import@^14.1.0: + version "14.1.0" + resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz" + integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-import@^15.1.0: + version "15.1.0" + resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz" + integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-js@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz" + integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== + dependencies: + camelcase-css "^2.0.1" + +postcss-load-config@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz" + integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== + dependencies: + lilconfig "^3.0.0" + yaml "^2.3.4" + +postcss-nested@^6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz" + integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== + dependencies: + postcss-selector-parser "^6.1.1" + +postcss-selector-parser@^6.1.1: + version "6.1.2" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-selector-parser@^6.1.2: + version "6.1.2" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-selector-parser@6.0.10: + version "6.0.10" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz" + integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.4.4, postcss@^8.4.47: + version "8.4.49" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz" + integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== + dependencies: + nanoid "^3.3.7" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +postcss@8.4.31: + version "8.4.31" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier@^3.2.4: + version "3.4.2" + resolved "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz" + integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ== + +prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +query-string@^8.1.0: + version "8.2.0" + resolved "https://registry.npmjs.org/query-string/-/query-string-8.2.0.tgz" + integrity sha512-tUZIw8J0CawM5wyGBiDOAp7ObdRQh4uBor/fUR9ZjmbZVvw95OD9If4w3MQxr99rg0DJZ/9CIORcpEqU5hQG7g== + dependencies: + decode-uri-component "^0.4.1" + filter-obj "^5.1.0" + split-on-first "^3.0.0" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +react-apexcharts@^1.4.1: + version "1.7.0" + resolved "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.7.0.tgz" + integrity sha512-03oScKJyNLRf0Oe+ihJxFZliBQM9vW3UWwomVn4YVRTN1jsIR58dLWt0v1sb8RwJVHDMbeHiKQueM0KGpn7nOA== + dependencies: + prop-types "^15.8.1" + +react-big-calendar@^1.10.3: + version "1.17.1" + resolved "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.17.1.tgz" + integrity sha512-LltUAMSGODWQBKx4013bRe6R0jaINV9hrs970+F860KedpozwRGGMT66esV9mA3mAhfSKoazF/QH1WCyLkXYZA== + dependencies: + "@babel/runtime" "^7.20.7" + clsx "^1.2.1" + date-arithmetic "^4.1.0" + dayjs "^1.11.7" + dom-helpers "^5.2.1" + globalize "^0.1.1" + invariant "^2.2.4" + lodash "^4.17.21" + lodash-es "^4.17.21" + luxon "^3.2.1" + memoize-one "^6.0.0" + moment "^2.29.4" + moment-timezone "^0.5.40" + prop-types "^15.8.1" + react-overlays "^5.2.1" + uncontrollable "^7.2.1" + +react-chartjs-2@^4.3.1: + version "4.3.1" + resolved "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-4.3.1.tgz" + integrity sha512-5i3mjP6tU7QSn0jvb8I4hudTzHJqS8l00ORJnVwI2sYu0ihpj83Lv2YzfxunfxTZkscKvZu2F2w9LkwNBhj6xA== + +react-datepicker@^4.10.0: + version "4.25.0" + resolved "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.25.0.tgz" + integrity sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg== + dependencies: + "@popperjs/core" "^2.11.8" + classnames "^2.2.6" + date-fns "^2.30.0" + prop-types "^15.7.2" + react-onclickoutside "^6.13.0" + react-popper "^2.3.0" + +react-dom@^19.0.0: + version "19.0.0" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz" + integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ== + dependencies: + scheduler "^0.25.0" + +react-fast-compare@^2.0.1: + version "2.0.4" + resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz" + integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== + +react-fast-compare@^3.0.1: + version "3.2.2" + resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz" + integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== + +react-is@^16.13.1, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +react-is@^19.0.0: + version "19.0.0" + resolved "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz" + integrity sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g== + +react-lifecycles-compat@^3.0.4: + 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== + +react-onclickoutside@^6.13.0: + version "6.13.1" + resolved "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.1.tgz" + integrity sha512-LdrrxK/Yh9zbBQdFbMTXPp3dTSN9B+9YJQucdDu3JNKRrbdU+H+/TVONJoWtOwy4II8Sqf1y/DTI6w/vGPYW0w== + +react-overlays@^5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz" + integrity sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA== + dependencies: + "@babel/runtime" "^7.13.8" + "@popperjs/core" "^2.11.6" + "@restart/hooks" "^0.4.7" + "@types/warning" "^3.0.0" + dom-helpers "^5.2.0" + prop-types "^15.7.2" + uncontrollable "^7.2.1" + warning "^4.0.3" + +react-popper@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz" + integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q== + dependencies: + react-fast-compare "^3.0.1" + warning "^4.0.2" + +react-redux@^8.0.2: + version "8.0.2" + resolved "https://registry.npmjs.org/react-redux/-/react-redux-8.0.2.tgz" + integrity sha512-nBwiscMw3NoP59NFCXFf02f8xdo+vSHT/uZ1ldDwF7XaTpzm+Phk97VT4urYBl5TYAPNVaFm12UHAEyzkpNzRA== + dependencies: + "@babel/runtime" "^7.12.1" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/use-sync-external-store" "^0.0.3" + hoist-non-react-statics "^3.3.2" + react-is "^18.0.0" + use-sync-external-store "^1.0.0" + +react-select-async-paginate@^0.7.9: + version "0.7.9" + resolved "https://registry.npmjs.org/react-select-async-paginate/-/react-select-async-paginate-0.7.9.tgz" + integrity sha512-oGdmXWatj06MNUcLwFkyt8qLBA0km0gFvDF52qaQQEArRjprbTajaYwhtjVotKTw1IXfw3fiq3G9Lgsjfpjz/Q== + dependencies: + "@seznam/compose-react-refs" "^1.0.6" + "@vtaits/use-lazy-ref" "^0.1.3" + krustykrab "^1.1.0" + sleep-promise "^9.1.0" + use-is-mounted-ref "^1.5.0" + use-latest "^1.3.0" + +react-select@^5.7.0: + version "5.9.0" + resolved "https://registry.npmjs.org/react-select/-/react-select-5.9.0.tgz" + integrity sha512-nwRKGanVHGjdccsnzhFte/PULziueZxGD8LL2WojON78Mvnq7LdAMEtu2frrwld1fr3geixg3iiMBIc/LLAZpw== + dependencies: + "@babel/runtime" "^7.12.0" + "@emotion/cache" "^11.4.0" + "@emotion/react" "^11.8.1" + "@floating-ui/dom" "^1.0.1" + "@types/react-transition-group" "^4.4.0" + memoize-one "^6.0.0" + prop-types "^15.6.0" + react-transition-group "^4.3.0" + use-isomorphic-layout-effect "^1.2.0" + +react-switch@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/react-switch/-/react-switch-7.1.0.tgz" + integrity sha512-4xVeyImZE8QOTDw2FmhWz0iqo2psoRiS7XzdjaZBCIP8Dzo3rT0esHUjLee5WsAPSFXWWl1eVA5arp9n2C6yQA== + dependencies: + prop-types "^15.7.2" + +react-toastify@^9.1.2: + version "9.1.3" + resolved "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz" + integrity sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg== + dependencies: + clsx "^1.1.1" + +react-transition-group@^4.3.0, react-transition-group@^4.4.5: + 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" + +react@^19.0.0: + version "19.0.0" + resolved "https://registry.npmjs.org/react/-/react-19.0.0.tgz" + integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ== + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz" + integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== + dependencies: + pify "^2.3.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +redux-thunk@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz" + integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw== + +redux@^4.0.0: + version "4.2.0" + resolved "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz" + integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== + dependencies: + "@babel/runtime" "^7.9.2" + +redux@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz" + integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== + +regenerator-runtime@^0.13.4: + version "0.13.9" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3: + version "1.4.3" + resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + functions-have-names "^1.2.2" + +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +reselect@^4.1.8: + version "4.1.8" + resolved "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz" + integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== + +reselect@^5.1.0: + version "5.1.1" + resolved "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz" + integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.1.7, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.8: + version "1.22.10" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + dependencies: + is-core-module "^2.16.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^2.0.0-next.3: + version "2.0.0-next.4" + resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz" + integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@^5.0.1: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + +scheduler@^0.25.0: + version "0.25.0" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz" + integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA== + +semver@^6.3.0: + version "6.3.0" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.3.7, semver@^7.5.4: + version "7.6.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + +sleep-promise@^9.1.0: + version "9.1.0" + resolved "https://registry.npmjs.org/sleep-promise/-/sleep-promise-9.1.0.tgz" + integrity sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA== + +source-map-js@^1.0.2, source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +split-on-first@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz" + integrity sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.matchall@^4.0.7: + version "4.0.7" + resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz" + integrity sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + get-intrinsic "^1.1.1" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + regexp.prototype.flags "^1.4.1" + side-channel "^1.0.4" + +string.prototype.trimend@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz" + integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + +string.prototype.trimstart@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz" + integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +styled-jsx@5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz" + integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== + dependencies: + client-only "0.0.1" + +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + +sucrase@^3.35.0: + version "3.35.0" + resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz" + integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== + 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" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svg.draggable.js@^2.2.2: + version "2.2.2" + resolved "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz" + integrity sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw== + dependencies: + svg.js "^2.0.1" + +svg.easing.js@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz" + integrity sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA== + dependencies: + svg.js ">=2.3.x" + +svg.filter.js@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz" + integrity sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw== + dependencies: + svg.js "^2.2.5" + +svg.js@^2.0.1, svg.js@^2.2.5, svg.js@^2.4.0, svg.js@^2.6.5, svg.js@>=2.3.x: + version "2.7.1" + resolved "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz" + integrity sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA== + +svg.pathmorphing.js@^0.1.3: + version "0.1.3" + resolved "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz" + integrity sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww== + dependencies: + svg.js "^2.4.0" + +svg.resize.js@^1.4.3: + version "1.4.3" + resolved "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz" + integrity sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw== + dependencies: + svg.js "^2.6.5" + svg.select.js "^2.1.2" + +svg.select.js@^2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz" + integrity sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ== + dependencies: + svg.js "^2.2.5" + +svg.select.js@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz" + integrity sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw== + dependencies: + svg.js "^2.6.5" + +swr@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/swr/-/swr-1.3.0.tgz" + integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw== + +synckit@^0.8.4: + version "0.8.4" + resolved "https://registry.npmjs.org/synckit/-/synckit-0.8.4.tgz" + integrity sha512-Dn2ZkzMdSX827QbowGbU/4yjWuvNaCoScLLoMo/yKbu+P4GBR6cRGKZH27k6a9bRzdqcyd1DE96pQtQ6uNkmyw== + dependencies: + "@pkgr/utils" "^2.3.1" + tslib "^2.4.0" + +tailwindcss@^3.4.1: + version "3.4.17" + resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz" + integrity sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og== + dependencies: + "@alloc/quick-lru" "^5.2.0" + arg "^5.0.2" + chokidar "^3.6.0" + didyoumean "^1.2.2" + dlv "^1.1.3" + fast-glob "^3.3.2" + glob-parent "^6.0.2" + is-glob "^4.0.3" + jiti "^1.21.6" + lilconfig "^3.1.3" + micromatch "^4.0.8" + normalize-path "^3.0.0" + object-hash "^3.0.0" + picocolors "^1.1.1" + postcss "^8.4.47" + postcss-import "^15.1.0" + postcss-js "^4.0.1" + postcss-load-config "^4.0.2" + postcss-nested "^6.2.0" + postcss-selector-parser "^6.1.2" + resolve "^1.22.8" + sucrase "^3.35.0" + +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + +tiny-glob@^0.2.9: + version "0.2.9" + resolved "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz" + integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg== + dependencies: + globalyzer "0.1.0" + globrex "^0.1.2" + +tiny-warning@^1.0.2: + version "1.0.3" + resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + +"tinymce@^6.0.0 || ^5.5.1": + version "6.8.5" + resolved "https://registry.npmjs.org/tinymce/-/tinymce-6.8.5.tgz" + integrity sha512-qAL/FxL7cwZHj4BfaF818zeJJizK9jU5IQzTcSLL4Rj5MaJdiVblEj7aDr80VCV1w9h4Lak9hlnALhq/kVtN1g== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-interface-checker@^0.1.9: + version "0.1.13" + resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== + +tsconfig-paths@^3.14.1: + version "3.14.1" + resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.0.0, tslib@^2.4.0: + version "2.4.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +typescript@^4.8.3: + version "4.8.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz" + integrity sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig== + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +uncontrollable@^7.2.1: + 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" + +update-browserslist-db@^1.0.5: + version "1.0.9" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz" + integrity sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +use-is-mounted-ref@^1.5.0: + version "1.5.0" + resolved "https://registry.npmjs.org/use-is-mounted-ref/-/use-is-mounted-ref-1.5.0.tgz" + integrity sha512-p5FksHf/ospZUr5KU9ese6u3jp9fzvZ3wuSb50i0y6fdONaHWgmOqQtxR/PUcwi6hnhQDbNxWSg3eTK3N6m+dg== + +use-isomorphic-layout-effect@^1.1.1, use-isomorphic-layout-effect@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz" + integrity sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w== + +use-latest@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz" + integrity sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ== + dependencies: + use-isomorphic-layout-effect "^1.1.1" + +use-sync-external-store@^1.0.0: + version "1.2.0" + resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + +util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +uuid@^9.0.0: + version "9.0.1" + resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + +warning@^4.0.2, warning@^4.0.3: + 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" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + 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" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yaml@^2.3.4: + version "2.7.0" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz" + integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==