diff --git a/public/src/client/career.js b/public/src/client/career.js new file mode 100644 index 000000000..bc3db7f98 --- /dev/null +++ b/public/src/client/career.js @@ -0,0 +1,112 @@ +'use strict'; + +define('forum/career', [ + 'translator', 'jquery-form', +], function (translator) { + const Career = {}; + let validationError = false; + + Career.init = function () { + const student_id = $('#student_id'); + const age = $('#age'); + const gpa = $('#gpa'); + const num_programming_languages = $('#num_programming_languages'); + const num_past_internships = $('#num_past_internships'); + const signup = $('#signup'); + + function validateForm(callback) { + validationError = false; + validateNonEmpty(student_id.val(), $('#student-id-notify')); + validateRangedInt(age.val(), $('#age-notify'), 18, 25); + validateRangedFloat(gpa.val(), $('#gpa-notify'), 0.0, 4.0); + validateRangedInt(num_programming_languages.val(), $('#num-programming-languages-notify'), 1, 5); + validateRangedInt(num_past_internships.val(), $('#num-past-internships-notify'), 0, 4); + callback(); + } + + signup.on('click', function (e) { + const registerBtn = $(this); + const errorEl = $('#career-error-notify'); + errorEl.addClass('hidden'); + e.preventDefault(); + validateForm(function () { + if (validationError) { + return; + } + + registerBtn.addClass('disabled'); + + registerBtn.parents('form').ajaxSubmit({ + headers: { + 'x-csrf-token': config.csrf_token, + }, + success: function () { + location.reload(); + }, + error: function (data) { + translator.translate(data.responseText, config.defaultLang, function (translated) { + if (data.status === 403 && data.responseText === 'Forbidden') { + window.location.href = config.relative_path + '/career?error=csrf-invalid'; + } else { + errorEl.find('p').text(translated); + errorEl.removeClass('hidden'); + registerBtn.removeClass('disabled'); + } + }); + }, + }); + }); + }); + }; + + function validateNonEmpty(value, value_notify) { + if (!value || value.length === 0) { + showError(value_notify, 'Must be non-empty'); + } else { + showSuccess(value_notify); + } + } + + function validateRangedInt(value, value_notify, min_val, max_val) { + if (!value || isNaN(value)) { + showError(value_notify, `Must be a valid integer`); + } else if (parseInt(value, 10) < min_val || parseInt(value, 10) > max_val) { + showError(value_notify, `Must be within the range [${min_val}, ${max_val}]`); + } else { + showSuccess(value_notify); + } + } + + function validateRangedFloat(value, value_notify, min_val, max_val) { + if (!value || isNaN(value)) { + showError(value_notify, `Must be a valid floating point value`); + } else if (parseFloat(value) < min_val || parseFloat(value) > max_val) { + showError(value_notify, `Must be within the range [${min_val}, ${max_val}]`); + } else { + showSuccess(value_notify); + } + } + + function showError(element, msg) { + translator.translate(msg, function (msg) { + element.html(msg); + element.parent() + .removeClass('register-success') + .addClass('register-danger'); + element.show(); + }); + validationError = true; + } + + function showSuccess(element, msg) { + translator.translate(msg, function (msg) { + element.html(msg); + element.parent() + .removeClass('register-danger') + .addClass('register-success'); + element.show(); + }); + } + + return Career; +}); \ No newline at end of file diff --git a/public/src/modules/helpers.common.js b/public/src/modules/helpers.common.js index b85fb5f66..17c11fdb7 100644 --- a/public/src/modules/helpers.common.js +++ b/public/src/modules/helpers.common.js @@ -24,6 +24,8 @@ module.exports = function (utils, Benchpress, relative_path) { userAgentIcons, buildAvatar, register, + getPredictionColor, + formatPrediction, __escape: identity, }; @@ -336,6 +338,15 @@ module.exports = function (utils, Benchpress, relative_path) { styles.push('background-color: ' + userObj['icon:bgColor'] + ';'); return '' + userObj['icon:text'] + ''; } + function getPredictionColor(prediction) { + if (prediction === 1) { return `"background-color: rgb(0, 255, 0);"`; } + return `"background-color: rgb(255, 0, 0);"`; + } + + function formatPrediction(prediction) { + return prediction; + } + function register() { Object.keys(helpers).forEach(function (helperName) { diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index ecd8e6a73..82dab20f1 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -105,7 +105,7 @@ authenticationController.register = async function (req, res) { } if (!userData['account-type'] || - (userData['account-type'] !== 'student' && userData['account-type'] !== 'instructor')) { + (userData['account-type'] !== 'student' && userData['account-type'] !== 'instructor' && userData['account-type'] !== 'recruiter')) { throw new Error('Invalid account type'); } diff --git a/src/controllers/career.js b/src/controllers/career.js index beb8b6e83..ac9d30c1b 100644 --- a/src/controllers/career.js +++ b/src/controllers/career.js @@ -1,8 +1,28 @@ 'use strict'; +const user = require('../user'); +const helpers = require('./helpers'); + const careerController = module.exports; careerController.get = async function (req, res) { - const careerData = {}; + const userData = await user.getUserFields(req.uid, ['accounttype']); + + const accountType = userData.accounttype; + let careerData = {}; + + if (accountType === 'recruiter') { + careerData.allData = await user.getAllCareerData(); + } else { + const userCareerData = await user.getCareerData(req.uid); + if (userCareerData) { + careerData = userCareerData; + } else { + careerData.newAccount = true; + } + } + + careerData.accountType = accountType; + careerData.breadcrumbs = helpers.buildBreadcrumbs([{ text: 'Career', url: '/career' }]); res.render('career', careerData); -}; +}; \ No newline at end of file diff --git a/src/controllers/index.js b/src/controllers/index.js index b2f816cd9..796b294a8 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -188,6 +188,7 @@ Controllers.register = async function (req, res, next) { `, }, diff --git a/src/controllers/write/career.js b/src/controllers/write/career.js new file mode 100644 index 000000000..d92460d2a --- /dev/null +++ b/src/controllers/write/career.js @@ -0,0 +1,32 @@ +'use strict'; + +const helpers = require('../helpers'); +const user = require('../../user'); +const db = require('../../database'); + +const Career = module.exports; + +Career.register = async (req, res) => { + const userData = req.body; + try { + const userCareerData = { + student_id: userData.student_id, + major: userData.major, + age: userData.age, + gender: userData.gender, + gpa: userData.gpa, + extra_curricular: userData.extra_curricular, + num_programming_languages: userData.num_programming_languages, + num_past_internships: userData.num_past_internships, + }; + + userCareerData.prediction = Math.round(Math.random()); // TODO: Change this line to do call and retrieve actual candidate success prediction from the model instead of using a random number + + await user.setCareerData(req.uid, userCareerData); + db.sortedSetAdd('users:career', req.uid, req.uid); + res.json({}); + } catch (err) { + console.log(err); + helpers.noScriptErrors(req, res, err.message, 400); + } +}; \ No newline at end of file diff --git a/src/user/career.js b/src/user/career.js new file mode 100644 index 000000000..0e84965d4 --- /dev/null +++ b/src/user/career.js @@ -0,0 +1,25 @@ +'use strict'; + +const db = require('../database'); +const plugins = require('../plugins'); + +module.exports = function (User) { + User.getCareerData = async function (uid) { + uid = isNaN(uid) ? 0 : parseInt(uid, 10); + const careerData = await db.getObject(`user:${uid}:career`); + return careerData; + }; + + User.getAllCareerData = async function () { + const uids = await db.getSortedSetRange('users:career', 0, -1); + const allData = await db.getObjects(uids.map(uid => `user:${uid}:career`)); + return allData; + }; + + User.setCareerData = async function (uid, data) { + await db.setObject(`user:${uid}:career`, data); + for (const [field, value] of Object.entries(data)) { + plugins.hooks.fire('action:user.set', { uid, field, value, type: 'set' }); + } + }; +}; \ No newline at end of file diff --git a/src/user/index.js b/src/user/index.js index 3d1594af2..95f9a0d76 100644 --- a/src/user/index.js +++ b/src/user/index.js @@ -40,6 +40,7 @@ require('./info')(User); require('./online')(User); require('./blocks')(User); require('./uploads')(User); +require('./career')(User); User.exists = async function (uids) { return await ( diff --git a/themes/nodebb-theme-persona/less/career.less b/themes/nodebb-theme-persona/less/career.less index 250ae5cb0..33eefaec7 100644 --- a/themes/nodebb-theme-persona/less/career.less +++ b/themes/nodebb-theme-persona/less/career.less @@ -6,3 +6,87 @@ font-weight: normal; line-height: 1.42857143; } + +.career-block { + width: 85%; + margin-top: 20px; + + .help-block { + font-size: 13px; + } + + .card { + position: relative; + display: inline-block; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid rgba(0,0,0,.125); + border-radius: 0.25rem; + margin: 15px; + } + + .card-body { + padding: 15px; + } + + .card-title { + font-weight: bold; + font-size: 15px; + } + + .card-text { + margin-top: 5px; + } + + .prediction { + -webkit-transform: translateX(1.2em); + transform: translateX(1.2em); + color: #fff; + text-align: center; + padding: 10px; + border-radius: 10px; + position: absolute; + z-index: 100; + right: -5px; + bottom: -10px; + white-space: nowrap; + } + + .register-success { + .form-control { + border-color: #5cb85c; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%235cb85c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E"); + padding-right: 2.25rem; + background-repeat: no-repeat; + background-position: center right .625rem; + -webkit-background-size: 1.25rem 1.25rem; + background-size: 1.25rem 1.25rem; + } + + .register-feedback { + color: #5cb85c; + display: block; + margin-top: .25rem; + } + } + + .register-danger { + .form-control { + border-color: #d9534f; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23d9534f' viewBox='-2 -2 7 7'%3E%3Cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3E%3Ccircle r='.5'/%3E%3Ccircle cx='3' r='.5'/%3E%3Ccircle cy='3' r='.5'/%3E%3Ccircle cx='3' cy='3' r='.5'/%3E%3C/svg%3E"); + padding-right: 2.25rem; + background-repeat: no-repeat; + background-position: center right .625rem; + -webkit-background-size: 1.25rem 1.25rem; + background-size: 1.25rem 1.25rem; + } + + .register-feedback { + color: #d9534f; + display: block; + margin-top: .25rem; + } + } +} \ No newline at end of file diff --git a/themes/nodebb-theme-persona/templates/career.tpl b/themes/nodebb-theme-persona/templates/career.tpl index 459c159e1..b2863c1f7 100644 --- a/themes/nodebb-theme-persona/templates/career.tpl +++ b/themes/nodebb-theme-persona/templates/career.tpl @@ -11,7 +11,16 @@ Welcome to the careers page! This is a brand new feature added to allow students to connect with various job recruiters.

- This page is still under development. + + This page is only available for students and recruiters. + + + + + + + +
{{{each widgets.sidebar}}} @@ -23,4 +32,4 @@ {{{each widgets.footer}}} {{widgets.footer.html}} {{{end}}} -
+ \ No newline at end of file diff --git a/themes/nodebb-theme-persona/templates/partials/career/recruiter.tpl b/themes/nodebb-theme-persona/templates/partials/career/recruiter.tpl new file mode 100644 index 000000000..918c5a071 --- /dev/null +++ b/themes/nodebb-theme-persona/templates/partials/career/recruiter.tpl @@ -0,0 +1,29 @@ +
+ Below is a list of all students who have signed up for this feature. Our new + ML-based system also provides a recommendation based on intial student application + details. + + Note: The ML system is currently under testing and should only be taken as a + recommendation, not an official decision. +
+
+ {{{each allData}}} +
+
+
+ {../student_id} +
+
+ Major: {../major}
+ GPA: {../gpa}
+ Extracurricular: {../extra_curricular}
+ # Prog Languages: {../num_programming_languages}
+ # Past Internships: {../num_past_internships}
+
+
+ {function.formatPrediction, ../prediction} +
+
+
+ {{{end}}} +
\ No newline at end of file diff --git a/themes/nodebb-theme-persona/templates/partials/career/student.tpl b/themes/nodebb-theme-persona/templates/partials/career/student.tpl new file mode 100644 index 000000000..5f05f344f --- /dev/null +++ b/themes/nodebb-theme-persona/templates/partials/career/student.tpl @@ -0,0 +1,110 @@ +
+ + Ready to get started? Sign up below and find your next job opportunity! + + You have been successfully registered! You can update your personal information below at any time. + +
+
+
+ Registration Error +

{error}

+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ +
+
+ +
+
+ + + +
+
\ No newline at end of file