diff --git a/.gitignore b/.gitignore index 61debd9ec..bfe0efc46 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ node_modules .DS_Store scripts/sync-production-to-local.sh -scripts/sync-staging-to-dev.sh -scripts/sync-staging-to-local.sh \ No newline at end of file +scripts/sync-production-to-dev.sh +scripts/sync-production-to-staging.sh \ No newline at end of file diff --git a/backend/common/errors/errorHandler.js b/backend/common/errors/errorHandler.js index dc4389077..7c0983983 100644 --- a/backend/common/errors/errorHandler.js +++ b/backend/common/errors/errorHandler.js @@ -40,9 +40,7 @@ const errorHandler = (error, request, response, next) => { } } } - console.log('ERROR', error.message); - - // console.error('Unexpected error', error.toJSON()); + console.log('FOOBAR', error.message); return response.status(500).json({ message: 'Unexpected error', diff --git a/backend/common/services/sendgrid.js b/backend/common/services/sendgrid.js index 33256e379..e3e12d05e 100644 --- a/backend/common/services/sendgrid.js +++ b/backend/common/services/sendgrid.js @@ -1,6 +1,7 @@ const sgMail = require('@sendgrid/mail'); const sgClient = require('@sendgrid/client'); const _ = require('lodash'); +const moment = require('moment'); sgMail.setApiKey(global.gConfig.SENDGRID_API_KEY); sgClient.setApiKey(global.gConfig.SENDGRID_API_KEY); @@ -34,15 +35,46 @@ const sendgridAddRecipientsToList = (list_id, recipient_ids) => { }; const SendgridService = { - buildAcceptanceEmail: (to, { event_name }) => { - return SendgridService.buildTemplateMessage(to, global.gConfig.SENDGRID_ACCEPTED_TEMPLATE, { - event_name + sendAcceptanceEmail: (event, user) => { + const msg = SendgridService.buildTemplateMessage(user.email, global.gConfig.SENDGRID_ACCEPTED_TEMPLATE, { + event_name: event.name, + first_name: user.firstName, + dashboard_link: `${global.gConfig.FRONTEND_URL}/dashboard/${event.slug}` }); + return SendgridService.send(msg); }, - buildRejectionEmail: (to, { event_name }) => { - return SendgridService.buildTemplateMessage(to, global.gConfig.SENDGRID_REJECTED_TEMPLATE, { - event_name + sendRejectionEmail: (event, user) => { + return Promise.resolve(); + }, + sendRegisteredEmail: (event, user) => { + const msg = SendgridService.buildTemplateMessage(user.email, global.gConfig.SENDGRID_GENERIC_TEMPLATE, { + header_image: event.logo.url, + subject: `Thanks for registering to ${event.name}!`, + subtitle: 'Awesome! Now just sit back and relax.', + body: `The application period ends ${moment(event.registrationEndTime).format( + 'MMMM Do' + )}, and we'll process all applications by ${moment(event.registrationEndTime) + .add(5, 'days') + .format( + 'MMMM Do' + )}.

