diff --git a/backend/.env.example b/backend/.env.example index 2e731d12a..7501d7b99 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -12,10 +12,14 @@ SENDGRID_API_KEY= SENDGRID_FROM_EMAIL= SENDGRID_FROM_NAME= SENDGRID_GENERIC_TEMPLATE= - # - # Note! We are using the dev.app.hackjunction.com - # database here instead of the local one - # + # + + # Note! We are using the dev.app.hackjunction.com + + # database here instead of the local one + + # + MONGODB_URI= FRONTEND_URL= DEVTOOLS_ENABLED= diff --git a/backend/modules/recruitment/controller.js b/backend/modules/recruitment/controller.js index fc72cd887..ba1d74ada 100644 --- a/backend/modules/recruitment/controller.js +++ b/backend/modules/recruitment/controller.js @@ -112,6 +112,7 @@ controller.createRecruitmentProfile = async ( github: userProfile.github, linkedin: userProfile.linkedin, portfolio: userProfile.portfolio, + curriculumVitae: userProfile.curriculumVitae, }, recruitmentOptions: userProfile.recruitmentOptions, registrations: userProfile.registrations, diff --git a/backend/modules/user-profile/controller.js b/backend/modules/user-profile/controller.js index f9ef5881a..8fa345127 100644 --- a/backend/modules/user-profile/controller.js +++ b/backend/modules/user-profile/controller.js @@ -1,44 +1,46 @@ -const _ = require('lodash'); -const { UserProfile } = require('./model'); -const { NotFoundError } = require('../../common/errors/errors'); -const UserProfileHelpers = require('./helpers'); +const _ = require('lodash') +const { UserProfile } = require('./model') +const { NotFoundError } = require('../../common/errors/errors') +const UserProfileHelpers = require('./helpers') -const controller = {}; +const controller = {} controller.getUserProfile = userId => { return UserProfile.findOne({ - userId + userId, }).then(userProfile => { if (!userProfile) { - throw new NotFoundError(`UserProfile with id ${userId} does not exist`); + throw new NotFoundError( + `UserProfile with id ${userId} does not exist` + ) } - return userProfile; - }); -}; + return userProfile + }) +} controller.getUserProfiles = userIds => { return UserProfile.find({ userId: { - $in: userIds - } - }); -}; + $in: userIds, + }, + }) +} controller.queryProfiles = async query => { const found = await UserProfile.find(query.query) .sort('updatedAt') .skip(query.pagination.skip) - .limit(query.pagination.limit); + .limit(query.pagination.limit) - const count = await UserProfile.find(query.query).countDocuments(); - return { found, count }; -}; + const count = await UserProfile.find(query.query).countDocuments() + return { found, count } +} controller.getUserProfilesPublic = userIds => { return controller.getUserProfiles(userIds).then(profiles => { - return UserProfile.publicFields(profiles); - }); -}; + return UserProfile.publicFields(profiles) + }) +} controller.createUserProfile = (data, userId) => { const userProfile = new UserProfile({ @@ -46,61 +48,64 @@ controller.createUserProfile = (data, userId) => { firstName: data.firstName, lastName: data.lastName, email: data.email, - avatar: data.avatar - }); + avatar: data.avatar, + }) - return userProfile.save(); -}; + return userProfile.save() +} controller.updateUserProfile = async (data, userId) => { - const validatedData = await UserProfileHelpers.validate(data); + const validatedData = await UserProfileHelpers.validate(data) return controller.getUserProfile(userId).then(userProfile => { - return UserProfile.updateAllowed(userProfile, validatedData); - }); -}; + return UserProfile.updateAllowed(userProfile, validatedData) + }) +} controller.syncRegistration = async registration => { const data = { registration: registration._id, event: registration.event, - status: registration.status - }; + status: registration.status, + } return controller.getUserProfile(registration.user).then(profile => { if (profile.registrations.length === 0) { - profile.registrations = [data]; + profile.registrations = [data] } else { profile.registrations = profile.toJSON().registrations.map(r => { if (r.event.toString() === data.event.toString()) { - return data; + return data } - return r; - }); + return r + }) } - return profile.save(); - }); -}; + return profile.save() + }) +} controller.getUsersByEmail = email => { - return UserProfile.find({ email }); -}; + return UserProfile.find({ email }) +} controller.searchUsers = terms => { - return UserProfile.find({ $text: { $search: terms } }).limit(25); -}; + return UserProfile.find({ $text: { $search: terms } }).limit(25) +} controller.getRecruiters = () => { return UserProfile.find({ - $nor: [{ recruiterEvents: { $exists: false } }, { recruiterEvents: { $size: 0 } }] - }); -}; + $nor: [ + { recruiterEvents: { $exists: false } }, + { recruiterEvents: { $size: 0 } }, + ], + }) +} controller.updateRecruiter = (userId, events, organisation) => { return UserProfile.findOne({ userId }).then(user => { - user.recruiterEvents = events; - user.recruiterOrganisation = organisation; - return user.save(); - }); -}; + user.recruiterEvents = events + user.recruiterOrganisation = organisation + return user.save() + }) +} -module.exports = controller; +module.exports = controller diff --git a/backend/modules/user-profile/helpers.js b/backend/modules/user-profile/helpers.js index 10443957e..6dd5ef83d 100644 --- a/backend/modules/user-profile/helpers.js +++ b/backend/modules/user-profile/helpers.js @@ -1,24 +1,24 @@ -const { RegistrationFields } = require('@hackjunction/shared'); -const yup = require('yup'); +const { RegistrationFields } = require('@hackjunction/shared') +const yup = require('yup') const UserProfileHelpers = { validate: data => { - const validations = {}; + const validations = {} Object.keys(data).forEach(field => { - const fieldConfig = RegistrationFields.getField(field); + const fieldConfig = RegistrationFields.getField(field) if (fieldConfig) { - validations[field] = fieldConfig.validationSchema(false); + validations[field] = fieldConfig.validationSchema(false) } - }); + }) - validations['avatar'] = yup + validations.avatar = yup .string() .url() - .nullable(); + .nullable() - const schema = yup.object().shape(validations); - return schema.validate(data, { stripUnknown: true }); - } -}; + const schema = yup.object().shape(validations) + return schema.validate(data, { stripUnknown: true }) + }, +} -module.exports = UserProfileHelpers; +module.exports = UserProfileHelpers diff --git a/backend/modules/user-profile/model.js b/backend/modules/user-profile/model.js index 5886d0957..9b17556ee 100644 --- a/backend/modules/user-profile/model.js +++ b/backend/modules/user-profile/model.js @@ -1,49 +1,50 @@ -const mongoose = require('mongoose'); -const _ = require('lodash'); -const updateAllowedPlugin = require('../../common/plugins/updateAllowed'); -const publicFieldsPlugin = require('../../common/plugins/publicFields'); -const Shared = require('@hackjunction/shared'); -const AuthController = require('../auth/controller'); -const { RegistrationFields } = Shared; +const mongoose = require('mongoose') +const _ = require('lodash') +const Shared = require('@hackjunction/shared') +const updateAllowedPlugin = require('../../common/plugins/updateAllowed') +const publicFieldsPlugin = require('../../common/plugins/publicFields') +const AuthController = require('../auth/controller') + +const { RegistrationFields } = Shared const UserProfileSchema = new mongoose.Schema({ userId: { type: String, required: true, - unique: true + unique: true, }, avatar: { - type: String + type: String, }, registrations: { type: Array, required: false, - default: [] + default: [], }, recruiterEvents: { type: Array, required: false, default: [], - set: function(recruiterEvents) { - this._previousRecruiterEvents = this.recruiterEvents; - return recruiterEvents; - } + set(recruiterEvents) { + this._previousRecruiterEvents = this.recruiterEvents + return recruiterEvents + }, }, recruiterOrganisation: { type: String, - required: false - } -}); + required: false, + }, +}) /** Build user profile fields based on possible registration questions */ -const fields = {}; +const fields = {} _.forOwn(RegistrationFields.getFields(), (value, fieldName) => { if (value.hasOwnProperty('userProfileConfig')) { - fields[fieldName] = value.userProfileConfig; + fields[fieldName] = value.userProfileConfig } -}); +}) -UserProfileSchema.add(fields); +UserProfileSchema.add(fields) /* // Virtual field to fetch registrations if required UserProfileSchema.virtual('registrations', { @@ -53,43 +54,52 @@ UserProfileSchema.virtual('registrations', { }); */ UserProfileSchema.post('save', function(doc, next) { - if (_.xor(this._previousRecruiterEvents, this.recruiterEvents).length !== 0) { + if ( + _.xor(this._previousRecruiterEvents, this.recruiterEvents).length !== 0 + ) { if (this.recruiterEvents.length === 0) { - AuthController.revokeRecruiterPermission(this.userId); + AuthController.revokeRecruiterPermission(this.userId) } else { - AuthController.grantRecruiterPermission(this.userId); + AuthController.grantRecruiterPermission(this.userId) } AuthController.updateMetadata(this.userId, { recruiterEvents: this.recruiterEvents, - recruiterOrganisation: this.recruiterOrganisation - }); + recruiterOrganisation: this.recruiterOrganisation, + }) } - next(); -}); + next() +}) -UserProfileSchema.set('timestamps', true); +UserProfileSchema.set('timestamps', true) UserProfileSchema.index({ - userId: 1 -}); + userId: 1, +}) UserProfileSchema.index({ firstName: 'text', lastName: 'text', - email: 'text' -}); + email: 'text', +}) UserProfileSchema.plugin(updateAllowedPlugin, { - blacklisted: ['__v', '_id', 'createdAt', 'updatedAt', 'userId'] -}); + blacklisted: ['__v', '_id', 'createdAt', 'updatedAt', 'userId'], +}) UserProfileSchema.plugin(publicFieldsPlugin, { - fields: ['userId', 'avatar', 'firstName', 'lastName', 'email', 'phoneNumber'] -}); + fields: [ + 'userId', + 'avatar', + 'firstName', + 'lastName', + 'email', + 'phoneNumber', + ], +}) -const UserProfile = mongoose.model('UserProfile', UserProfileSchema); +const UserProfile = mongoose.model('UserProfile', UserProfileSchema) module.exports = { UserProfile, - UserProfileSchema -}; + UserProfileSchema, +} diff --git a/backend/modules/user-profile/routes.js b/backend/modules/user-profile/routes.js index a809516e9..578a2ea9e 100644 --- a/backend/modules/user-profile/routes.js +++ b/backend/modules/user-profile/routes.js @@ -1,71 +1,92 @@ -const express = require('express'); -const router = express.Router(); -const asyncHandler = require('express-async-handler'); -const UserProfileController = require('./controller'); -const TeamController = require('../team/controller'); -const { Auth } = require('@hackjunction/shared'); +const express = require('express') -const { hasToken } = require('../../common/middleware/token'); -const { hasPermission } = require('../../common/middleware/permissions'); +const router = express.Router() +const asyncHandler = require('express-async-handler') +const { Auth } = require('@hackjunction/shared') +const UserProfileController = require('./controller') +const TeamController = require('../team/controller') + +const { hasToken } = require('../../common/middleware/token') +const { hasPermission } = require('../../common/middleware/permissions') const getUserProfile = asyncHandler(async (req, res) => { - const userProfile = await UserProfileController.getUserProfile(req.user.sub); - return res.status(200).json(userProfile); -}); + const userProfile = await UserProfileController.getUserProfile(req.user.sub) + return res.status(200).json(userProfile) +}) const getUserProfilesPublic = asyncHandler(async (req, res) => { - const userProfiles = await UserProfileController.getUserProfilesPublic(req.query.userIds); - return res.status(200).json(userProfiles); -}); + const userProfiles = await UserProfileController.getUserProfilesPublic( + req.query.userIds + ) + return res.status(200).json(userProfiles) +}) const getUserProfilesByTeamPublic = asyncHandler(async (req, res) => { - const teamMembers = await TeamController.getTeamMembers(req.params.teamId); - const userProfiles = await UserProfileController.getUserProfilesPublic(teamMembers); - return res.status(200).json(userProfiles); -}); + const teamMembers = await TeamController.getTeamMembers(req.params.teamId) + const userProfiles = await UserProfileController.getUserProfilesPublic( + teamMembers + ) + return res.status(200).json(userProfiles) +}) const createUserProfile = asyncHandler(async (req, res) => { - const userProfile = await UserProfileController.createUserProfile(req.body, req.user.sub); - return res.status(201).json(userProfile); -}); + const userProfile = await UserProfileController.createUserProfile( + req.body, + req.user.sub + ) + return res.status(201).json(userProfile) +}) const updateUserProfile = asyncHandler(async (req, res) => { - const updatedUserProfile = await UserProfileController.updateUserProfile(req.body, req.user.sub); - return res.status(200).json(updatedUserProfile); -}); + const updatedUserProfile = await UserProfileController.updateUserProfile( + req.body, + req.user.sub + ) + return res.status(200).json(updatedUserProfile) +}) const searchUsers = asyncHandler(async (req, res) => { - const users = await UserProfileController.searchUsers(req.params.terms); - return res.status(200).json(users); -}); + const users = await UserProfileController.searchUsers(req.params.terms) + return res.status(200).json(users) +}) const getRecruiters = asyncHandler(async (req, res) => { - const users = await UserProfileController.getRecruiters(); - return res.status(200).json(users); -}); + const users = await UserProfileController.getRecruiters() + return res.status(200).json(users) +}) const updateRecruiter = asyncHandler(async (req, res) => { const user = await UserProfileController.updateRecruiter( req.body.recruiterId, req.body.events, req.body.organisation - ); - return res.status(200).json(user); -}); + ) + return res.status(200).json(user) +}) router .route('/') .get(hasToken, getUserProfile) .post(hasToken, createUserProfile) - .patch(hasToken, updateUserProfile); + .patch(hasToken, updateUserProfile) -router.route('/public').get(getUserProfilesPublic); -router.route('/public/team/:teamId').get(getUserProfilesByTeamPublic); +router.route('/public').get(getUserProfilesPublic) +router.route('/public/team/:teamId').get(getUserProfilesByTeamPublic) -router.get('/search/:terms', hasToken, searchUsers); +router.get('/search/:terms', hasToken, searchUsers) router - .get('/recruiters', hasToken, hasPermission(Auth.Permissions.MANAGE_RECRUITMENT), getRecruiters) - .patch('/recruiters', hasToken, hasPermission(Auth.Permissions.MANAGE_RECRUITMENT), updateRecruiter); + .get( + '/recruiters', + hasToken, + hasPermission(Auth.Permissions.MANAGE_RECRUITMENT), + getRecruiters + ) + .patch( + '/recruiters', + hasToken, + hasPermission(Auth.Permissions.MANAGE_RECRUITMENT), + updateRecruiter + ) -module.exports = router; +module.exports = router diff --git a/frontend/.env.example b/frontend/.env.example index 69a9a6588..d509f474b 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -4,5 +4,6 @@ REACT_APP_AUTH0_DOMAIN= REACT_APP_AUTH0_CLIENT_ID= REACT_APP_BASE_URL= REACT_APP_IS_DEBUG= - # Not necessary in development + + # Not necessary in development # REACT_APP_FACEBOOK_PIXEL_ID=your-pixel-id \ No newline at end of file diff --git a/frontend/src/components/generic/DescriptionItem/index.js b/frontend/src/components/generic/DescriptionItem/index.js index 4f65391ee..6784955e8 100644 --- a/frontend/src/components/generic/DescriptionItem/index.js +++ b/frontend/src/components/generic/DescriptionItem/index.js @@ -130,6 +130,7 @@ const DescriptionItem = ({ title, content, fieldName }) => { case 'portfolio': case 'github': case 'linkedin': + case 'curriculumVitae': return ( {content} diff --git a/frontend/src/pages/_account/profile/index.js b/frontend/src/pages/_account/profile/index.js index fc9d2dd16..17c196dfb 100644 --- a/frontend/src/pages/_account/profile/index.js +++ b/frontend/src/pages/_account/profile/index.js @@ -597,6 +597,153 @@ export default () => { )} /> + + + Additional Links + + + You can link additional links related to you in + here. + + + + ( + + form.setFieldValue( + field.name, + value + ) + } + onBlur={() => + form.setFieldTouched( + field.name + ) + } + /> + )} + /> + + { + RegistrationFields.getFields()[ + 'curriculumVitae' + ].hint + } + + + + ( + + form.setFieldValue( + field.name, + value + ) + } + onBlur={() => + form.setFieldTouched( + field.name + ) + } + /> + )} + /> + + { + RegistrationFields.getFields()[ + 'portfolio' + ].hint + } + + + + ( + + form.setFieldValue( + field.name, + value + ) + } + onBlur={() => + form.setFieldTouched( + field.name + ) + } + /> + )} + /> + + { + RegistrationFields.getFields()[ + 'github' + ].hint + } + + + + ( + + form.setFieldValue( + field.name, + value + ) + } + onBlur={() => + form.setFieldTouched( + field.name + ) + } + /> + )} + /> + + { + RegistrationFields.getFields()[ + 'linkedin' + ].hint + } + + + + userProfile.curriculumVitae || undefined, + validationSchema: required => { + const base = yup + .string() + .url() + .label(FieldProps.curriculumVitae.label); + + return required ? base.required() : base; + } + }, github: { ...FieldProps.github, category: Categories.links,