We'll send you an email once we've made the decision, but in the meantime you can click the link below to access your event dashboard, where you'll be able to see your registration status in real-time. If you're applying as a team, the event dashboard is where you can create and manage your team as well.`, + cta_text: 'Event dashboard', + cta_link: `${global.gConfig.FRONTEND_URL}/dashboard/${event.slug}` }); + + return SendgridService.send(msg); + }, + sendGenericEmail: (to, params) => { + const msg = SendgridService.buildTemplateMessage(to, global.gConfig.SENDGRID_GENERIC_TEMPLATE, { + subject: params.subject, + subtitle: params.subtitle, + header_image: params.header_image, + body: params.body, + cta_text: params.cta_text, + cta_link: params.cta_link + }); + + return SendgridService.send(msg); }, buildTemplateMessage: (to, templateId, data) => { return { diff --git a/backend/misc/config.js b/backend/misc/config.js index baa5409b9..05213d198 100644 --- a/backend/misc/config.js +++ b/backend/misc/config.js @@ -66,6 +66,10 @@ const settings = { value: process.env.SENDGRID_REJECTED_TEMPLATE, required: true }, + SENDGRID_GENERIC_TEMPLATE: { + value: process.env.SENDGRID_GENERIC_TEMPLATE, + required: true + }, FRONTEND_URL: { value: process.env.FRONTEND_URL, default: '', @@ -74,6 +78,10 @@ const settings = { ENVIRONMENT_TAG: { value: process.env.ENVIRONMENT_TAG, default: 'none' + }, + DEVTOOLS_ENABLED: { + value: process.env.DEVTOOLS_ENABLED === 'true' && process.env.NODE_ENV !== 'production', + default: false } }; @@ -81,7 +89,7 @@ const buildConfig = () => { const config = {}; _.forOwn(settings, (obj, key) => { if (!obj.value) { - if (obj.default) { + if (obj.default || obj.default === false) { config[key] = obj.default; } else { throw new Error( @@ -94,7 +102,6 @@ const buildConfig = () => { }); console.log('Running app with config', config); - console.log('Running app with env', process.env); return config; }; diff --git a/backend/modules/devtools/routes.js b/backend/modules/devtools/routes.js new file mode 100644 index 000000000..8b1dff554 --- /dev/null +++ b/backend/modules/devtools/routes.js @@ -0,0 +1,35 @@ +const express = require('express'); +const router = express.Router(); +const Registration = require('../registration/model'); + +router.route('/').get((req, res) => { + return res.status(200).send('DEVTOOLS HERE'); +}); + +router.route('/anonymize-registrations').get(async (req, res) => { + const registrations = await Registration.find({}); + + const updates = registrations.map(registration => { + return { + updateOne: { + filter: { + _id: registration._id + }, + update: { + $set: { + 'answers.firstName': 'Anonymous', + 'answers.lastName': 'Owl', + 'answers.email': + 'juuso.lappalainen+' + Math.floor(Math.random() * 1000000) + '@hackjunction.com' + } + } + } + }; + }); + + const result = await Registration.bulkWrite(updates); + + return res.status(200).json(result); +}); + +module.exports = router; diff --git a/backend/modules/email-task/controller.js b/backend/modules/email-task/controller.js index ff83fe1b8..8080c5fe0 100644 --- a/backend/modules/email-task/controller.js +++ b/backend/modules/email-task/controller.js @@ -1,55 +1,111 @@ const EmailTask = require('./model'); const SendgridService = require('../../common/services/sendgrid'); const EmailTypes = require('./types'); +const EventController = require('../event/controller'); +const UserController = require('../user-profile/controller'); +const shortid = require('shortid'); +const Promise = require('bluebird'); const controller = {}; -controller.createTask = (msg, taskParams) => { +controller.createTask = (userId, eventId, type, params, schedule) => { const task = new EmailTask({ - message: msg, - ...taskParams + user: userId, + event: eventId, + type: type }); + if (schedule) { + task.schedule = schedule; + } + + if (params) { + task.params = params; + } return task.save().catch(err => { if (err.code === 11000) { - //The task already exists, so it's ok + console.log('ALREADY EXISTS'); return Promise.resolve(); } // For other types of errors, we'll want to throw the error normally - return Promise.reject(); + return Promise.reject(err); }); }; -controller.sendEmail = (msg, taskParams) => { - return SendgridService.send(msg).catch(message => { - /** If sending the email fails, save a task to the DB and we can retry later */ - return controller.createTask(message, taskParams); +controller.createAcceptedTask = async (userId, eventId, deliverNow = false) => { + const task = await controller.createTask(userId, eventId, EmailTypes.registrationAccepted); + if (deliverNow) { + return controller.deliverEmailTask(task); + } + return task; +}; + +controller.createRejectedTask = async (userId, eventId, deliverNow = false) => { + const task = await controller.createTask(userId, eventId, EmailTypes.registrationRejected); + if (deliverNow) { + return controller.deliverEmailTask(task); + } + return task; +}; + +controller.createRegisteredTask = async (userId, eventId, deliverNow = false) => { + const task = await controller.createTask(userId, eventId, EmailTypes.registrationReceived); + if (task && deliverNow) { + return controller.deliverEmailTask(task); + } + return task; +}; + +controller.createGenericTask = async (userId, eventId, uniqueId, msgParams, deliverNow = false) => { + if (!uniqueId) { + uniqueId = shortid.generate(); + } + const task = await controller.createTask(userId, eventId, 'generic_' + uniqueId, msgParams); + if (task && deliverNow) { + return controller.deliverEmailTask(task); + } + return task; +}; + +controller.deliverEmailTask = async task => { + const [user, event] = await Promise.all([ + UserController.getUserProfile(task.user), + EventController.getEventById(task.event) + ]); + switch (task.type) { + case EmailTypes.registrationAccepted: { + await SendgridService.sendAcceptanceEmail(event, user); + break; + } + case EmailTypes.registrationRejected: { + await SendgridService.sendRejectionEmail(event, user); + break; + } + case EmailTypes.registrationReceived: { + await SendgridService.sendRegisteredEmail(event, user); + break; + } + default: { + await SendgridService.sendGenericEmail(user.email, task.params); + break; + } + } + + /** Here we'll have success so we can set the task as delivered */ + task.deliveredAt = Date.now(); + return task.save(); +}; + +controller.sendPreviewEmail = async (to, msgParams) => { + return SendgridService.sendGenericEmail(to, msgParams).catch(err => { + return; }); }; -controller.sendAcceptanceEmail = (event, user) => { - const msgParams = { - event_name: event.name - }; - const msg = SendgridService.buildAcceptanceEmail(user.email, msgParams); - const taskParams = { - userId: user.userId, - eventId: event._id, - type: EmailTypes.registrationAccepted - }; - return controller.sendEmail(msg, taskParams); -}; - -controller.sendRejectionEmail = (event, user) => { - const msgParams = { - event_name: event.name - }; - const msg = SendgridService.buildRejectionEmail(user.email, msgParams); - const taskParams = { - userId: user.userId, - eventId: event._id, - type: EmailTypes.registrationRejected - }; - return controller.sendEmail(msg, taskParams); +controller.sendBulkEmail = async (recipients, msgParams, event, uniqueId) => { + const promises = recipients.map(recipient => { + return controller.createGenericTask(recipient, event._id.toString(), uniqueId, msgParams, true); + }); + return Promise.all(promises); }; module.exports = controller; diff --git a/backend/modules/email-task/model.js b/backend/modules/email-task/model.js index fd15ca675..2dac2e93c 100644 --- a/backend/modules/email-task/model.js +++ b/backend/modules/email-task/model.js @@ -1,24 +1,37 @@ const mongoose = require('mongoose'); const EmailTaskSchema = new mongoose.Schema({ - message: mongoose.Mixed, - deliveredAt: { - type: Date + params: { + type: mongoose.Schema.Types.Mixed, + default: null }, schedule: { type: Date, default: Date.now }, - eventId: String, - userId: String, - type: String + deliveredAt: { + type: Date, + default: null + }, + event: { + type: String, + required: true + }, + user: { + type: String, + required: true + }, + type: { + type: String, + required: true + } }); EmailTaskSchema.set('timestamps', true); EmailTaskSchema.index( { - eventId: 1, - userId: 1, + event: 1, + user: 1, type: 1 }, { diff --git a/backend/modules/email-task/routes.js b/backend/modules/email-task/routes.js new file mode 100644 index 000000000..5a8fd15df --- /dev/null +++ b/backend/modules/email-task/routes.js @@ -0,0 +1,30 @@ +const express = require('express'); +const router = express.Router(); +const asyncHandler = require('express-async-handler'); +const { Auth } = require('@hackjunction/shared'); + +const { hasToken } = require('../../common/middleware/token'); +const { hasPermission } = require('../../common/middleware/permissions'); +const { isEventOrganiser } = require('../../common/middleware/events'); + +const EmailTaskController = require('./controller'); + +const sendPreviewEmail = asyncHandler(async (req, res) => { + await EmailTaskController.sendPreviewEmail(req.body.to, req.body.params); + return res.status(200).json({}); +}); + +const sendBulkEmail = asyncHandler(async (req, res) => { + await EmailTaskController.sendBulkEmail(req.body.recipients, req.body.params, req.event, req.body.uniqueId); + return res.status(200).json({}); +}); + +router + .route('/:slug/preview') + .post(hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOrganiser, sendPreviewEmail); + +router + .route('/:slug/send') + .post(hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOrganiser, sendBulkEmail); + +module.exports = router; diff --git a/backend/modules/email-task/types.js b/backend/modules/email-task/types.js index 6877e7b0f..4f8983f62 100644 --- a/backend/modules/email-task/types.js +++ b/backend/modules/email-task/types.js @@ -1,6 +1,7 @@ const EmailTypes = { registrationAccepted: 'registration-accepted', - registrationRejected: 'registration-rejected' + registrationRejected: 'registration-rejected', + registrationReceived: 'registration-received' }; module.exports = EmailTypes; diff --git a/backend/modules/event/routes.js b/backend/modules/event/routes.js index 2ce9ea0fd..c49a6a9e2 100644 --- a/backend/modules/event/routes.js +++ b/backend/modules/event/routes.js @@ -39,17 +39,6 @@ const updateEvent = asyncHandler(async (req, res) => { return res.status(200).json(updatedEvent); }); -const getEventStats = asyncHandler(async (req, res) => { - const eventId = req.event._id.toString(); - const registrationStats = await RegistrationController.getRegistrationStatsForEvent(eventId); - const teamStats = await TeamController.getTeamStatsForEvent(eventId); - - return res.status(200).json({ - ...registrationStats, - ...teamStats - }); -}); - const getEventAsOrganiser = asyncHandler(async (req, res) => { const event = await EventController.getEventBySlug(req.event.slug); return res.status(200).json(event); @@ -105,10 +94,6 @@ router .patch(hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOrganiser, updateEvent) .delete(hasToken, hasPermission(Auth.Permissions.DELETE_EVENT), isEventOwner, deleteEvent); -router - .route('/:slug/stats') - .get(hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOrganiser, getEventStats); - /** Get organisers for single event */ router.get('/organisers/:slug', hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOwner, getOrganisers); diff --git a/backend/modules/registration/controller.js b/backend/modules/registration/controller.js index c7ff08e54..2e9fbbc99 100644 --- a/backend/modules/registration/controller.js +++ b/backend/modules/registration/controller.js @@ -1,12 +1,12 @@ const _ = require('lodash'); -const moment = require('moment'); - +const Promise = require('bluebird'); +const { RegistrationStatuses } = require('@hackjunction/shared'); const Registration = require('./model'); -const { NotFoundError } = require('../../common/errors/errors'); +const { NotFoundError, ForbiddenError } = require('../../common/errors/errors'); const UserProfileController = require('../user-profile/controller'); const RegistrationHelpers = require('./helpers'); -const { RegistrationStatuses } = require('@hackjunction/shared'); +const STATUSES = RegistrationStatuses.asObject; const controller = {}; controller.getUserRegistrations = user => { @@ -46,28 +46,51 @@ controller.updateRegistration = (user, event, data) => { }); }; -controller.getRegistrationsForEvent = eventId => { - return Registration.find( - { - event: eventId +controller.confirmRegistration = (user, event) => { + return controller.getRegistration(user.sub, event._id.toString()).then(registration => { + if (registration.status === STATUSES.accepted.id) { + registration.status = STATUSES.confirmed.id; + return registration.save(); } - // { - // user: 1, - // rating: 1, - // ratedBy: 1, - // assignedTo: 1, - // tags: 1, - // status: 1, - // 'answers.email': 1, - // 'answers.firstName': 1, - // 'answers.lastName': 1 - // } - ); + + throw new ForbiddenError('Only accepted registrations can be confirmed'); + }); +}; + +controller.cancelRegistration = (user, event) => { + return controller.getRegistration(user.sub, event._id.toString()).then(registration => { + if (registration.status === STATUSES.confirmed.id) { + registration.status = STATUSES.cancelled.id; + return registration.save(); + } + + throw new ForbiddenError('Only confirmed registrations can be cancelled'); + }); }; -controller.searchRegistrationsForEvent = (eventId, userId, params) => { - const aggregationSteps = RegistrationHelpers.buildAggregation(eventId, userId, params); - return Registration.aggregate(aggregationSteps); +controller.getRegistrationsForEvent = eventId => { + return Registration.find({ + event: eventId + }).then(registrations => { + /** Do some minor optimisation here to cut down on size */ + return registrations.map(reg => { + reg.answers = _.mapValues(reg.answers, answer => { + if (typeof answer === 'string' && answer.length > 50) { + return answer.slice(0, 10) + '...'; + } + if (typeof answer === 'object' && Object.keys(answer).length > 0) { + return _.mapValues(answer, subAnswer => { + if (typeof subAnswer === 'string' && subAnswer.length > 50) { + return subAnswer.slice(0, 10); + } + return subAnswer; + }); + } + return answer; + }); + return reg; + }); + }); }; controller.selfAssignRegistrationsForEvent = (eventId, userId) => { @@ -75,27 +98,21 @@ controller.selfAssignRegistrationsForEvent = (eventId, userId) => { rating: null, assignedTo: null }) - .sort([['createdAt', -1]]) + .sort({ createdAt: 'asc' }) .limit(10) .then(registrations => { - const updates = registrations.map(reg => { - return { - updateOne: { - filter: { - _id: reg._id - }, - update: { - assignedTo: userId - } + const registrationIds = registrations.map(r => r._id.toString()); + return Registration.updateMany( + { + event: eventId, + _id: { + $in: registrationIds } - }; - }); - - if (updates.length === 0) { - return 0; - } - - return Registration.bulkWrite(updates).then(data => { + }, + { + assignedTo: userId + } + ).then(data => { return data.nModified; }); }); @@ -131,14 +148,6 @@ controller.getFullRegistration = (eventId, registrationId) => { }); }; -controller.rateRegistration = (registrationId, event, user, rating) => { - return controller.getFullRegistration(event._id.toString(), registrationId).then(registration => { - registration.rating = rating; - registration.ratedBy = user.sub; - return registration.save(); - }); -}; - controller.editRegistration = (registrationId, event, data, user) => { return controller.getFullRegistration(event._id.toString(), registrationId).then(registration => { registration.status = data.status; @@ -150,73 +159,28 @@ controller.editRegistration = (registrationId, event, data, user) => { }); }; -controller.acceptRegistration = (registrationId, event) => { - return controller.getFullRegistration(event._id.toString(), registrationId).then(registration => { - registration.status = RegistrationStatuses.asObject.accepted.id; - return registration.save(); - }); -}; - -controller.rejectRegistration = (registrationId, event) => { - return controller.getFullRegistration(event._id.toString(), registrationId).then(registration => { - registration.status = RegistrationStatuses.asObject.rejected.id; - return registration.save(); - }); -}; - controller.getFullRegistrationsForEvent = eventId => { return Registration.find({ event: eventId }); }; -controller.getRegistrationStatsForEvent = async eventId => { - const registrations = await Registration.find({ event: eventId }, [ - 'answers.firstName', - 'answers.lastName', - 'answers.secretCode', - 'rating', - 'ratedBy', - 'createdAt' - ]); - - const registrationsByDay = _.countBy(registrations, r => moment(r.createdAt).format('YYYY-MM-DD')); - - const numRegistrations = registrations.length; - const numRegistrationsLastDay = registrations.filter(r => { - return Date.now() - r.createdAt < 24 * 60 * 60 * 1000; - }).length; - const reviewedRegistrations = registrations.filter(r => { - return !isNaN(r.rating); - }); - const registrationsByReviewer = _.countBy(reviewedRegistrations, r => r.ratedBy); - const numRegistrationsReviewed = reviewedRegistrations.length; - const registrationsLastFive = _.sortBy(registrations, r => r.createdAt).slice(-5); - const topSecretCodes = _.countBy(registrations, r => r.answers.secretCode); - const topSecretCodesArray = []; - _.forOwn(topSecretCodes, (count, code) => { - if (!_.isEmpty(code) && code !== 'undefined') { - topSecretCodesArray.push({ - code, - count - }); - } +controller.acceptSoftAccepted = async eventId => { + const users = await Registration.find({ event: eventId, status: RegistrationStatuses.asObject.softAccepted.id }); + const accepted = await Promise.each(users, user => { + user.status = RegistrationStatuses.asObject.accepted.id; + user.save(); }); + return accepted; +}; - const registrationsAvgRating = _.meanBy(reviewedRegistrations, 'rating'); - const registrationsSplit = _.countBy(reviewedRegistrations, 'rating'); - - return { - numRegistrations, - numRegistrationsLastDay, - numRegistrationsReviewed, - registrationsByDay, - registrationsByReviewer, - registrationsAvgRating, - registrationsLastFive, - registrationsSplit, - registrationsTopSecretCodes: _.sortBy(topSecretCodesArray, item => item.count * -1) - }; +controller.rejectSoftRejected = async eventId => { + const users = await Registration.find({ event: eventId, status: RegistrationStatuses.asObject.softRejected.id }); + const rejected = await Promise.each(users, user => { + user.status = RegistrationStatuses.asObject.rejected.id; + user.save(); + }); + return rejected; }; module.exports = controller; diff --git a/backend/modules/registration/model.js b/backend/modules/registration/model.js index e21f5b687..ffddd9cf0 100644 --- a/backend/modules/registration/model.js +++ b/backend/modules/registration/model.js @@ -1,7 +1,8 @@ const mongoose = require('mongoose'); const _ = require('lodash'); -const RegistrationStatuses = require('@hackjunction/shared'); +const { RegistrationStatuses } = require('@hackjunction/shared'); const updateAllowedPlugin = require('../../common/plugins/updateAllowed'); +const EmailTaskController = require('../email-task/controller'); const RegistrationSchema = new mongoose.Schema({ event: { @@ -14,7 +15,11 @@ const RegistrationSchema = new mongoose.Schema({ status: { type: String, enum: RegistrationStatuses.ids, - default: 'pending' + default: RegistrationStatuses.asObject.pending.id, + set: function(status) { + this._previousStatus = this.status; + return status; + } }, assignedTo: { type: String @@ -43,6 +48,35 @@ RegistrationSchema.plugin(updateAllowedPlugin, { blacklisted: ['__v', '_id', 'event', 'user', 'createdAt', 'updatedAt'] }); +RegistrationSchema.pre('save', function(next) { + this._wasNew = this.isNew; + next(); +}); + +/** Trigger email sending on status changes etc. */ +RegistrationSchema.post('save', function(doc, next) { + const SOFT_ACCEPTED = RegistrationStatuses.asObject.softAccepted.id; + const ACCEPTED = RegistrationStatuses.asObject.accepted.id; + const SOFT_REJECTED = RegistrationStatuses.asObject.softRejected.id; + const REJECTED = RegistrationStatuses.asObject.rejected.id; + /** If a registration was just created, create an email notification about it */ + if (this._wasNew) { + EmailTaskController.createRegisteredTask(doc.user, doc.event, true); + } + + /** If a registration is accepted, create an email notification about it */ + if (this._previousStatus === SOFT_ACCEPTED && this.status === ACCEPTED) { + EmailTaskController.createAcceptedTask(doc.user, doc.event, true); + } + + /** If a registration is rejected, create an email notification about it */ + if (this._previousStatus === SOFT_REJECTED && this.status === REJECTED) { + EmailTaskController.createRejectedTask(doc.user, doc.event, true); + } + + next(); +}); + RegistrationSchema.index({ event: 1, user: 1 }, { unique: true }); const Registration = mongoose.model('Registration', RegistrationSchema); diff --git a/backend/modules/registration/routes.js b/backend/modules/registration/routes.js index e0c3eb4a1..2fe447920 100644 --- a/backend/modules/registration/routes.js +++ b/backend/modules/registration/routes.js @@ -1,15 +1,13 @@ const express = require('express'); const router = express.Router(); const asyncHandler = require('express-async-handler'); -const { Auth } = require('@hackjunction/shared'); +const { Auth, RegistrationStatuses } = require('@hackjunction/shared'); const RegistrationController = require('./controller'); const EventController = require('../event/controller'); -const UserProfileController = require('../user-profile/controller'); -const EmailTaskController = require('../email-task/controller'); const { hasToken } = require('../../common/middleware/token'); const { hasPermission } = require('../../common/middleware/permissions'); -const { canRegisterToEvent, isEventOrganiser } = require('../../common/middleware/events'); +const { canRegisterToEvent, hasRegisteredToEvent, isEventOrganiser } = require('../../common/middleware/events'); const getUserRegistrations = asyncHandler(async (req, res) => { const registrations = await RegistrationController.getUserRegistrations(req.user); @@ -32,6 +30,16 @@ const updateRegistration = asyncHandler(async (req, res) => { return res.status(200).json(registration); }); +const confirmRegistration = asyncHandler(async (req, res) => { + const registration = await RegistrationController.confirmRegistration(req.user, req.event); + return res.status(200).json(registration); +}); + +const cancelRegistration = asyncHandler(async (req, res) => { + const registration = await RegistrationController.cancelRegistration(req.user, req.event); + return res.status(200).json(registration); +}); + const editRegistration = asyncHandler(async (req, res) => { const registration = await RegistrationController.editRegistration( req.params.registrationId, @@ -42,44 +50,11 @@ const editRegistration = asyncHandler(async (req, res) => { return res.status(200).json(registration); }); -const rateRegistration = asyncHandler(async (req, res) => { - const registration = await RegistrationController.rateRegistration( - req.params.registrationId, - req.event, - req.user, - req.body.rating - ); - return res.status(200).json(registration); -}); - -const acceptRegistration = asyncHandler(async (req, res) => { - const registration = await RegistrationController.acceptRegistration(req.params.registrationId, req.event); - const user = await UserProfileController.getUserProfile(registration.user); - await EmailTaskController.sendAcceptanceEmail(req.event, user); - return res.status(200).json(registration); -}); - -const rejectRegistration = asyncHandler(async (req, res) => { - const registration = await RegistrationController.rejectRegistration(req.params.registrationId, req.event); - const user = await UserProfileController.getUserProfile(registration.user); - await EmailTaskController.sendAcceptanceEmail(req.event, user); - return res.status(200).json(registration); -}); - const getRegistrationsForEvent = asyncHandler(async (req, res) => { const registrations = await RegistrationController.getRegistrationsForEvent(req.event._id.toString()); return res.status(200).json(registrations); }); -const searchRegistrationsForEvent = asyncHandler(async (req, res) => { - const registrations = await RegistrationController.searchRegistrationsForEvent( - req.event._id.toString(), - req.user.sub, - req.query - ); - return res.status(200).json(registrations); -}); - const selfAssignRegistrationsForEvent = asyncHandler(async (req, res) => { const registrations = await RegistrationController.selfAssignRegistrationsForEvent( req.event._id.toString(), @@ -112,6 +87,18 @@ const bulkEditRegistrations = asyncHandler(async (req, res) => { return res.status(200).json([]); }); +const bulkAcceptRegistrations = asyncHandler(async (req, res) => { + const eventId = req.event._id.toString(); + const accepted = await RegistrationController.acceptSoftAccepted(eventId); + return res.status(200).json(accepted); +}); + +const bulkRejectRegistrations = asyncHandler(async (req, res) => { + const eventId = req.event._id.toString(); + const rejected = await RegistrationController.rejectSoftRejected(eventId); + return res.status(200).json(rejected); +}); + router.route('/').get(hasToken, getUserRegistrations); /** Get, create or update a registration */ @@ -121,6 +108,10 @@ router .post(hasToken, canRegisterToEvent, createRegistration) .patch(hasToken, canRegisterToEvent, updateRegistration); +router.route('/:slug/confirm').patch(hasToken, hasRegisteredToEvent, confirmRegistration); + +router.route('/:slug/cancel').patch(hasToken, hasRegisteredToEvent, cancelRegistration); + /** Get all registration as organiser */ router.get( '/:slug/all', @@ -130,15 +121,6 @@ router.get( getRegistrationsForEvent ); -/** Search registrations as organiser */ -router.get( - '/:slug/search', - hasToken, - hasPermission(Auth.Permissions.MANAGE_EVENT), - isEventOrganiser, - searchRegistrationsForEvent -); - router .route('/:slug/assign') .get(hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOrganiser, selfAssignRegistrationsForEvent) @@ -148,23 +130,18 @@ router .route('/:slug/bulk') .patch(hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOrganiser, bulkEditRegistrations); -/** Get or edit single registration as an organiser */ router - .route('/:slug/:registrationId') - .get(hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOrganiser, getFullRegistration) - .patch(hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOrganiser, editRegistration); + .route('/:slug/bulk/accept') + .patch(hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOrganiser, bulkAcceptRegistrations); -/** Rate a single registration */ router - .route('/:slug/:registrationId/rate') - .patch(hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOrganiser, rateRegistration); - -router - .route('/:slug/:registrationId/accept') - .patch(hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOrganiser, acceptRegistration); + .route('/:slug/bulk/reject') + .patch(hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOrganiser, bulkRejectRegistrations); +/** Get or edit single registration as an organiser */ router - .route('/:slug/:registrationId/reject') - .patch(hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOrganiser, rejectRegistration); + .route('/:slug/:registrationId') + .get(hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOrganiser, getFullRegistration) + .patch(hasToken, hasPermission(Auth.Permissions.MANAGE_EVENT), isEventOrganiser, editRegistration); module.exports = router; diff --git a/backend/modules/routes.js b/backend/modules/routes.js index 0c948788d..cdd954964 100644 --- a/backend/modules/routes.js +++ b/backend/modules/routes.js @@ -5,6 +5,8 @@ const userProfileRouter = require('./user-profile/routes'); const registrationRouter = require('./registration/routes'); const newsletterRouter = require('./newsletter/routes'); const teamRouter = require('./team/routes'); +const emailRouter = require('./email-task/routes'); +const devToolsRouter = require('./devtools/routes'); module.exports = function(app) { app.get('/api', (req, res) => { @@ -15,10 +17,16 @@ module.exports = function(app) { app.use('/api/auth', authRouter); app.use('/api/upload', uploadRouter); app.use('/api/newsletter', newsletterRouter); + app.use('/api/email', emailRouter); /** Model related routes */ app.use('/api/events', eventRouter); app.use('/api/teams', teamRouter); app.use('/api/user-profiles', userProfileRouter); app.use('/api/registrations', registrationRouter); + + /** Admin tools (development only) */ + if (global.gConfig.DEVTOOLS_ENABLED) { + app.use('/api/devtools', devToolsRouter); + } }; diff --git a/backend/package-lock.json b/backend/package-lock.json index 0c6365c6c..df76a536d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -25,9 +25,7 @@ } }, "@hackjunction/shared": { - "version": "1.1.44", - "resolved": "https://registry.npmjs.org/@hackjunction/shared/-/shared-1.1.44.tgz", - "integrity": "sha512-2EG2wNFFLGmzoMp6kF0xjmIoCop9SiRzKGxez/INVLf+4mcu8vcInIyfd8wSRD+kMPO6BSvsb2M+jJa6bJ9qig==" + "version": "file:../shared" }, "@sendgrid/client": { "version": "6.4.0", @@ -1839,24 +1837,29 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "dev": true + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "dev": true, "optional": true, "requires": { @@ -1866,13 +1869,17 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "dev": true + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", - "bundled": true, + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1880,34 +1887,43 @@ }, "chownr": { "version": "1.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "dev": true, "optional": true }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "dev": true + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "dev": true + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "dev": true + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true, "optional": true }, "debug": { "version": "4.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "optional": true, "requires": { @@ -1916,25 +1932,29 @@ }, "deep-extend": { "version": "0.6.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "optional": true }, "delegates": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true, "optional": true }, "fs-minipass": { "version": "1.2.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "dev": true, "optional": true, "requires": { @@ -1943,13 +1963,15 @@ }, "fs.realpath": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true, "optional": true }, "gauge": { "version": "2.7.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "optional": true, "requires": { @@ -1965,7 +1987,8 @@ }, "glob": { "version": "7.1.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "optional": true, "requires": { @@ -1979,13 +2002,15 @@ }, "has-unicode": { "version": "2.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.24", - "bundled": true, + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "optional": true, "requires": { @@ -1994,7 +2019,8 @@ }, "ignore-walk": { "version": "3.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "dev": true, "optional": true, "requires": { @@ -2003,7 +2029,8 @@ }, "inflight": { "version": "1.0.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "optional": true, "requires": { @@ -2013,46 +2040,58 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "dev": true + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } }, "isarray": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "dev": true + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2060,7 +2099,8 @@ }, "minizlib": { "version": "1.2.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "dev": true, "optional": true, "requires": { @@ -2069,21 +2109,25 @@ }, "mkdirp": { "version": "0.5.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } }, "ms": { "version": "2.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true, "optional": true }, "needle": { "version": "2.3.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.0.tgz", + "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "dev": true, "optional": true, "requires": { @@ -2094,7 +2138,8 @@ }, "node-pre-gyp": { "version": "0.12.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz", + "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "dev": true, "optional": true, "requires": { @@ -2112,7 +2157,8 @@ }, "nopt": { "version": "4.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "dev": true, "optional": true, "requires": { @@ -2122,13 +2168,15 @@ }, "npm-bundled": { "version": "1.0.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "dev": true, "optional": true }, "npm-packlist": { "version": "1.4.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", + "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "dev": true, "optional": true, "requires": { @@ -2138,7 +2186,8 @@ }, "npmlog": { "version": "4.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, "optional": true, "requires": { @@ -2150,38 +2199,46 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "dev": true + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, "optional": true }, "once": { "version": "1.4.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, + "optional": true, "requires": { "wrappy": "1" } }, "os-homedir": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true, "optional": true }, "osenv": { "version": "0.1.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "optional": true, "requires": { @@ -2191,19 +2248,22 @@ }, "path-is-absolute": { "version": "1.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true, "optional": true }, "rc": { "version": "1.2.8", - "bundled": true, + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "optional": true, "requires": { @@ -2215,7 +2275,8 @@ "dependencies": { "minimist": { "version": "1.2.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true } @@ -2223,7 +2284,8 @@ }, "readable-stream": { "version": "2.3.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "optional": true, "requires": { @@ -2238,7 +2300,8 @@ }, "rimraf": { "version": "2.6.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "optional": true, "requires": { @@ -2247,43 +2310,52 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "dev": true + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "optional": true }, "sax": { "version": "1.2.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, "optional": true }, "semver": { "version": "5.7.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true, "optional": true }, "string-width": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2292,7 +2364,8 @@ }, "string_decoder": { "version": "1.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "optional": true, "requires": { @@ -2301,21 +2374,25 @@ }, "strip-ansi": { "version": "3.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } }, "strip-json-comments": { "version": "2.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, "optional": true }, "tar": { "version": "4.4.8", - "bundled": true, + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "dev": true, "optional": true, "requires": { @@ -2330,13 +2407,15 @@ }, "util-deprecate": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, "optional": true }, "wide-align": { "version": "1.1.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, "optional": true, "requires": { @@ -2345,13 +2424,17 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "dev": true + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true, - "dev": true + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "dev": true, + "optional": true } } }, diff --git a/backend/package.json b/backend/package.json index 165884ed8..553aea050 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,7 +11,7 @@ "author": "", "license": "ISC", "dependencies": { - "@hackjunction/shared": "^1.1.44", + "@hackjunction/shared": "file:../shared", "@sendgrid/client": "^6.4.0", "@sendgrid/mail": "^6.4.0", "auth0": "^2.17.0", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ef4da176b..93ea280a1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1020,9 +1020,7 @@ "integrity": "sha512-6It2EVfGskxZCQhuykrfnALg7oVeiI6KclWSmGDqB0AiInVrTGB9Jp9i4/Ad21u9Jde/voVQz6eFX/eSg/UsPA==" }, "@hackjunction/shared": { - "version": "1.1.44", - "resolved": "https://registry.npmjs.org/@hackjunction/shared/-/shared-1.1.44.tgz", - "integrity": "sha512-2EG2wNFFLGmzoMp6kF0xjmIoCop9SiRzKGxez/INVLf+4mcu8vcInIyfd8wSRD+kMPO6BSvsb2M+jJa6bJ9qig==" + "version": "file:../shared" }, "@hapi/address": { "version": "2.0.0", @@ -1410,6 +1408,95 @@ "style-value-types": "^3.1.6" } }, + "@react-pdf/fontkit": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@react-pdf/fontkit/-/fontkit-1.13.0.tgz", + "integrity": "sha512-g4rxtkSkEbIwDoqZc6ZM2yHq/imqmgGXQuMDnrkM2yI9TpTGhQfJGnz9IrCadrRgOrEx9orxRuTALOxzAce7Kg==", + "requires": { + "@react-pdf/unicode-properties": "^2.2.0", + "brotli": "^1.2.0", + "clone": "^1.0.1", + "deep-equal": "^1.0.0", + "dfa": "^1.0.0", + "restructure": "^0.5.3", + "tiny-inflate": "^1.0.2", + "unicode-trie": "^0.3.0" + } + }, + "@react-pdf/pdfkit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-1.2.0.tgz", + "integrity": "sha1-9f4ohXhWdTbljeGZij4T7i5KyTA=", + "requires": { + "@react-pdf/fontkit": "^1.11.0", + "@react-pdf/png-js": "^1.0.0", + "lz-string": "^1.4.4" + } + }, + "@react-pdf/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-pdf/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha1-APy5adykzoKgp2c0E63gOeR7Nh4=" + }, + "@react-pdf/renderer": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@react-pdf/renderer/-/renderer-1.6.4.tgz", + "integrity": "sha512-/R/NPc3plpZnw8oE6ngoRuVVYV1mlXl3QQd64gK9mUG09T/hMhTJFtGnoJAEFNfAoxoO9P2P/6jxpYCpFUbuxA==", + "requires": { + "@babel/runtime": "^7.3.1", + "@react-pdf/fontkit": "^1.13.0", + "@react-pdf/pdfkit": "^1.2.0", + "@react-pdf/png-js": "^1.0.0", + "@react-pdf/textkit": "^0.3.7", + "blob-stream": "^0.1.3", + "cross-fetch": "^3.0.2", + "emoji-regex": "^8.0.0", + "is-url": "^1.2.4", + "media-engine": "^1.0.3", + "page-wrapping": "^1.1.0", + "ramda": "^0.26.1", + "react": "^16.8.6", + "react-reconciler": "^0.20.4", + "scheduler": "^0.14.0", + "yoga-layout-prebuilt": "^1.9.3" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "scheduler": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.14.0.tgz", + "integrity": "sha512-9CgbS06Kki2f4R9FjLSITjZo5BZxPsryiRNyL3LpvrM9WxcVmhlqAOc9E+KQbeI2nqej4JIIbOsfdL51cNb4Iw==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + } + } + }, + "@react-pdf/textkit": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-0.3.7.tgz", + "integrity": "sha512-JyK06VofTG4/6Mxv9ugqGyKS/nnbg7MXXHmzeYLP+wwxZsgiatjybVBSXI+DW+++UeyUKLrCYQh2RaxliRm+NQ==", + "requires": { + "@babel/runtime": "^7.4.3", + "@react-pdf/unicode-properties": "^2.2.0", + "babel-runtime": "^6.26.0", + "hyphen": "^1.1.1", + "ramda": "^0.26.1" + } + }, + "@react-pdf/unicode-properties": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-pdf/unicode-properties/-/unicode-properties-2.2.0.tgz", + "integrity": "sha1-8QnqrCRM6xCAEdQDjO5Mx4fLQPM=", + "requires": { + "unicode-trie": "^0.3.0" + } + }, "@svgr/babel-plugin-add-jsx-attribute": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz", @@ -2218,6 +2305,58 @@ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, + "ast-transform": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/ast-transform/-/ast-transform-0.0.0.tgz", + "integrity": "sha1-dJRAWIh9goPhidlUYAlHvJj+AGI=", + "requires": { + "escodegen": "~1.2.0", + "esprima": "~1.0.4", + "through": "~2.3.4" + }, + "dependencies": { + "escodegen": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.2.0.tgz", + "integrity": "sha1-Cd55Z3kcyVi3+Jot220jRRrzJ+E=", + "requires": { + "esprima": "~1.0.4", + "estraverse": "~1.5.0", + "esutils": "~1.0.0", + "source-map": "~0.1.30" + } + }, + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=" + }, + "estraverse": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", + "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=" + }, + "esutils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", + "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=" + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "ast-types": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.7.8.tgz", + "integrity": "sha1-kC0uDWDQcb3NRtwRXhgJ7RHBOKk=" + }, "ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -2832,6 +2971,19 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==" }, + "blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=" + }, + "blob-stream": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/blob-stream/-/blob-stream-0.1.3.tgz", + "integrity": "sha1-mNZor2mW4PMu9mbQbiFczH13aGw=", + "requires": { + "blob": "0.0.4" + } + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -2946,6 +3098,14 @@ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, + "brotli": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.2.tgz", + "integrity": "sha1-UlqcrU/LqWR119OI9q7LE+7VL0Y=", + "requires": { + "base64-js": "^1.1.2" + } + }, "browser-process-hrtime": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", @@ -3000,6 +3160,16 @@ "safe-buffer": "^5.1.2" } }, + "browserify-optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-optional/-/browserify-optional-1.0.1.tgz", + "integrity": "sha1-HhNyLP3g2F8SFnbCpyztUzoBiGk=", + "requires": { + "ast-transform": "0.0.0", + "ast-types": "^0.7.0", + "browser-resolve": "^1.8.1" + } + }, "browserify-rsa": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", @@ -3287,22 +3457,26 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "optional": true }, "ansi-regex": { "version": "2.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "optional": true }, "aproba": { "version": "1.2.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "optional": true, "requires": { "delegates": "^1.0.0", @@ -3311,12 +3485,14 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "optional": true }, "brace-expansion": { "version": "1.1.11", - "bundled": true, + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "optional": true, "requires": { "balanced-match": "^1.0.0", @@ -3325,32 +3501,38 @@ }, "chownr": { "version": "1.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "optional": true }, "code-point-at": { "version": "1.1.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "optional": true }, "core-util-is": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "optional": true }, "debug": { "version": "4.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "optional": true, "requires": { "ms": "^2.1.1" @@ -3358,22 +3540,26 @@ }, "deep-extend": { "version": "0.6.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "optional": true }, "delegates": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "optional": true }, "detect-libc": { "version": "1.0.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "optional": true }, "fs-minipass": { "version": "1.2.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "optional": true, "requires": { "minipass": "^2.2.1" @@ -3381,12 +3567,14 @@ }, "fs.realpath": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "optional": true }, "gauge": { "version": "2.7.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "optional": true, "requires": { "aproba": "^1.0.3", @@ -3401,7 +3589,8 @@ }, "glob": { "version": "7.1.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "optional": true, "requires": { "fs.realpath": "^1.0.0", @@ -3414,12 +3603,14 @@ }, "has-unicode": { "version": "2.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "optional": true }, "iconv-lite": { "version": "0.4.24", - "bundled": true, + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "optional": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" @@ -3427,7 +3618,8 @@ }, "ignore-walk": { "version": "3.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "optional": true, "requires": { "minimatch": "^3.0.4" @@ -3435,7 +3627,8 @@ }, "inflight": { "version": "1.0.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "optional": true, "requires": { "once": "^1.3.0", @@ -3444,17 +3637,20 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "optional": true }, "ini": { "version": "1.3.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "optional": true, "requires": { "number-is-nan": "^1.0.0" @@ -3462,12 +3658,14 @@ }, "isarray": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "optional": true }, "minimatch": { "version": "3.0.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "optional": true, "requires": { "brace-expansion": "^1.1.7" @@ -3475,12 +3673,14 @@ }, "minimist": { "version": "0.0.8", - "bundled": true, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "optional": true }, "minipass": { "version": "2.3.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "optional": true, "requires": { "safe-buffer": "^5.1.2", @@ -3489,7 +3689,8 @@ }, "minizlib": { "version": "1.2.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "optional": true, "requires": { "minipass": "^2.2.1" @@ -3497,7 +3698,8 @@ }, "mkdirp": { "version": "0.5.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "optional": true, "requires": { "minimist": "0.0.8" @@ -3505,12 +3707,14 @@ }, "ms": { "version": "2.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "optional": true }, "needle": { "version": "2.3.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.0.tgz", + "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "optional": true, "requires": { "debug": "^4.1.0", @@ -3520,7 +3724,8 @@ }, "node-pre-gyp": { "version": "0.12.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz", + "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "optional": true, "requires": { "detect-libc": "^1.0.2", @@ -3537,7 +3742,8 @@ }, "nopt": { "version": "4.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "optional": true, "requires": { "abbrev": "1", @@ -3546,12 +3752,14 @@ }, "npm-bundled": { "version": "1.0.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "optional": true }, "npm-packlist": { "version": "1.4.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", + "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "optional": true, "requires": { "ignore-walk": "^3.0.1", @@ -3560,7 +3768,8 @@ }, "npmlog": { "version": "4.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "optional": true, "requires": { "are-we-there-yet": "~1.1.2", @@ -3571,17 +3780,20 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "optional": true }, "object-assign": { "version": "4.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "optional": true }, "once": { "version": "1.4.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "optional": true, "requires": { "wrappy": "1" @@ -3589,17 +3801,20 @@ }, "os-homedir": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "optional": true }, "os-tmpdir": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "optional": true }, "osenv": { "version": "0.1.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "optional": true, "requires": { "os-homedir": "^1.0.0", @@ -3608,17 +3823,20 @@ }, "path-is-absolute": { "version": "1.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "optional": true }, "process-nextick-args": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "optional": true }, "rc": { "version": "1.2.8", - "bundled": true, + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "optional": true, "requires": { "deep-extend": "^0.6.0", @@ -3629,14 +3847,16 @@ "dependencies": { "minimist": { "version": "1.2.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "optional": true } } }, "readable-stream": { "version": "2.3.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "optional": true, "requires": { "core-util-is": "~1.0.0", @@ -3650,7 +3870,8 @@ }, "rimraf": { "version": "2.6.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "optional": true, "requires": { "glob": "^7.1.3" @@ -3658,37 +3879,44 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "optional": true }, "safer-buffer": { "version": "2.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "optional": true }, "sax": { "version": "1.2.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "optional": true }, "semver": { "version": "5.7.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "optional": true }, "set-blocking": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "optional": true }, "signal-exit": { "version": "3.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "optional": true }, "string-width": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "optional": true, "requires": { "code-point-at": "^1.0.0", @@ -3698,7 +3926,8 @@ }, "string_decoder": { "version": "1.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "optional": true, "requires": { "safe-buffer": "~5.1.0" @@ -3706,7 +3935,8 @@ }, "strip-ansi": { "version": "3.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "optional": true, "requires": { "ansi-regex": "^2.0.0" @@ -3714,12 +3944,14 @@ }, "strip-json-comments": { "version": "2.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "optional": true }, "tar": { "version": "4.4.8", - "bundled": true, + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "optional": true, "requires": { "chownr": "^1.1.1", @@ -3733,12 +3965,14 @@ }, "util-deprecate": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "optional": true }, "wide-align": { "version": "1.1.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "optional": true, "requires": { "string-width": "^1.0.2 || 2" @@ -3746,12 +3980,14 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "optional": true } } @@ -3854,6 +4090,11 @@ "wrap-ansi": "^2.0.0" } }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + }, "clone-deep": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", @@ -4286,6 +4527,22 @@ "gud": "^1.0.0" } }, + "cross-fetch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.4.tgz", + "integrity": "sha512-MSHgpjQqgbT/94D4CyADeNoYh52zMkCX4pcJvPP5WqPsLFMKjr2TCMg381ox5qI0ii2dPwaLx/00477knXqXVw==", + "requires": { + "node-fetch": "2.6.0", + "whatwg-fetch": "3.0.0" + }, + "dependencies": { + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + } + } + }, "cross-spawn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", @@ -4851,6 +5108,11 @@ } } }, + "dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" + }, "diff-sequences": { "version": "24.3.0", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.3.0.tgz", @@ -7050,6 +7312,11 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, + "hyphen": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.1.1.tgz", + "integrity": "sha512-S6KSoGZWPutjTB7koZ9Ci9xzETBa7GVlNe42r0hF+rhoE/6lLHwEvi2FQSEhyWIjVo46tV94vn96hcsuum1THg==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -7593,6 +7860,11 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" + }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -8195,22 +8467,26 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "optional": true }, "ansi-regex": { "version": "2.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "optional": true }, "aproba": { "version": "1.2.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "optional": true, "requires": { "delegates": "^1.0.0", @@ -8219,12 +8495,14 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "optional": true }, "brace-expansion": { "version": "1.1.11", - "bundled": true, + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "optional": true, "requires": { "balanced-match": "^1.0.0", @@ -8233,32 +8511,38 @@ }, "chownr": { "version": "1.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "optional": true }, "code-point-at": { "version": "1.1.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "optional": true }, "core-util-is": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "optional": true }, "debug": { "version": "4.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "optional": true, "requires": { "ms": "^2.1.1" @@ -8266,22 +8550,26 @@ }, "deep-extend": { "version": "0.6.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "optional": true }, "delegates": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "optional": true }, "detect-libc": { "version": "1.0.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "optional": true }, "fs-minipass": { "version": "1.2.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "optional": true, "requires": { "minipass": "^2.2.1" @@ -8289,12 +8577,14 @@ }, "fs.realpath": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "optional": true }, "gauge": { "version": "2.7.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "optional": true, "requires": { "aproba": "^1.0.3", @@ -8309,7 +8599,8 @@ }, "glob": { "version": "7.1.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "optional": true, "requires": { "fs.realpath": "^1.0.0", @@ -8322,12 +8613,14 @@ }, "has-unicode": { "version": "2.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "optional": true }, "iconv-lite": { "version": "0.4.24", - "bundled": true, + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "optional": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" @@ -8335,7 +8628,8 @@ }, "ignore-walk": { "version": "3.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "optional": true, "requires": { "minimatch": "^3.0.4" @@ -8343,7 +8637,8 @@ }, "inflight": { "version": "1.0.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "optional": true, "requires": { "once": "^1.3.0", @@ -8352,17 +8647,20 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "optional": true }, "ini": { "version": "1.3.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "optional": true, "requires": { "number-is-nan": "^1.0.0" @@ -8370,12 +8668,14 @@ }, "isarray": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "optional": true }, "minimatch": { "version": "3.0.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "optional": true, "requires": { "brace-expansion": "^1.1.7" @@ -8383,12 +8683,14 @@ }, "minimist": { "version": "0.0.8", - "bundled": true, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "optional": true }, "minipass": { "version": "2.3.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "optional": true, "requires": { "safe-buffer": "^5.1.2", @@ -8397,7 +8699,8 @@ }, "minizlib": { "version": "1.2.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "optional": true, "requires": { "minipass": "^2.2.1" @@ -8405,7 +8708,8 @@ }, "mkdirp": { "version": "0.5.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "optional": true, "requires": { "minimist": "0.0.8" @@ -8413,12 +8717,14 @@ }, "ms": { "version": "2.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "optional": true }, "needle": { "version": "2.3.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.0.tgz", + "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "optional": true, "requires": { "debug": "^4.1.0", @@ -8428,7 +8734,8 @@ }, "node-pre-gyp": { "version": "0.12.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz", + "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "optional": true, "requires": { "detect-libc": "^1.0.2", @@ -8445,7 +8752,8 @@ }, "nopt": { "version": "4.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "optional": true, "requires": { "abbrev": "1", @@ -8454,12 +8762,14 @@ }, "npm-bundled": { "version": "1.0.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "optional": true }, "npm-packlist": { "version": "1.4.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", + "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "optional": true, "requires": { "ignore-walk": "^3.0.1", @@ -8468,7 +8778,8 @@ }, "npmlog": { "version": "4.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "optional": true, "requires": { "are-we-there-yet": "~1.1.2", @@ -8479,17 +8790,20 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "optional": true }, "object-assign": { "version": "4.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "optional": true }, "once": { "version": "1.4.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "optional": true, "requires": { "wrappy": "1" @@ -8497,17 +8811,20 @@ }, "os-homedir": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "optional": true }, "os-tmpdir": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "optional": true }, "osenv": { "version": "0.1.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "optional": true, "requires": { "os-homedir": "^1.0.0", @@ -8516,17 +8833,20 @@ }, "path-is-absolute": { "version": "1.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "optional": true }, "process-nextick-args": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "optional": true }, "rc": { "version": "1.2.8", - "bundled": true, + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "optional": true, "requires": { "deep-extend": "^0.6.0", @@ -8537,14 +8857,16 @@ "dependencies": { "minimist": { "version": "1.2.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "optional": true } } }, "readable-stream": { "version": "2.3.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "optional": true, "requires": { "core-util-is": "~1.0.0", @@ -8558,7 +8880,8 @@ }, "rimraf": { "version": "2.6.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "optional": true, "requires": { "glob": "^7.1.3" @@ -8566,37 +8889,44 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "optional": true }, "safer-buffer": { "version": "2.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "optional": true }, "sax": { "version": "1.2.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "optional": true }, "semver": { "version": "5.7.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "optional": true }, "set-blocking": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "optional": true }, "signal-exit": { "version": "3.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "optional": true }, "string-width": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "optional": true, "requires": { "code-point-at": "^1.0.0", @@ -8606,7 +8936,8 @@ }, "string_decoder": { "version": "1.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "optional": true, "requires": { "safe-buffer": "~5.1.0" @@ -8614,7 +8945,8 @@ }, "strip-ansi": { "version": "3.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "optional": true, "requires": { "ansi-regex": "^2.0.0" @@ -8622,12 +8954,14 @@ }, "strip-json-comments": { "version": "2.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "optional": true }, "tar": { "version": "4.4.8", - "bundled": true, + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "optional": true, "requires": { "chownr": "^1.1.1", @@ -8641,12 +8975,14 @@ }, "util-deprecate": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "optional": true }, "wide-align": { "version": "1.1.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "optional": true, "requires": { "string-width": "^1.0.2 || 2" @@ -8654,12 +8990,14 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "optional": true } } @@ -9866,6 +10204,11 @@ "yallist": "^2.1.2" } }, + "lz-string": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=" + }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -9954,6 +10297,11 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" }, + "media-engine": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz", + "integrity": "sha1-vjGI9s0kPqKkCASjXeWlsDL1ja0=" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -10597,6 +10945,11 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, + "object-path": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.4.tgz", + "integrity": "sha1-NwrnUvvzfePqcKhhwju6iRVpGUk=" + }, "object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", @@ -10842,6 +11195,11 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, + "page-wrapping": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/page-wrapping/-/page-wrapping-1.1.0.tgz", + "integrity": "sha512-DAnqZJ3FHKLXVbdQfvGoHyZRFZL+N1IIZlo2RImFqrZ3scoFS8lOHZoLQxnYbsnCMQkwoEyESd0ZNs5RbEsArA==" + }, "pako": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", @@ -13201,6 +13559,17 @@ } } }, + "react-reconciler": { + "version": "0.20.4", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.20.4.tgz", + "integrity": "sha512-kxERc4H32zV2lXMg/iMiwQHOtyqf15qojvkcZ5Ja2CPkjVohHw9k70pdDBwrnQhLVetUJBSYyqU3yqrlVTOajA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.13.6" + } + }, "react-redux": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-6.0.1.tgz", @@ -13800,6 +14169,14 @@ "signal-exit": "^3.0.2" } }, + "restructure": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-0.5.4.tgz", + "integrity": "sha1-9U591WNZD7NP1r9Vh2EJrsyyjeg=", + "requires": { + "browserify-optional": "^1.0.0" + } + }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -15198,6 +15575,11 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tiny-inflate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.2.tgz", + "integrity": "sha1-k9nez/yIBb1X6uQxDwt0Xptvs6c=" + }, "tiny-invariant": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz", @@ -15460,6 +15842,22 @@ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==" }, + "unicode-trie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-0.3.1.tgz", + "integrity": "sha1-1nHd3YkQGgi6w3tqUWEBBgIFIIU=", + "requires": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + }, + "dependencies": { + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" + } + } + }, "unified": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz", @@ -16513,6 +16911,11 @@ "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" } } + }, + "yoga-layout-prebuilt": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/yoga-layout-prebuilt/-/yoga-layout-prebuilt-1.9.3.tgz", + "integrity": "sha512-9SNQpwuEh2NucU83i2KMZnONVudZ86YNcFk9tq74YaqrQfgJWO3yB9uzH1tAg8iqh5c9F5j0wuyJ2z72wcum2w==" } } } diff --git a/frontend/package.json b/frontend/package.json index 686f774fb..689cf0911 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,7 +3,8 @@ "version": "0.1.0", "private": true, "dependencies": { - "@hackjunction/shared": "^1.1.44", + "@hackjunction/shared": "file:../shared", + "@react-pdf/renderer": "^1.6.4", "antd": "^3.18.2", "auth0-js": "^9.10.0", "axios": "^0.18.0", @@ -22,6 +23,7 @@ "moment": "^2.24.0", "moment-timezone": "^0.5.25", "node-sass": "^4.11.0", + "object-path": "^0.11.4", "react": "^16.8.1", "react-animate-height": "^2.0.15", "react-app-rewired": "^2.1.3", diff --git a/frontend/src/assets/images/visa_signature.jpg b/frontend/src/assets/images/visa_signature.jpg new file mode 100644 index 000000000..1209ea142 Binary files /dev/null and b/frontend/src/assets/images/visa_signature.jpg differ diff --git a/frontend/src/assets/logos/wordmark_black_small.png b/frontend/src/assets/logos/wordmark_black_small.png new file mode 100644 index 000000000..e8e8415fe Binary files /dev/null and b/frontend/src/assets/logos/wordmark_black_small.png differ diff --git a/frontend/src/components/generic/NotificationBlock/NotificationBlock.module.scss b/frontend/src/components/generic/NotificationBlock/NotificationBlock.module.scss index 2d1046e8c..236fe42cd 100644 --- a/frontend/src/components/generic/NotificationBlock/NotificationBlock.module.scss +++ b/frontend/src/components/generic/NotificationBlock/NotificationBlock.module.scss @@ -1,48 +1,63 @@ @import '~styles/variables'; .wrapper { - padding: 1rem; display: flex; flex-direction: column; - align-items: flex-start; + align-items: stretch; border-radius: 10px; box-shadow: 4px 6px 20px #f3f3f3; + overflow: hidden; - .title { - font-size: 18px; - text-transform: uppercase; - color: $black; + .iconWrapper { display: flex; - flex-direction: row; + flex-direction: column; align-items: center; - } - - .body { - font-size: 18px; - } + justify-content: center; + background: rgba($black, 0.5); + padding: 1rem; - .titleExtra { - padding-left: 5px; - } + &Warning { + background: rgba($junction-orange, 0.7); + } - .typeIcon { - margin-right: 10px; - font-size: 24px; + &Error { + background: rgba($color-error, 0.5); + } - &Info { - color: $green; + &Success { + background: $green; } - &Warning { - color: $junction-orange; + .icon { + color: white; + font-size: 32px; } + } - &Error { - color: $color-error; + .contentWrapper { + padding: 1rem; + display: flex; + flex-direction: column; + align-items: center; + background: $lightgrey; + + .title { + font-size: 18px; + text-transform: uppercase; + color: $black; + display: flex; + flex-direction: row; + align-items: center; + + &Extra { + padding-left: 5px; + font-weight: 900; + } } - &Success { - color: green; + .body { + font-size: 18px; + text-align: center; } } } diff --git a/frontend/src/components/generic/NotificationBlock/index.js b/frontend/src/components/generic/NotificationBlock/index.js index 699fd2538..3e6ce62e6 100644 --- a/frontend/src/components/generic/NotificationBlock/index.js +++ b/frontend/src/components/generic/NotificationBlock/index.js @@ -11,30 +11,39 @@ const NotificationBlock = ({ title, titleExtra, body, bottom, type }) => { if (!type) return null; switch (type) { case 'success': - return ; + return ; case 'error': - return ; + return ; case 'warning': - return ( - - ); + return ; case 'info': - return ; + return ; default: return null; } }; + const iconWrapperClass = classNames(styles.iconWrapper, { + [styles.iconWrapperSuccess]: type === 'success', + [styles.iconWrapperInfo]: type === 'info', + [styles.iconWrapperWarning]: type === 'warning', + [styles.iconWrapperError]: type === 'error' + }); + return (
- - {renderTypeIcon()} - {title} - {titleExtra && {titleExtra}} - - -

{body}

-
{bottom}
+
+
{renderTypeIcon()}
+
+
+ + {title} + {titleExtra && {titleExtra}} + + +

{body}

+
{bottom}
+
); }; diff --git a/frontend/src/components/modals/BulkEditRegistrationDrawer/index.js b/frontend/src/components/modals/BulkEditRegistrationDrawer/index.js index a88029476..acca46b96 100644 --- a/frontend/src/components/modals/BulkEditRegistrationDrawer/index.js +++ b/frontend/src/components/modals/BulkEditRegistrationDrawer/index.js @@ -273,12 +273,7 @@ const BulkEditRegistrationDrawer = ({ /> - + ); }; diff --git a/frontend/src/components/modals/BulkEmailDrawer/BulkEmailDrawer.module.scss b/frontend/src/components/modals/BulkEmailDrawer/BulkEmailDrawer.module.scss new file mode 100644 index 000000000..437085590 --- /dev/null +++ b/frontend/src/components/modals/BulkEmailDrawer/BulkEmailDrawer.module.scss @@ -0,0 +1,4 @@ +.label { + display: block; + font-weight: bold; +} diff --git a/frontend/src/components/modals/BulkEmailDrawer/index.js b/frontend/src/components/modals/BulkEmailDrawer/index.js new file mode 100644 index 000000000..99ea0e174 --- /dev/null +++ b/frontend/src/components/modals/BulkEmailDrawer/index.js @@ -0,0 +1,219 @@ +import React, { useState, useCallback } from 'react'; +import styles from './BulkEmailDrawer.module.scss'; + +import { connect } from 'react-redux'; +import { Drawer, Button as AntButton, Divider as AntDivider, Input, Modal, notification, Popconfirm } from 'antd'; + +import Divider from 'components/generic/Divider'; +import * as AuthSelectors from 'redux/auth/selectors'; +import * as OrganiserSelectors from 'redux/organiser/selectors'; +import EmailService from 'services/email'; + +const BulkEmailDrawer = ({ registrationIds, buttonProps, user, idToken, event }) => { + const [visible, setVisible] = useState(false); + const [testEmail, setTestEmail] = useState(user.email); + const [testModalVisible, setTestModalVisible] = useState(false); + const [testModalLoading, setTestModalLoading] = useState(false); + const [loading, setLoading] = useState(false); + + const [subject, setSubject] = useState(); + const [subtitle, setSubtitle] = useState(); + const [headerImage, setHeaderImage] = useState(); + const [body, setBody] = useState(); + const [ctaText, setCtaText] = useState(); + const [ctaLink, setCtaLink] = useState(); + const [messageId, setMessageId] = useState(); + + const params = { + subject: subject, + subtitle: subtitle, + header_image: headerImage, + body: body, + cta_text: ctaText, + cta_link: ctaLink + }; + + const handleClose = useCallback(() => { + setVisible(false); + }, []); + + const handleCloseModal = useCallback(() => { + setTestModalVisible(false); + }, []); + + const handleTestEmail = useCallback(() => { + setTestModalLoading(true); + EmailService.sendPreviewEmail(idToken, event.slug, testEmail, params) + .then(() => { + notification.success({ + message: 'Success', + description: 'Check your inbox!' + }); + }) + .catch(err => { + console.log(err); + notification.error({ + message: 'Something went wrong...', + description: 'Did you enter a valid email address?' + }); + }) + .finally(() => { + setTestModalLoading(false); + setTestModalVisible(false); + }); + return null; + }, [idToken, event.slug, testEmail, params]); + + const handleConfirm = useCallback(() => { + setLoading(true); + EmailService.sendBulkEmail(idToken, event.slug, registrationIds, params, messageId) + .then(() => { + notification.success({ + message: 'Success' + }); + }) + .catch(err => { + notification.error({ + message: 'Something went wrong...', + description: 'Are you connected to the internet?' + }); + }) + .finally(() => { + setLoading(false); + }); + }, [idToken, event.slug, params, registrationIds, messageId]); + + return ( + + + setTestEmail(e.target.value)} + /> + + + Bulk email +

+ Here you can send an email to all selected participants. Type in your own email address below to + test the email before sending it out to everyone! +

+ + setHeaderImage(e.target.value)} + type="text" + size="large" + placeholder="https://" + > + Url to a header image for your email + + + setSubject(e.target.value)} + type="text" + size="large" + placeholder="Subject" + > + The subject line of your message + + + setSubtitle(e.target.value)} + type="text" + size="large" + placeholder="Your subtitle" + > + A subtitle to be shown under the subject (optional) + + + setBody(e.target.value)} + autosize={{ + minRows: 10, + maxRows: 20 + }} + > + The content of your email + + + setMessageId(e.target.value)} + size="large" + placeholder="something-you-will-remember" + /> + + If you want, you can enter a unique message id here. Messages with the same id will only be sent + once to a given participant - this is useful if you want to avoid sending out the same message to a + participant who has already received it earlier. + + + + setCtaText(e.target.value)} + size="large" + placeholder="Click this button" + /> + + If your want a Call to Action button in your email, enter the text for the button here. + + + + setCtaLink(e.target.value)} + size="large" + placeholder="https://..." + /> + Enter the link where your Call to Action button should lead + + + Test email + + + + + Send to {registrationIds.length} recipients + + +
+ +
+ ); +}; + +const mapState = state => ({ + user: AuthSelectors.getCurrentUser(state), + event: OrganiserSelectors.event(state), + idToken: AuthSelectors.getIdToken(state) +}); + +export default connect(mapState)(BulkEmailDrawer); diff --git a/frontend/src/components/modals/VisaInvitationDrawer/VisaInvitationDrawer.module.scss b/frontend/src/components/modals/VisaInvitationDrawer/VisaInvitationDrawer.module.scss new file mode 100644 index 000000000..1338132c3 --- /dev/null +++ b/frontend/src/components/modals/VisaInvitationDrawer/VisaInvitationDrawer.module.scss @@ -0,0 +1,3 @@ +.label { + font-size: 16px; +} diff --git a/frontend/src/components/modals/VisaInvitationDrawer/VisaInvitationPDF.js b/frontend/src/components/modals/VisaInvitationDrawer/VisaInvitationPDF.js new file mode 100644 index 000000000..e1733f77e --- /dev/null +++ b/frontend/src/components/modals/VisaInvitationDrawer/VisaInvitationPDF.js @@ -0,0 +1,104 @@ +import React from 'react'; +import { Page, Text, Image, Document, StyleSheet, Font, View } from '@react-pdf/renderer'; + +Font.register({ + family: 'Montserrat', + src: 'https://fonts.gstatic.com/s/montserrat/v10/zhcz-_WihjSQC0oHJ9TCYC3USBnSvpkopQaUR-2r7iU.ttf', + fontWeight: 'bold' +}); +Font.register({ + family: 'Lato', + src: 'https://fonts.gstatic.com/s/lato/v13/v0SdcGFAl2aezM9Vq_aFTQ.ttf', + fontWeight: 'light' +}); + +const styles = StyleSheet.create({ + body: { + paddingTop: 35, + paddingBottom: 65, + paddingHorizontal: 35 + }, + topLeftLogo: { + width: 200 + }, + topRightTitle: { + fontSize: 22, + marginBottom: 10, + fontFamily: 'Montserrat', + textAlign: 'right' + }, + topRightItem: { + fontSize: 13, + fontFamily: 'Lato', + textAlign: 'right' + }, + paragraph: { + fontSize: 13, + fontFamily: 'Lato', + marginBottom: 16 + }, + signature: { + width: 200 + } +}); + +const VisaInvitationPDF = ({ + hostName = 'Karoliina Pellinen', + hostAddress = 'Junction Oy (2823785-1)', + hostAddress2 = 'PL1188, 00101 Helsinki', + hostPhone = '+358 45 2318287', + hostEmail = 'karoliina.pellinen@hackjunction.com', + hostTitle = 'Head of Participants', + hostCompany = 'Junction Oy', + date = '01.01.2019', + granteeFirstName = 'Juuso', + granteeLastName = 'Lappalainen', + granteeNationality = 'Finnish', + granteePassportNo = '755245118', + profession = 'Employed at VKontakte', + arrivalCity = 'Helsinki', + arrivalCountry = 'Finland', + arrivalDate = '01.01.2019' +}) => ( + + + + {hostName} + {hostAddress} + {hostAddress2} + {hostPhone} + {hostEmail} + {date} + Dear Madame/Sir, + + We hereby kindly ask you to issue Visitor Schengen Visa with one entry for {granteeFirstName}{' '} + {granteeLastName}, {granteeNationality}, bearer of passport number {granteePassportNo}. + + + {granteeFirstName}, {profession}, will arrive in {arrivalCity}, {arrivalCountry} on {arrivalDate}. + + + During their stay in {arrivalCountry}, {granteeFirstName} will be hosted at the Aalto University campus + (Väre & School of Business Building), Otaniementie 14, 02150 Espoo, and will be attending the following + events organised by Junction: + + 1) Junction 2019, from November 15th to 17th 2019 + + 2) Optional Junction 2019 pre-events (networking & conferences), from November 11th to 15th 2019 + + + We also invite {granteeFirstName} to get to know {arrivalCity} at their leisure and attend complementary + events such as those organised by Slush during the week after, November 18th to 25th. + + + Sincerely, + + {hostName} + + {hostTitle}, {hostCompany} + + + +); + +export default VisaInvitationPDF; diff --git a/frontend/src/components/modals/VisaInvitationDrawer/index.js b/frontend/src/components/modals/VisaInvitationDrawer/index.js new file mode 100644 index 000000000..0c139e8aa --- /dev/null +++ b/frontend/src/components/modals/VisaInvitationDrawer/index.js @@ -0,0 +1,111 @@ +import React, { useState, useCallback } from 'react'; +import styles from './VisaInvitationDrawer.module.scss'; + +import { PDFDownloadLink } from '@react-pdf/renderer'; +import { connect } from 'react-redux'; +import moment from 'moment'; + +import { Drawer, Input, Button as AntButton } from 'antd'; + +import Divider from 'components/generic/Divider'; +import Button from 'components/generic/Button'; +import { useFormField } from 'hooks/formHooks'; + +import VisaInvitationPDF from './VisaInvitationPDF'; +import * as DashboardSelectors from 'redux/dashboard/selectors'; + +const VisaInvitationDrawer = ({ registration }) => { + const [visible, setVisible] = useState(false); + const firstName = useFormField(registration ? registration.answers.firstName : ''); + const lastName = useFormField(registration ? registration.answers.lastName : ''); + const nationality = useFormField(registration ? registration.answers.nationality : ''); + const passportNo = useFormField(''); + const profession = useFormField(''); + const arrivalDate = useFormField(''); + const arrivalCity = 'Helsinki'; + const arrivalCountry = 'Finland'; + + const [generated, setGenerated] = useState(false); + + const handleClose = useCallback(() => { + setVisible(false); + }, []); + + return ( + + +

+ Just fill in a few more travel details and we'll generate a visa invitation letter for you. We will + not save this information for later use - in fact it is never sent anywhere from your device. +
+
+ Once you've generated the visa invitation letter, double check it to make sure all of the + information is correct. You can always generate a new invitation should you need to. +

+ + + + + + + + + + E.g. "Finnish", "American", "German" + + + + + + + E.g. "Student at Aalto University" / "Employed at BigCorp Inc." + + + + The date of your arrival to the country + + + Generate PDF + + + {generated && ( + + } + fileName="visa_invitation_letter.pdf" + > + + Download PDF + + + )} +
+ +
+ ); +}; + +const mapState = state => ({ + registration: DashboardSelectors.registration(state) +}); + +export default connect(mapState)(VisaInvitationDrawer); diff --git a/frontend/src/components/plots/ApplicationsCount.js b/frontend/src/components/plots/ApplicationsCount.js new file mode 100644 index 000000000..a4b681a32 --- /dev/null +++ b/frontend/src/components/plots/ApplicationsCount.js @@ -0,0 +1,15 @@ +import React from 'react'; + +import { Statistic } from 'antd'; +import { connect } from 'react-redux'; +import * as OrganiserSelectors from 'redux/organiser/selectors'; + +const ApplicationsCount = ({ value }) => { + return ; +}; + +const mapState = state => ({ + value: OrganiserSelectors.registrationsCount(state) +}); + +export default connect(mapState)(ApplicationsCount); diff --git a/frontend/src/components/plots/ApplicationsLast24h.js b/frontend/src/components/plots/ApplicationsLast24h.js new file mode 100644 index 000000000..b11c0c566 --- /dev/null +++ b/frontend/src/components/plots/ApplicationsLast24h.js @@ -0,0 +1,15 @@ +import React from 'react'; + +import { Statistic } from 'antd'; +import { connect } from 'react-redux'; +import * as OrganiserSelectors from 'redux/organiser/selectors'; + +const ApplicationsLast24h = ({ value }) => { + return ; +}; + +const mapState = state => ({ + value: OrganiserSelectors.registrationsLast24h(state) +}); + +export default connect(mapState)(ApplicationsLast24h); diff --git a/frontend/src/components/plots/ApplicationsOverTime.js b/frontend/src/components/plots/ApplicationsOverTime.js index 58e00f78b..bbb3dfe54 100644 --- a/frontend/src/components/plots/ApplicationsOverTime.js +++ b/frontend/src/components/plots/ApplicationsOverTime.js @@ -1,7 +1,9 @@ import React, { useMemo } from 'react'; import { sortBy } from 'lodash-es'; +import { connect } from 'react-redux'; import { ResponsiveContainer, BarChart, XAxis, YAxis, Tooltip, Legend, CartesianGrid, Bar } from 'recharts'; +import * as OrganiserSelectors from 'redux/organiser/selectors'; const ApplicationsOverTime = ({ data }) => { const formattedData = useMemo(() => { @@ -39,4 +41,8 @@ const ApplicationsOverTime = ({ data }) => { ); }; -export default ApplicationsOverTime; +const mapState = state => ({ + data: OrganiserSelectors.registrationsByDay(state) +}); + +export default connect(mapState)(ApplicationsOverTime); diff --git a/frontend/src/components/plots/RatingsSplit.js b/frontend/src/components/plots/RatingsSplit.js index efa98e3ac..43fc69e96 100644 --- a/frontend/src/components/plots/RatingsSplit.js +++ b/frontend/src/components/plots/RatingsSplit.js @@ -1,5 +1,8 @@ import React, { useMemo } from 'react'; import { sortBy } from 'lodash-es'; +import { connect } from 'react-redux'; + +import * as OrganiserSelectors from 'redux/organiser/selectors'; import { ResponsiveContainer, BarChart, XAxis, YAxis, Tooltip, Legend, CartesianGrid, Bar } from 'recharts'; @@ -39,4 +42,8 @@ const RatingsSplit = ({ data }) => { ); }; -export default RatingsSplit; +const mapState = state => ({ + data: OrganiserSelectors.registrationsByRating(state) +}); + +export default connect(mapState)(RatingsSplit); diff --git a/frontend/src/components/plots/ReviewedAverage.js b/frontend/src/components/plots/ReviewedAverage.js new file mode 100644 index 000000000..15ebe312a --- /dev/null +++ b/frontend/src/components/plots/ReviewedAverage.js @@ -0,0 +1,15 @@ +import React from 'react'; + +import { Statistic, Icon } from 'antd'; +import { connect } from 'react-redux'; +import * as OrganiserSelectors from 'redux/organiser/selectors'; + +const ReviewedAverage = ({ value }) => { + return } />; +}; + +const mapState = state => ({ + value: OrganiserSelectors.averageRating(state) +}); + +export default connect(mapState)(ReviewedAverage); diff --git a/frontend/src/components/plots/ReviewedPercent.js b/frontend/src/components/plots/ReviewedPercent.js new file mode 100644 index 000000000..ec9e87db8 --- /dev/null +++ b/frontend/src/components/plots/ReviewedPercent.js @@ -0,0 +1,15 @@ +import React from 'react'; + +import { Statistic } from 'antd'; +import { connect } from 'react-redux'; +import * as OrganiserSelectors from 'redux/organiser/selectors'; + +const ReviewedPercent = ({ value }) => { + return ; +}; + +const mapState = state => ({ + value: OrganiserSelectors.percentReviewed(state) +}); + +export default connect(mapState)(ReviewedPercent); diff --git a/frontend/src/components/plots/ReviewersList.js b/frontend/src/components/plots/ReviewersList.js index e864a4e8b..36b582807 100644 --- a/frontend/src/components/plots/ReviewersList.js +++ b/frontend/src/components/plots/ReviewersList.js @@ -1,17 +1,27 @@ import React, { useMemo } from 'react'; import { List, Avatar } from 'antd'; -import { map, sortBy } from 'lodash-es'; +import { sortBy } from 'lodash-es'; +import { connect } from 'react-redux'; -const ReviewersList = ({ data, userProfilesMap = {} }) => { +import * as OrganiserSelectors from 'redux/organiser/selectors'; + +const ReviewersList = ({ data, userProfilesMap = {}, averagesMap = {} }) => { const formattedData = useMemo(() => { - const asArray = map(Object.keys(data), userId => ({ - user: userProfilesMap[userId] || {}, - ratings: data[userId] - })); + const arr = []; + Object.keys(data).forEach(userId => { + const user = userProfilesMap[userId]; + if (user) { + arr.push({ + user, + ratings: data[userId], + average: averagesMap[userId] + }); + } + }); - return sortBy(asArray, 'ratings'); - }, [data, userProfilesMap]); + return sortBy(arr, item => item.ratings * -1); + }, [data, userProfilesMap, averagesMap]); return ( { } title={`${item.user.firstName} ${item.user.lastName}`} - description={`${item.ratings} applications reviewed`} + description={`${item.ratings} reviews / ${item.average.toFixed(2)} average rating`} /> )} @@ -30,4 +40,10 @@ const ReviewersList = ({ data, userProfilesMap = {} }) => { ); }; -export default ReviewersList; +const mapState = state => ({ + data: OrganiserSelectors.registrationsByReviewer(state), + userProfilesMap: OrganiserSelectors.organisersMap(state), + averagesMap: OrganiserSelectors.reviewAverageByReviewer(state) +}); + +export default connect(mapState)(ReviewersList); diff --git a/frontend/src/components/plots/TeamsCount.js b/frontend/src/components/plots/TeamsCount.js new file mode 100644 index 000000000..d0f105de8 --- /dev/null +++ b/frontend/src/components/plots/TeamsCount.js @@ -0,0 +1,15 @@ +import React from 'react'; + +import { Statistic } from 'antd'; +import { connect } from 'react-redux'; +import * as OrganiserSelectors from 'redux/organiser/selectors'; + +const TeamsCount = ({ value }) => { + return ; +}; + +const mapState = state => ({ + value: OrganiserSelectors.teamsCount(state) +}); + +export default connect(mapState)(TeamsCount); diff --git a/frontend/src/constants/filters.js b/frontend/src/constants/filters.js index 67f2d08db..fad2ca636 100644 --- a/frontend/src/constants/filters.js +++ b/frontend/src/constants/filters.js @@ -35,6 +35,14 @@ const FilterOptions = [ id: 'tags-not-contain', label: 'Tags do not contain' }, + { + id: 'apply-as-team', + label: 'Applying as team' + }, + { + id: 'not-apply-as-team', + label: 'Not applying as team' + }, { id: 'field-equals', label: 'Field equals' diff --git a/frontend/src/index.js b/frontend/src/index.js index b54b76f44..8c673a36e 100755 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -13,6 +13,14 @@ import config from 'constants/config'; const { store, persistor } = configureStore(); +/** Disable log statements in production */ +function noop() {} +if (process.env.NODE_ENV !== 'development') { + console.log = noop; + console.warn = noop; + console.error = noop; +} + ReactDOM.render( } persistor={persistor}> diff --git a/frontend/src/pages/Account/AccountDashboard/index.js b/frontend/src/pages/Account/AccountDashboard/index.js index dbcbd7833..7351aaed9 100644 --- a/frontend/src/pages/Account/AccountDashboard/index.js +++ b/frontend/src/pages/Account/AccountDashboard/index.js @@ -19,7 +19,7 @@ const AccountDashboard = ({ registrations, updateRegistrations }) => {

Your registrations

{registrations.map(registration => ( - + ))} diff --git a/frontend/src/pages/EventDashboard/EventDashboardHome/EventDashboardHomeRegistration/RegistrationStatusBlock.js b/frontend/src/pages/EventDashboard/EventDashboardHome/EventDashboardHomeRegistration/RegistrationStatusBlock.js new file mode 100644 index 000000000..28b1d8508 --- /dev/null +++ b/frontend/src/pages/EventDashboard/EventDashboardHome/EventDashboardHomeRegistration/RegistrationStatusBlock.js @@ -0,0 +1,198 @@ +import React, { useCallback, useState } from 'react'; + +import { connect } from 'react-redux'; +import { Col, Button as AntButton, Popconfirm } from 'antd'; + +import { RegistrationStatuses } from '@hackjunction/shared'; +import NotificationBlock from 'components/generic/NotificationBlock'; +import Button from 'components/generic/Button'; +import Divider from 'components/generic/Divider'; + +import * as DashboardSelectors from 'redux/dashboard/selectors'; +import * as DashboardActions from 'redux/dashboard/actions'; + +const STATUSES = RegistrationStatuses.asObject; + +const RegistrationStatusBlock = ({ event, registration, confirmRegistration, cancelRegistration }) => { + const [loading, setLoading] = useState(false); + const handleConfirm = useCallback(() => { + setLoading(true); + confirmRegistration(event.slug).finally(() => { + setLoading(false); + }); + }, [event.slug, confirmRegistration]); + + const handleCancel = useCallback(() => { + setLoading(true); + cancelRegistration(event.slug).finally(() => { + setLoading(false); + }); + }, [event.slug, cancelRegistration]); + + if (!registration || !event) return null; + + const PENDING_STATUSES = [STATUSES.pending.id, STATUSES.softAccepted.id, STATUSES.softRejected.id]; + + if (PENDING_STATUSES.indexOf(registration.status) !== -1) { + return ( + + + + } + /> + + ); + } + + if (registration.status === STATUSES.accepted.id) { + return ( + + + + } + /> + + ); + } + + if (registration.status === STATUSES.rejected.id) { + return ( + + + + } + /> + + ); + } + + if (registration.status === STATUSES.confirmed.id) { + return ( + + + + To stay in the loop and let your friends know you're coming, you should go attend the{' '} + + Junction 2019 Facebook event! + {' '} + For any other questions and further event details such as tracks and challenges, see the{' '} + + event website + + . +
+
+ Can't make it after all? Bummer. Please let us know by clicking the button below, so we can + accept someone else in your place. +
+ + + Cancel participation + + +

+ } + /> + + ); + } + + if (registration.status === STATUSES.cancelled.id) { + return ( + + + + )} /> diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AdminPage.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AdminPage.js index eab4a21a5..1ff4d8e0f 100644 --- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AdminPage.js +++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AdminPage.js @@ -1,16 +1,20 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import './AdminPage.scss'; import { connect } from 'react-redux'; import { groupBy, filter } from 'lodash-es'; import { RegistrationStatuses } from '@hackjunction/shared'; -import { Row, Col, Card, Statistic, Tag, List, Button as AntButton } from 'antd'; +import { Row, Col, Card, Statistic, Tag, List, Button as AntButton, notification } from 'antd'; import Divider from 'components/generic/Divider'; import * as OrganiserSelectors from 'redux/organiser/selectors'; +import * as AuthSelectors from 'redux/auth/selectors'; +import RegistrationsService from 'services/registrations'; const STATUSES = RegistrationStatuses.asObject; -const AdminPage = ({ registrations }) => { +const AdminPage = ({ registrations, idToken, event }) => { + const [bulkAcceptLoading, setBulkAcceptLoading] = useState(false); + const [bulkRejectLoading, setBulkRejectLoading] = useState(false); const groupedByStatus = useMemo(() => { return groupBy(registrations, 'status'); }, [registrations]); @@ -24,6 +28,26 @@ const AdminPage = ({ registrations }) => { }, 0); }; + const handleBulkAccept = () => { + setBulkAcceptLoading(true); + RegistrationsService.bulkAcceptRegistrationsForEvent(idToken, event.slug) + .then(data => { + notification.success({ + message: 'Success!', + description: 'All soft accepted registrations have been accepted' + }); + }) + .catch(err => { + notification.error({ + message: 'Something went wrong...', + description: "Are you sure you're connected to the internet?" + }); + }) + .finally(() => { + setBulkAcceptLoading(false); + }); + }; + const total = registrations.length; const rated = filter(registrations, reg => reg.rating).length; const ratedOrAssigned = filter(registrations, reg => reg.rating || reg.assignedTo).length; @@ -34,7 +58,7 @@ const AdminPage = ({ registrations }) => { description: 'Change the status of all Soft Accepted participants to Accepted, and notify them via email that they have been accepted to the event!', extra: ( - window.alert('Get permission from Juuso to do this ;--)')} type="link"> + Accept ) @@ -44,15 +68,17 @@ const AdminPage = ({ registrations }) => { description: 'Change the status of all Soft Rejected participants to Rejected, and notify them via email that they did not make it.', extra: ( - window.alert('Get permission from Juuso to do this ;--)')} type="link"> + window.alert('Get permission from Juuso to do this ;--)')} + type="link" + loading={bulkRejectLoading} + > Reject ) } ]; - console.log('RATED', rated); - return ( @@ -152,6 +178,8 @@ const AdminPage = ({ registrations }) => { }; const mapState = state => ({ - registrations: OrganiserSelectors.registrations(state) + registrations: OrganiserSelectors.registrations(state), + event: OrganiserSelectors.event(state), + idToken: AuthSelectors.getIdToken(state) }); export default connect(mapState)(AdminPage); diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AssignAttendeesPage.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AssignAttendeesPage.js index 0c1b7152a..a4b30e9b7 100644 --- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AssignAttendeesPage.js +++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AssignAttendeesPage.js @@ -1,7 +1,7 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import styles from './AssignAttendeesPage.module.scss'; -import { Button as AntButton, Modal, message } from 'antd'; +import { Button as AntButton, Modal, message, Switch } from 'antd'; import { connect } from 'react-redux'; import Divider from 'components/generic/Divider'; @@ -15,6 +15,7 @@ import RegistrationsService from 'services/registrations'; import BulkEditRegistrationDrawer from 'components/modals/BulkEditRegistrationDrawer'; const SearchAttendeesPage = ({ idToken, event, registrations = [], registrationsLoading, updateRegistrations }) => { + const [hideRated, setHideRated] = useState(false); const { slug } = event; const handleSelfAssign = () => { Modal.confirm({ @@ -42,11 +43,24 @@ const SearchAttendeesPage = ({ idToken, event, registrations = [], registrations }); }; + const filtered = useMemo(() => { + return registrations.filter(registration => { + if (hideRated) { + if (registration.rating) return false; + } + return true; + }); + }, [registrations, hideRated]); + return (
- {registrations.length} registrations + {filtered.length} registrations
+
+ Hide rated registrations + +
Assign random registrations @@ -54,7 +68,7 @@ const SearchAttendeesPage = ({ idToken, event, registrations = [], registrations r._id)} />
- +
); diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AssignAttendeesPage.module.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AssignAttendeesPage.module.scss index 14e6227f4..df0249912 100644 --- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AssignAttendeesPage.module.scss +++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AssignAttendeesPage.module.scss @@ -15,3 +15,14 @@ flex-wrap: wrap; justify-content: flex-end; } + +.toggle { + display: flex; + flex-direction: row; + align-items: center; + padding: 0 0.5rem; +} + +.toggleText { + padding-right: 1rem; +} diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AttendeeFilters.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AttendeeFilters.js index 6ace4eccc..12c3aee23 100644 --- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AttendeeFilters.js +++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AttendeeFilters.js @@ -201,81 +201,85 @@ const AttendeeFilters = ({ event, registrations, filters = [], setFilters }) => ); }; - const renderItemValue = filter => { + const renderItemValue = (filter, label) => { switch (filter.type) { case 'status-equals': case 'status-nequals': { - return RegistrationStatuses.asArray + const statuses = RegistrationStatuses.asArray .filter(status => { return filter.value && filter.value.indexOf(status.id) !== -1; }) .map(status => { return {status.label}; }); + return ( + + {label} {statuses} + + ); } case 'rating-lte': case 'rating-gte': { - return ; + return ( + + {label} + + ); } case 'tags-contain': case 'tags-not-contain': - return event.tags + const tags = event.tags .filter(tag => { return filter.value && filter.value.indexOf(tag.label) !== -1; }) .map(tag => { return {tag.label}; }); + return ( + + {label} {tags} + + ); case 'field-equals': return ( - - {filter.field} - EQUALS - {filter.value} + + {filter.field} EQUALS {filter.value} ); case 'field-nequals': return ( - - {filter.field} - IS NOT - {filter.value} + + {filter.field} DOES NOT EQUAL {filter.value} ); case 'field-empty': return ( - - {filter.field} - IS EMPTY + + {filter.field} IS EMPTY ); case 'field-not-empty': return ( - - {filter.field} - IS NOT EMPTY + + {filter.field} IS NOT EMPTY ); case 'field-contains': { return ( - - {filter.field} - CONTAINS - {filter.value} + + {filter.field} CONTAINS {filter.value} ); } case 'field-not-contains': { return ( - - {filter.field} - DOES NOT CONTAIN - {filter.value} + + {filter.field} DOES NOT CONTAIN {filter.value} ); } default: - return null; + return {label}; } }; @@ -287,15 +291,12 @@ const AttendeeFilters = ({ event, registrations, filters = [], setFilters }) => - {label} - handleRemove(idx)}> - Remove - - + title={
{renderItemValue(filter, label)}
} + description={ + handleRemove(idx)}> + Remove filter + } - description={
{renderItemValue(filter)}
} /> ); }); @@ -324,36 +325,6 @@ const AttendeeFilters = ({ event, registrations, filters = [], setFilters }) => ); - - // return ( - // - // - // - // - // - // - // - // - // - // - // - // ); }; const mapState = state => ({ diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/SearchAttendeesPage.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/SearchAttendeesPage.js index f1c341894..dfce04240 100644 --- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/SearchAttendeesPage.js +++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/SearchAttendeesPage.js @@ -9,6 +9,7 @@ import * as FilterUtils from 'utils/filters'; import Divider from 'components/generic/Divider'; import AttendeeTable from 'components/tables/AttendeeTable'; import BulkEditRegistrationDrawer from 'components/modals/BulkEditRegistrationDrawer'; +import BulkEmailDrawer from 'components/modals/BulkEmailDrawer'; import AttendeeFilters from './AttendeeFilters'; const SearchAttendeesPage = ({ registrations, registrationsLoading, filters }) => { @@ -16,10 +17,15 @@ const SearchAttendeesPage = ({ registrations, registrationsLoading, filters }) = const renderBulkActions = () => { if (!registrations.length) return null; + const ids = registrations.map(r => r._id); + const userIds = registrations.map(r => r.user); return (
- {registrations.length} registrations - r._id)} /> +
+ {registrations.length} registrations +
+ +
); }; diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/TeamsPage.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/TeamsPage.js index 93f4c2811..fd92b3f69 100644 --- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/TeamsPage.js +++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/TeamsPage.js @@ -1,17 +1,20 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import styles from './TeamsPage.module.scss'; import { connect } from 'react-redux'; -import { Tag, Table, Switch } from 'antd'; +import { Tag, Table, Switch, Input } from 'antd'; import { sumBy } from 'lodash-es'; import * as OrganiserSelectors from 'redux/organiser/selectors'; +import PageWrapper from 'components/PageWrapper'; import AttendeeTable from 'components/tables/AttendeeTable'; import BulkEditRegistrationDrawer from 'components/modals/BulkEditRegistrationDrawer'; const TeamsPage = ({ event, teams, registrationsLoading, teamsLoading, registrations, registrationsMap }) => { const [onlyLocked, setOnlyLocked] = useState(false); + const [onlyReviewed, setOnlyReviewed] = useState(false); + const [minRating, setMinRating] = useState(0); const renderAttendees = team => { return ( @@ -43,11 +46,14 @@ const TeamsPage = ({ event, teams, registrationsLoading, teamsLoading, registrat .filter(member => typeof member !== 'undefined'); const ownerMapped = registrationsMap[team.owner] || {}; const allMembers = membersMapped.concat(ownerMapped); + const reviewedCount = allMembers.filter(member => member && member.rating).length; + const memberCount = allMembers.length; return { ...team, owner: ownerMapped, members: allMembers, - avgRating: (sumBy(allMembers, m => m.rating || 0) / allMembers.length).toFixed(2) + avgRating: (sumBy(allMembers, m => m.rating || 0) / allMembers.length).toFixed(2), + reviewedPercent: Math.floor((reviewedCount * 100) / memberCount) }; }); }, [teams, registrationsMap]); @@ -56,19 +62,46 @@ const TeamsPage = ({ event, teams, registrationsLoading, teamsLoading, registrat if (onlyLocked && !team.locked) { return false; } + if (onlyReviewed && team.reviewedPercent < 100) { + return false; + } + if (minRating && team.avgRating < minRating) { + return false; + } return true; }); + const filteredMemberIds = teamsFiltered.reduce((res, team) => { + return res.concat(team.members.map(m => m._id)); + }, []); + return ( - +
Only locked teams
+
+ Only fully reviewed teams + +
+
+ Min. team rating + setMinRating(e.target.value)} + /> +
{teamsFiltered.length} teams +
{ - const reviewedCount = members.filter(member => member && member.rating).length; - const memberCount = members.length; - if (reviewedCount === memberCount) { + render={percent => { + if (percent === 100) { return 100%; } else { - return {Math.floor((reviewedCount * 100) / memberCount)}%; + return {percent}%; } }} /> @@ -120,7 +151,7 @@ const TeamsPage = ({ event, teams, registrationsLoading, teamsLoading, registrat render={locked => (locked ? Yes : No)} />
-
+ ); }; diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/TeamsPage.module.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/TeamsPage.module.scss index c95b6ea91..645cb84cc 100644 --- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/TeamsPage.module.scss +++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/TeamsPage.module.scss @@ -14,9 +14,11 @@ .filters { display: flex; flex-direction: row; + align-items: flex-start; flex-wrap: wrap; .filterItem { + margin-bottom: 1rem; padding: 1rem; display: flex; flex-direction: row; diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventStats/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventStats/index.js index 606854993..750dd16d6 100644 --- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventStats/index.js +++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventStats/index.js @@ -1,138 +1,79 @@ -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect } from 'react'; import styles from './OrganiserEditEventStats.module.scss'; -import { isEmpty } from 'lodash-es'; import { connect } from 'react-redux'; -import moment from 'moment'; -import { PageHeader, Row, Col, Statistic, Card, List, Empty, Icon, Rate } from 'antd'; +import { PageHeader, Row, Col, Card } from 'antd'; -import Divider from 'components/generic/Divider'; -import Button from 'components/generic/Button'; -import PageWrapper from 'components/PageWrapper'; import * as OrganiserSelectors from 'redux/organiser/selectors'; import * as OrganiserActions from 'redux/organiser/actions'; + +import Divider from 'components/generic/Divider'; +import PageWrapper from 'components/PageWrapper'; import ApplicationsOverTime from 'components/plots/ApplicationsOverTime'; import RatingsSplit from 'components/plots/RatingsSplit'; import ReviewersList from 'components/plots/ReviewersList'; -const OrganiserEditEventStats = ({ stats, statsLoading, slug, updateEventStats, organisersMap }) => { - const updateStats = useCallback(() => { - updateEventStats(slug); - }, [slug, updateEventStats]); +import ApplicationsCount from 'components/plots/ApplicationsCount'; +import TeamsCount from 'components/plots/TeamsCount'; +import ReviewedPercent from 'components/plots/ReviewedPercent'; +import ReviewedAverage from 'components/plots/ReviewedAverage'; +import ApplicationsLast24h from 'components/plots/ApplicationsLast24h'; - console.log(stats); +const OrganiserEditEventStats = ({ slug, loading, updateRegistrations, updateTeams }) => { + useEffect(() => { + updateRegistrations(slug); + updateTeams(slug); + }, [slug, updateRegistrations, updateTeams]); const renderContent = () => { - if (isEmpty(stats)) { - return ( - -
-

No data

-
-
- } - /> - ); - } - return ( - + - + - + - } - /> + - + - + - + - - - - - - - ( - - - - )} - /> - - - - - - ( - - - - )} - /> + @@ -140,23 +81,25 @@ const OrganiserEditEventStats = ({ stats, statsLoading, slug, updateEventStats, }; return ( - + Key stats for the event

} footer={renderContent()} />
); }; -const mapStateToProps = state => ({ - stats: OrganiserSelectors.stats(state), - statsLoading: OrganiserSelectors.statsLoading(state), - organisersMap: OrganiserSelectors.organisersMap(state) +const mapState = state => ({ + loading: + OrganiserSelectors.registrationsLoading(state) || + OrganiserSelectors.teamsLoading(state) || + OrganiserSelectors.organisersLoading(state) }); -const mapDispatchToProps = dispatch => ({ - updateEventStats: slug => dispatch(OrganiserActions.updateEventStats(slug)) +const mapDispatch = dispatch => ({ + updateRegistrations: slug => dispatch(OrganiserActions.updateRegistrationsForEvent(slug)), + updateTeams: slug => dispatch(OrganiserActions.updateTeamsForEvent(slug)) }); export default connect( - mapStateToProps, - mapDispatchToProps + mapState, + mapDispatch )(OrganiserEditEventStats); diff --git a/frontend/src/redux/configureStore.js b/frontend/src/redux/configureStore.js index 8e03843fd..7f25a600a 100644 --- a/frontend/src/redux/configureStore.js +++ b/frontend/src/redux/configureStore.js @@ -13,7 +13,7 @@ import createRootReducer from './rootReducer'; const persistConfig = { key: 'root', storage, - blacklist: ['router', 'organiser', 'dashboard', 'events'], + whitelist: ['auth', 'user'], stateReconciler: autoMergeLevel2 }; diff --git a/frontend/src/redux/dashboard/actions.js b/frontend/src/redux/dashboard/actions.js index 35f57e1d1..4a254214a 100644 --- a/frontend/src/redux/dashboard/actions.js +++ b/frontend/src/redux/dashboard/actions.js @@ -1,4 +1,5 @@ import { sortBy } from 'lodash-es'; +import { push } from 'connected-react-router'; import * as ActionTypes from './actionTypes'; import * as AuthSelectors from '../auth/selectors'; @@ -23,18 +24,39 @@ export const updateRegistration = slug => (dispatch, getState) => { dispatch({ type: ActionTypes.UPDATE_REGISTRATION, - promise: RegistrationsService.getRegistration(idToken, slug).catch(err => { - if (err.response.status === 404) { - return Promise.resolve({}); - } - return Promise.reject(err); - }), + promise: RegistrationsService.getRegistration(idToken, slug), meta: { - onFailure: e => console.log('Error updating dashboard registration', e) + onFailure: () => dispatch(push('/')) } }); }; +export const confirmRegistration = slug => async (dispatch, getState) => { + const idToken = AuthSelectors.getIdToken(getState()); + + const registration = await RegistrationsService.confirmRegistration(idToken, slug); + + dispatch({ + type: ActionTypes.EDIT_REGISTRATION, + payload: registration + }); + + return registration; +}; + +export const cancelRegistration = slug => async (dispatch, getState) => { + const idToken = AuthSelectors.getIdToken(getState()); + + const registration = await RegistrationsService.cancelRegistration(idToken, slug); + + dispatch({ + type: ActionTypes.EDIT_REGISTRATION, + payload: registration + }); + + return registration; +}; + export const editRegistration = (slug, data) => async (dispatch, getState) => { const idToken = AuthSelectors.getIdToken(getState()); diff --git a/frontend/src/redux/eventdetail/selectors.js b/frontend/src/redux/eventdetail/selectors.js index 2030d44b3..c245f2680 100644 --- a/frontend/src/redux/eventdetail/selectors.js +++ b/frontend/src/redux/eventdetail/selectors.js @@ -11,6 +11,10 @@ export const registration = state => state.eventdetail.registration.data; export const registrationLoading = state => state.eventdetail.registration.loading; export const registrationError = state => state.eventdetail.registration.error; export const registrationUpdated = state => state.eventdetail.registration.updated; +export const hasRegistration = createSelector( + registration, + registration => registration && registration.hasOwnProperty('_id') +); export const eventStatus = createSelector( event, diff --git a/frontend/src/redux/organiser/selectors.js b/frontend/src/redux/organiser/selectors.js index 02c9dacc2..706f47683 100644 --- a/frontend/src/redux/organiser/selectors.js +++ b/frontend/src/redux/organiser/selectors.js @@ -1,6 +1,8 @@ import { createSelector } from 'reselect'; +import { meanBy, countBy, groupBy, mapValues } from 'lodash-es'; import * as FilterUtils from 'utils/filters'; import * as AuthSelectors from 'redux/auth/selectors'; +import moment from 'moment'; export const event = state => state.organiser.event.data; export const eventLoading = state => state.organiser.event.loading; @@ -43,6 +45,15 @@ export const registrationsAssigned = createSelector( } ); +export const registrationsReviewed = createSelector( + registrations, + registrations => { + return registrations.filter(registration => { + return !!registration.rating; + }); + } +); + export const teams = state => state.organiser.teams.data; export const teamsLoading = state => state.organiser.teams.loading; export const teamsError = state => state.organiser.teams.error; @@ -61,3 +72,88 @@ export const teamsPopulated = createSelector( }); } ); + +/** Stats selectors */ +export const registrationsCount = createSelector( + registrations, + registrations => registrations.length +); + +export const teamsCount = createSelector( + teams, + teams => teams.length +); + +export const percentReviewed = createSelector( + registrations, + registrations => { + const { reviewed, total } = registrations.reduce( + (res, registration) => { + res.total += 1; + if (registration.rating) { + res.reviewed += 1; + } + return res; + }, + { + reviewed: 0, + total: 0 + } + ); + return (reviewed * 100) / total; + } +); + +export const averageRating = createSelector( + registrationsReviewed, + registrations => { + return meanBy(registrations, 'rating'); + } +); + +export const registrationsLast24h = createSelector( + registrations, + registrations => { + return registrations.filter(registration => { + return registration.createdAt > Date.now() - 1000 * 60 * 60 * 24; + }).length; + } +); + +export const registrationsByDay = createSelector( + registrations, + registrations => { + return countBy(registrations, r => moment(r.createdAt).format('YYYY-MM-DD')); + } +); + +export const registrationsByRating = createSelector( + registrationsReviewed, + registrations => { + return countBy(registrations, 'rating'); + } +); + +export const registrationsByReviewer = createSelector( + registrationsReviewed, + registrations => { + return countBy(registrations, 'ratedBy'); + } +); + +export const registrationsBySecretCode = createSelector( + registrations, + registrations => { + return countBy(registrations, 'answers.secretCode'); + } +); + +export const reviewAverageByReviewer = createSelector( + registrationsReviewed, + registrations => { + const grouped = groupBy(registrations, 'ratedBy'); + return mapValues(grouped, registrations => { + return meanBy(registrations, 'rating'); + }); + } +); diff --git a/frontend/src/services/email.js b/frontend/src/services/email.js new file mode 100644 index 000000000..bab14bc60 --- /dev/null +++ b/frontend/src/services/email.js @@ -0,0 +1,33 @@ +import _axios from 'services/axios'; + +const EmailService = {}; + +function config(idToken) { + return { + headers: { + Authorization: `Bearer ${idToken}` + } + }; +} + +const BASE_ROUTE = '/email'; + +EmailService.sendPreviewEmail = (idToken, slug, to, params) => { + const data = { + to, + params + }; + return _axios.post(`${BASE_ROUTE}/${slug}/preview`, data, config(idToken)); +}; + +EmailService.sendBulkEmail = (idToken, slug, recipients, params, uniqueId) => { + const data = { + recipients, + params, + uniqueId + }; + + return _axios.post(`${BASE_ROUTE}/${slug}/send`, data, config(idToken)); +}; + +export default EmailService; diff --git a/frontend/src/services/events.js b/frontend/src/services/events.js index 731df5576..d0ec8efab 100644 --- a/frontend/src/services/events.js +++ b/frontend/src/services/events.js @@ -20,10 +20,6 @@ EventsService.getEventsByOrganiser = idToken => { return _axios.get(`${BASE_ROUTE}`, config(idToken)); }; -EventsService.getEventStats = (idToken, slug) => { - return _axios.get(`${BASE_ROUTE}/${slug}/stats`, config(idToken)); -}; - EventsService.getPublicEvents = () => { return _axios.get(`${BASE_ROUTE}/public`); }; diff --git a/frontend/src/services/registrations.js b/frontend/src/services/registrations.js index 9f1bd1888..16297882c 100644 --- a/frontend/src/services/registrations.js +++ b/frontend/src/services/registrations.js @@ -12,64 +12,95 @@ function config(idToken) { const BASE_ROUTE = '/registrations'; +/** Get all of your registrations as the logged in user + * GET / + */ RegistrationsService.getUserRegistrations = idToken => { return _axios.get(`${BASE_ROUTE}`, config(idToken)); }; +/** Get your registration for an event as the logged in user + * GET /:slug + */ RegistrationsService.getRegistration = (idToken, slug) => { return _axios.get(`${BASE_ROUTE}/${slug}`, config(idToken)); }; -RegistrationsService.createRegistration = (idToken, slug, data, subscribe = false) => { - return _axios.post(`${BASE_ROUTE}/${slug}/?subscribe=${subscribe}`, data, config(idToken)); +/** Create a registration for an event as the logged in user + * POST /:slug + */ +RegistrationsService.createRegistration = (idToken, slug, data) => { + return _axios.post(`${BASE_ROUTE}/${slug}`, data, config(idToken)); }; +/** Update a registration for an event as the logged in user + * PATCH /:slug + */ RegistrationsService.updateRegistration = (idToken, slug, data) => { return _axios.patch(`${BASE_ROUTE}/${slug}`, data, config(idToken)); }; +/** Confirm participation for an event as the logged in user + * PATCH /:slug/confirm + */ +RegistrationsService.confirmRegistration = (idToken, slug) => { + return _axios.patch(`${BASE_ROUTE}/${slug}/confirm`, {}, config(idToken)); +}; + +/** Cancel participation for an event as the logged in user + * PATCH /:slug/cancel + */ +RegistrationsService.cancelRegistration = (idToken, slug) => { + return _axios.patch(`${BASE_ROUTE}/${slug}/cancel`, {}, config(idToken)); +}; + +/** Get all registrations for event + * GET /:slug/all + */ RegistrationsService.getRegistrationsForEvent = (idToken, slug) => { return _axios.get(`${BASE_ROUTE}/${slug}/all`, config(idToken)); }; +/** Edit registrations in bulk + * PATCH /:slug/bulk + */ RegistrationsService.bulkEditRegistrationsForEvent = (idToken, slug, registrationIds, edits) => { return _axios.patch(`${BASE_ROUTE}/${slug}/bulk`, { registrationIds, edits }, config(idToken)); }; -RegistrationsService.searchRegistrationsForEvent = (idToken, slug, filters) => { - const options = { - ...config(idToken), - params: filters - }; - return _axios.get(`${BASE_ROUTE}/${slug}/search`, options); +/** Accept all soft-accepted registrations + * PATCH /:slug/bulk/accept + */ +RegistrationsService.bulkAcceptRegistrationsForEvent = (idToken, slug) => { + return _axios.patch(`${BASE_ROUTE}/${slug}/bulk/accept`, {}, config(idToken)); }; -RegistrationsService.assignRandomRegistrations = (idToken, slug) => { - return _axios.get(`${BASE_ROUTE}/${slug}/assign`, config(idToken)); +/** Reject all soft-rejected registrations + * PATCH /:slug/bulk/reject + */ +RegistrationsService.bulkRejectRegistrationsForEvent = (idToken, slug) => { + return _axios.patch(`${BASE_ROUTE}/${slug}/bulk/reject`); }; -RegistrationsService.assignRegistration = (idToken, slug, registrationId, userId) => { - return _axios.patch(`${BASE_ROUTE}/${slug}/assign`, { registrationId, userId }, config(idToken)); +/** Assign 10 registrations to logged in user + * GET /:slug/assign + */ +RegistrationsService.assignRandomRegistrations = (idToken, slug) => { + return _axios.get(`${BASE_ROUTE}/${slug}/assign`, config(idToken)); }; +/** Get a single registration with all fields + * GET /:slug/:registrationId + */ RegistrationsService.getFullRegistration = (idToken, slug, registrationId) => { return _axios.get(`${BASE_ROUTE}/${slug}/${registrationId}`, config(idToken)); }; +/** Edit a single registration + * PATCH /:slug/:registrationId + */ RegistrationsService.editRegistration = (idToken, slug, registrationId, data) => { return _axios.patch(`${BASE_ROUTE}/${slug}/${registrationId}`, data, config(idToken)); }; -RegistrationsService.rateRegistration = (idToken, slug, registrationId, rating) => { - return _axios.patch(`${BASE_ROUTE}/${slug}/${registrationId}/rate`, { rating }, config(idToken)); -}; - -RegistrationsService.acceptRegistration = (idToken, slug, registrationId) => { - return _axios.patch(`${BASE_ROUTE}/${slug}/${registrationId}/accept`, {}, config(idToken)); -}; - -RegistrationsService.rejectRegistration = (idToken, slug, registrationId) => { - return _axios.patch(`${BASE_ROUTE}/${slug}/${registrationId}/reject`, {}, config(idToken)); -}; - export default RegistrationsService; diff --git a/frontend/src/utils/filters.js b/frontend/src/utils/filters.js index 711b0c8bb..5112d4115 100644 --- a/frontend/src/utils/filters.js +++ b/frontend/src/utils/filters.js @@ -1,4 +1,5 @@ import { difference } from 'lodash-es'; +import objectPath from 'object-path'; const isEmpty = value => { if (Array.isArray(value)) { @@ -19,18 +20,28 @@ const contains = (value, answer) => { if (Array.isArray(answer)) { return answer.indexOf(value) !== -1; } else if (typeof answer === 'string') { - return answer - .toLowerCase() - .trim() - .indexOf(value.toLowerCase().trim()); + return ( + answer + .toLowerCase() + .trim() + .indexOf(value.toLowerCase().trim()) !== -1 + ); } return false; }; const equals = (value, answer) => { if (!value || !answer) return false; + const trimmed = value.trim().toLowerCase(); if (typeof answer === 'string') { - return value.toLowerCase().trim() === answer.toLowerCase().trim(); + return trimmed === answer.toLowerCase().trim(); + } + if (typeof answer === 'boolean') { + if (answer) { + return trimmed === 'true' || trimmed === 'yes'; + } else { + return trimmed === 'false' || trimmed === 'no'; + } } return answer === value; @@ -73,23 +84,31 @@ const filter = (registration, filter) => { difference(filter.value, registration.tags).length === filter.value.length ); } + case 'apply-as-team': { + const applyAsTeam = objectPath.get(registration, 'answers.teamOptions.applyAsTeam'); + return applyAsTeam === true; + } + case 'not-apply-as-team': { + const applyAsTeam = objectPath.get(registration, 'answers.teamOptions.applyAsTeam'); + return applyAsTeam !== true; + } case 'field-equals': { - return equals(filter.value, registration.answers[filter.field]); + return equals(filter.value, objectPath.get(registration, `answers.${filter.field}`)); } case 'field-nequals': { - return !equals(filter.value, registration.answers[filter.field]); + return !equals(filter.value, objectPath.get(registration, `answers.${filter.field}`)); } case 'field-contains': { - return contains(filter.value, registration.answers[filter.field]); + return contains(filter.value, objectPath.get(registration, `answers.${filter.field}`)); } case 'field-not-contains': { - return !contains(filter.value, registration.answers[filter.field]); + return !contains(filter.value, objectPath.get(registration, `answers.${filter.field}`)); } case 'field-empty': { - return isEmpty(registration.answers[filter.field]); + return isEmpty(objectPath.get(registration, `answers.${filter.field}`)); } case 'field-not-empty': { - return !isEmpty(registration.answers[filter.field]); + return !isEmpty(objectPath.get(registration, `answers.${filter.field}`)); } default: return true; diff --git a/package.json b/package.json index 9241a7250..045da08f0 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,9 @@ "setup": "better-npm-run setup", "dev:frontend": "cd frontend && npm start", "dev:backend": "cd backend && npm run dev", - "db-sync:prod-to-local": "sh ./scripts/sync-production-to-local.sh", - "db-sync:staging-to-dev": "sh ./scripts/sync-staging-to-dev.sh", - "db-sync:staging-to-local": "sh ./scripts/sync-staging-to-local.sh" + "db-sync:local": "sh ./scripts/sync-production-to-local.sh", + "db-sync:dev": "sh ./scripts/sync-production-to-dev.sh", + "db-sync:staging": "sh ./scripts/sync-production-to-staging.sh" }, "betterScripts": { "setup": "better-npm-run setup:backend && better-npm-run setup:frontend", diff --git a/shared/constants/registration-statuses.js b/shared/constants/registration-statuses.js index f3666d135..d462c2e3b 100644 --- a/shared/constants/registration-statuses.js +++ b/shared/constants/registration-statuses.js @@ -47,6 +47,14 @@ const RegistrationStatuses = { allowAssign: false, allowEdit: false }, + cancelled: { + id: 'cancelled', + label: 'Cancelled', + description: 'Has cancelled their participation', + color: '#ff7c0c', + allowAssign: false, + allowEdit: false + }, checkedIn: { id: 'checkedIn', label: 'Checked In', diff --git a/shared/package-lock.json b/shared/package-lock.json index 87f733287..eb0651471 100644 --- a/shared/package-lock.json +++ b/shared/package-lock.json @@ -1,5 +1,5 @@ { "name": "@hackjunction/shared", - "version": "1.1.44", + "version": "1.1.45", "lockfileVersion": 1 } diff --git a/shared/package.json b/shared/package.json index 7297a5687..0dbb3218c 100644 --- a/shared/package.json +++ b/shared/package.json @@ -1,6 +1,6 @@ { "name": "@hackjunction/shared", - "version": "1.1.44", + "version": "1.1.45", "description": "", "main": "index.js", "scripts": {