diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..38f518f571 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +public/js/lib/codemirror/* linguist-vendored +*.jsx linguist-language=JavaScript diff --git a/common/models/User-Identity.js b/common/models/User-Identity.js index 3df7f01400..a69685cc29 100644 --- a/common/models/User-Identity.js +++ b/common/models/User-Identity.js @@ -1,3 +1,4 @@ +import assign from 'object.assign'; import debugFactory from 'debug'; const debug = debugFactory('freecc:models:userIdent'); @@ -10,6 +11,39 @@ function getFirstImageFromProfile(profile) { null; } +// using es6 argument destructing +function setProfileFromGithub( + user, + { + profileUrl: githubURL, + username + }, + { + location, + email: githubEmail, + id: githubId, + 'created_at': joinedGithubOn, + blog: website, + name + } +) { + return assign( + user, + { isGithubCool: true, isMigrationGrandfathered: false }, + { + name, + username: username.toLowerCase(), + location, + joinedGithubOn, + website, + githubId, + githubURL, + githubEmail, + githubProfile: githubURL + } + ); +} + export default function(UserIdent) { UserIdent.observe('before save', function(ctx, next) { var userIdent = ctx.currentInstance || ctx.instance; @@ -25,7 +59,8 @@ export default function(UserIdent) { return next(); } - const picture = getFirstImageFromProfile(userIdent.profile); + const { profile } = userIdent; + const picture = getFirstImageFromProfile(profile); debug('picture', picture, user.picture); // check if picture was found @@ -41,19 +76,10 @@ export default function(UserIdent) { userChanged = true; } - // if user signed in with github - // and user is not github cool - // or username is different from github username - // then make them github cool - // and set their username from their github profile. - if ( - userIdent.provider === 'github-login' && - (!user.isGithubCool || - user.username !== userIdent.provider.username.toLowerCase()) - ) { + // if user signed in with github refresh their info + if (userIdent.provider === 'github-login') { debug("user isn't github cool or username from github is different"); - user.isGithubCool = true; - user.username = userIdent.profile.username.toLowerCase(); + setProfileFromGithub(user, profile, profile._json); userChanged = true; } diff --git a/common/models/challenge.json b/common/models/challenge.json index 3258d641ee..baa78a4e4c 100644 --- a/common/models/challenge.json +++ b/common/models/challenge.json @@ -6,7 +6,12 @@ "properties": { "name": { "type": "string", - "unique": true + "index": { + "mongodb": { + "unique": true, + "background": true + } + } }, "title": { "type": "string" diff --git a/common/models/nonprofit.json b/common/models/nonprofit.json index 738c43e9ad..5d8dddbc3d 100644 --- a/common/models/nonprofit.json +++ b/common/models/nonprofit.json @@ -6,7 +6,12 @@ "properties": { "name": { "type": "string", - "unique": true + "index": { + "mongodb": { + "unique": true, + "background": true + } + } }, "whatDoesNonprofitDo": { "type": "string" diff --git a/common/models/story.json b/common/models/story.json index 8aab09964b..6f252cefb3 100644 --- a/common/models/story.json +++ b/common/models/story.json @@ -6,28 +6,29 @@ "properties": { "name": { "type": "string", - "unique": true + "index": { + "mongodb": { + "unique": true, + "background": true + } + } }, "headline": { - "type": "string", - "unique": false + "type": "string" }, "timePosted": { "type": "number", "default": 0 }, "link": { - "type": "string", - "unique": false + "type": "string" }, "metaDescription": { "type": "string", - "default": "", - "unique": false + "default": "" }, "description": { - "type": "string", - "unique": false + "type": "string" }, "originalStoryAuthorEmail": { "type": "string", diff --git a/common/models/user.js b/common/models/user.js index 3c7e54cbb3..81078d6df0 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -1,4 +1,5 @@ import { Observable } from 'rx'; +import uuid from 'node-uuid'; import moment from 'moment'; import debugFactory from 'debug'; @@ -44,6 +45,16 @@ module.exports = function(User) { // username should be unique User.validatesUniquenessOf('username'); + User.observe('before save', function({ instance: user }, next) { + if (user) { + user.username = user.username.trim().toLowerCase(); + user.email = typeof user.email === 'string' ? + user.email.trim().toLowerCase() : + user.email; + } + next(); + }); + debug('setting up user hooks'); User.afterRemote('confirm', function(ctx) { ctx.req.flash('success', { @@ -54,6 +65,11 @@ module.exports = function(User) { ctx.res.redirect('/email-signin'); }); + User.beforeRemote('create', function({ req }, notUsed, next) { + req.body.username = 'fcc' + uuid.v4().slice(0, 8); + next(); + }); + User.afterRemote('login', function(ctx, user, next) { var res = ctx.res; var req = ctx.req; diff --git a/common/models/user.json b/common/models/user.json index d055e431ff..871cd2db46 100644 --- a/common/models/user.json +++ b/common/models/user.json @@ -10,11 +10,10 @@ "index": { "mongodb": { "unique": true, + "background": true, "sparse": true } - }, - "lowercase": true, - "trim": true + } }, "password": { "type": "string" @@ -27,11 +26,31 @@ "type": "boolean", "default": false }, + "githubId": { + "type": "string" + }, + "githubURL": { + "type": "string" + }, + "githubEmail": { + "type": "string" + }, + "joinedGithubOn": { + "type": "date" + }, + "isMigrationGrandfathered": { + "type": "boolean", + "default": false + }, "username": { "type": "string", - "lowercase": true, - "trim": true, - "require": true + "require": true, + "index": { + "mongodb": { + "unique": true, + "background": true + } + } }, "bio": { "type": "string", diff --git a/seed/challenges/basic-javascript.json b/seed/challenges/basic-javascript.json index 9d4e5b0b12..52aab20a55 100644 --- a/seed/challenges/basic-javascript.json +++ b/seed/challenges/basic-javascript.json @@ -423,7 +423,7 @@ "description": [ "", "In JavaScript we can store lists or collections of data in what are called arrays", - "Arrays are distinguished by the [ and ] around the data. Each piece of data is separated be a , ", + "Arrays are distinguished by the [ and ] around the data. Each piece of data is separated by a , ", "Now let's create a new array called myArray with a string and a number with a , separating each one", "Refer to the example if you get stuck", "" @@ -503,7 +503,7 @@ "difficulty":"9.98171", "description":[ "", - "We are able to modify the data store in an array be using indexes", + "We are able to modify the data stored in an array by using indexes", "Example:", "", "var ourArray = [1,2,3];", @@ -536,10 +536,10 @@ "difficulty": "9.9818", "description": [ "", - "When and array has been defined we still have the ability to make changes to it afterwards", + "When an array has been defined we still have the ability to make changes to it afterwards", "One common way in which we can manipulate the data in an array is through .pop() ", - " .pop() is used to \"pop\" a value from the end of an array. We can retrieve this value by preforming the pop in a variable declaration.", - "any type of variable can be \"popped\" from and array", + " .pop() is used to \"pop\" a value from the end of an array. We can retrieve this value by performing the pop in a variable declaration.", + "Any type of variable can be \"popped\" from an array", "Let's try .pop() now" ], "tests": [ @@ -639,13 +639,13 @@ "description":[ "", "In JavaScript we can divide up our code into separate and reusable parts called functions", - "here's and example of a function", + "Here's an example of a function", "", "function functionName (a, b){", " return(a + b);", "}", "", - "our function can be called like this", + "Our function can be called like this", "functionName();", "Let's try creating and calling a function now called myFunction" ], @@ -677,7 +677,7 @@ "description":[ "", "A very important data type in javascript is the Object ", - " Objects a similar to arrays except that instead of using indexes to access and modify their data, Objects have what are called properties ", + " Objects are similar to arrays except that instead of using indexes to access and modify their data, Objects have what are called properties ", "Here's a sample Object", "", "var cat = {", @@ -688,7 +688,7 @@ "};", "", "Objects are useful for storing data in a structured way or in a way that represents a real world object like a cat.", - "Let's try to make a Object that represents a dog called myDog!" + "Let's try to make an Object that represents a dog called myDog!" ], "tests":[ diff --git a/seed/challenges/bootstrap.json b/seed/challenges/bootstrap.json index 5a89962f12..def874e207 100644 --- a/seed/challenges/bootstrap.json +++ b/seed/challenges/bootstrap.json @@ -1883,7 +1883,7 @@ "Above your right-well, inside its \"col-xs-6\" div element, add a h4 element with the text \"#right-well\"." ], "tests": [ - "assert($('.col-xs-12').children('h4') && $('.col-xs-12').children('h4').length > 1, 'Add an h4 element to each of your <div class=\\'col-xs-6\\'> elements.');", + "assert($('.col-xs-6').children('h4') && $('.col-xs-6').children('h4').length > 1, 'Add an h4 element to each of your <div class=\\'col-xs-6\\'> elements.');", "assert(new RegExp('#left-well','gi').test($('h4').text()), 'One h4 element should have the text \"#left-well\".');", "assert(new RegExp('#right-well','gi').test($('h4').text()), 'One h4 element should have the text \"#right-well\".');" ], @@ -2036,9 +2036,9 @@ "Add a comment at the top of your HTML that says You shouldn't need to modify code below this line." ], "tests": [ - "assert(editor.match(//g).length > 1, 'Be sure to close your comment with -->.')" + "assert(editor.match(//g) && editor.match(/-->/g).length > 0, 'Be sure to close your comment with -->.')" ], "challengeSeed": [ "
", diff --git a/seed/challenges/intermediate-bonfires.json b/seed/challenges/intermediate-bonfires.json index 5ae11fc7a4..f9c9be22d8 100644 --- a/seed/challenges/intermediate-bonfires.json +++ b/seed/challenges/intermediate-bonfires.json @@ -103,7 +103,7 @@ "difficulty": "2.02", "description": [ "Convert the given number into a roman numeral.", - "All roman numerals answers should be provided in upper-case.", + "All roman numerals answers should be provided in upper-case.", "Remember to use RSAP if you get stuck. Try to pair program. Write your own code." ], "challengeSeed": [ @@ -356,7 +356,7 @@ " return arr1;", "}", "", - "unite([1, 2, 3], [5, 2, 1, 4], [2, 1]);" + "unite([1, 3, 2], [5, 2, 1, 4], [2, 1]);" ], "tests": [ "assert.deepEqual(unite([1, 3, 2], [5, 2, 1, 4], [2, 1]), [1, 3, 2, 5, 4], 'should return the union of the given arrays');", @@ -675,7 +675,8 @@ "tests": [ "assert.deepEqual(steamroller([[['a']], [['b']]]), ['a', 'b'], 'should flatten nested arrays');", "assert.deepEqual(steamroller([1, [2], [3, [[4]]]]), [1, 2, 3, 4], 'should flatten nested arrays');", - "assert.deepEqual(steamroller([1, [], [3, [[4]]]]), [1, 3, 4], 'should work with empty arrays');" + "assert.deepEqual(steamroller([1, [], [3, [[4]]]]), [1, 3, 4], 'should work with empty arrays');", + "assert.deepEqual(steamroller([1, {}, [3, [[4]]]]), [1, {}, 3, 4], 'should work with actual objects');" ], "MDNlinks": [ "Array.isArray()" diff --git a/seed/challenges/jquery-ajax-and-json.json b/seed/challenges/jquery-ajax-and-json.json index ed9c2c64c9..dad707f590 100644 --- a/seed/challenges/jquery-ajax-and-json.json +++ b/seed/challenges/jquery-ajax-and-json.json @@ -257,9 +257,9 @@ "dashedName": "waypoint-target-the-same-element-with-multiple-jQuery-Selectors", "difficulty": 3.06, "description": [ - "Now you know three ways of targeting elements: by type ($('button')), by class (($('.btn')), and by id (($'#target1')).", + "Now you know three ways of targeting elements: by type $('button'), by class $('.btn')), and by id $('#target1')).", "Use each of these jQuery selectors to target your button element with the class \"btn\" and the id \"target1\".", - "Use the addClass() jQuery function to give the element one new class for each selector: \"animated\", \"shake\", and \"button-primary\"." + "Use the addClass() jQuery function to give the element one new class for each selector: \"animated\", \"shake\", and \"btn-primary\"." ], "tests": [ "assert(editor.match(/\\$\\(\\'button\\'\\)/g), 'Use the $\\(\\'button\\'\\) selector.')", diff --git a/seed/loopbackMigration.js b/seed/loopbackMigration.js index 333629438a..d167ccec8b 100644 --- a/seed/loopbackMigration.js +++ b/seed/loopbackMigration.js @@ -92,6 +92,8 @@ var users = dbObservable user.username = 'fcc' + uuid.v4().slice(0, 8); if (user.github) { user.isGithubCool = true; + } else { + user.isMigrationGrandfathered = true; } return user; }) diff --git a/server/boot/challenge.js b/server/boot/challenge.js index 5e0cc37f0d..4228a3ff15 100644 --- a/server/boot/challenge.js +++ b/server/boot/challenge.js @@ -1,35 +1,3 @@ -/** - * Created by nathanleniz on 5/15/15. - * Copyright (c) 2015, Free Code Camp - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - 3. Neither the name of the copyright holder nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS - BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR - BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE - OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - var R = require('ramda'), Rx = require('rx'), assign = require('object.assign'), diff --git a/server/boot/user.js b/server/boot/user.js index 7278e0a07d..b1d3c6d8d3 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -122,113 +122,123 @@ module.exports = function(app) { */ function returnUser(req, res, next) { + const username = req.params.username.toLowerCase(); + const { path } = req; User.findOne( - { where: { 'username': req.params.username.toLowerCase() } }, + { where: { username } }, function(err, user) { if (err) { return next(err); } - if (user) { - user.progressTimestamps = - user.progressTimestamps.sort(function(a, b) { - return a - b; - }); - - var timeObject = Object.create(null); - R.forEach(function(time) { - timeObject[moment(time).format('YYYY-MM-DD')] = time; - }, user.progressTimestamps); - - var tmpLongest = 1; - var timeKeys = R.keys(timeObject); - - user.longestStreak = 0; - for (var i = 1; i <= timeKeys.length; i++) { - if (moment(timeKeys[i - 1]).add(1, 'd').toString() - === moment(timeKeys[i]).toString()) { - tmpLongest++; - if (tmpLongest > user.longestStreak) { - user.longestStreak = tmpLongest; - } - } else { - tmpLongest = 1; - } - } - - timeKeys = timeKeys.reverse(); - tmpLongest = 1; - - user.currentStreak = 1; - var today = moment(Date.now()).format('YYYY-MM-DD'); - - const yesterday = moment(today).subtract(1, 'd').toString(); - const yesteryesterday = moment(today).subtract(2, 'd').toString(); - - if ( - moment(today).toString() === moment(timeKeys[0]).toString() || - yesterday === moment(timeKeys[0]).toString() || - yesteryesterday === moment(timeKeys[0]).toString() - ) { - for (var _i = 1; _i <= timeKeys.length; _i++) { - - if ( - moment(timeKeys[_i - 1]).subtract(1, 'd').toString() === - moment(timeKeys[_i]).toString() - ) { - - tmpLongest++; - - if (tmpLongest > user.currentStreak) { - user.currentStreak = tmpLongest; - } - } else { - break; - } - } - } else { - user.currentStreak = 1; - } - - var data = {}; - var progressTimestamps = user.progressTimestamps; - progressTimestamps.forEach(function(timeStamp) { - data[(timeStamp / 1000)] = 1; - }); - - user.currentStreak = user.currentStreak || 1; - user.longestStreak = user.longestStreak || 1; - var challenges = user.completedChallenges.filter(function( obj ) { - return obj.challengeType === 3 || obj.challengeType === 4; - }); - - res.render('account/show', { - title: 'Camper ' + user.username + '\'s portfolio', - username: user.username, - name: user.name, - location: user.location, - githubProfile: user.githubProfile, - linkedinProfile: user.linkedinProfile, - codepenProfile: user.codepenProfile, - facebookProfile: user.facebookProfile, - twitterHandle: user.twitterHandle, - bio: user.bio, - picture: user.picture, - progressTimestamps: user.progressTimestamps, - challenges: challenges, - calender: data, - moment: moment, - longestStreak: user.longestStreak + - (user.longestStreak === 1 ? ' day' : ' days'), - currentStreak: user.currentStreak + - (user.currentStreak === 1 ? ' day' : ' days') - }); - } else { + if (!user) { req.flash('errors', { - msg: "404: We couldn't find a page with that url. " + - 'Please double check the link.' + msg: `404: We couldn't find path ${ path }` }); return res.redirect('/'); } + if (!user.isGithubCool && !user.isMigrationGrandfathered) { + req.flash('errors', { + msg: ` + user ${ username } has not completed account signup + ` + }); + return res.redirect('/'); + } + user.progressTimestamps = + user.progressTimestamps.sort(function(a, b) { + return a - b; + }); + + var timeObject = Object.create(null); + R.forEach(function(time) { + timeObject[moment(time).format('YYYY-MM-DD')] = time; + }, user.progressTimestamps); + + var tmpLongest = 1; + var timeKeys = R.keys(timeObject); + + user.longestStreak = 0; + for (var i = 1; i <= timeKeys.length; i++) { + if (moment(timeKeys[i - 1]).add(1, 'd').toString() + === moment(timeKeys[i]).toString()) { + tmpLongest++; + if (tmpLongest > user.longestStreak) { + user.longestStreak = tmpLongest; + } + } else { + tmpLongest = 1; + } + } + + timeKeys = timeKeys.reverse(); + tmpLongest = 1; + + user.currentStreak = 1; + var today = moment(Date.now()).format('YYYY-MM-DD'); + + const yesterday = moment(today).subtract(1, 'd').toString(); + const yesteryesterday = moment(today).subtract(2, 'd').toString(); + + if ( + moment(today).toString() === moment(timeKeys[0]).toString() || + yesterday === moment(timeKeys[0]).toString() || + yesteryesterday === moment(timeKeys[0]).toString() + ) { + for (var _i = 1; _i <= timeKeys.length; _i++) { + + if ( + moment(timeKeys[_i - 1]).subtract(1, 'd').toString() === + moment(timeKeys[_i]).toString() + ) { + + tmpLongest++; + + if (tmpLongest > user.currentStreak) { + user.currentStreak = tmpLongest; + } + } else { + break; + } + } + } else { + user.currentStreak = 1; + } + + var data = {}; + var progressTimestamps = user.progressTimestamps; + progressTimestamps.forEach(function(timeStamp) { + data[(timeStamp / 1000)] = 1; + }); + var challenges = user.completedChallenges.filter(function( obj ) { + return obj.challengeType === 3 || obj.challengeType === 4; + }); + + user.currentStreak = user.currentStreak || 1; + user.longestStreak = user.longestStreak || 1; + + res.render('account/show', { + title: 'Camper ' + user.username + '\'s portfolio', + username: user.username, + name: user.name, + isMigrationGrandfathered: user.isMigrationGrandfathered, + isGithubCool: user.isGithubCool, + location: user.location, + githubProfile: user.githubProfile, + linkedinProfile: user.linkedinProfile, + codepenProfile: user.codepenProfile, + facebookProfile: user.facebookProfile, + twitterHandle: user.twitterHandle, + bio: user.bio, + picture: user.picture, + progressTimestamps: user.progressTimestamps, + calender: data, + challenges: challenges, + moment: moment, + longestStreak: user.longestStreak + + (user.longestStreak === 1 ? ' day' : ' days'), + currentStreak: user.currentStreak + + (user.currentStreak === 1 ? ' day' : ' days') + }); } ); } diff --git a/server/server.js b/server/server.js index 93b0a1a322..8a75517964 100755 --- a/server/server.js +++ b/server/server.js @@ -2,12 +2,13 @@ require('dotenv').load(); var pmx = require('pmx'); pmx.init(); -var assign = require('lodash').assign, - loopback = require('loopback'), - boot = require('loopback-boot'), - expressState = require('express-state'), - path = require('path'), - passportProviders = require('./passport-providers'); +var uuid = require('node-uuid'), + assign = require('lodash').assign, + loopback = require('loopback'), + boot = require('loopback-boot'), + expressState = require('express-state'), + path = require('path'), + passportProviders = require('./passport-providers'); var generateKey = require('loopback-component-passport/lib/models/utils').generateKey; @@ -43,6 +44,39 @@ passportConfigurator.setupModels({ userCredentialModel: app.models.userCredential }); +// using es6 argument destructing +function setProfileFromGithub( + user, + { + profileUrl: githubURL, + username + }, + { + location, + email: githubEmail, + id: githubId, + 'created_at': joinedGithubOn, + blog: website, + name + } +) { + return assign( + user, + { isGithubCool: true, isMigrationGrandfathered: false }, + { + name, + username: username.toLowerCase(), + location, + joinedGithubOn, + website, + githubId, + githubURL, + githubEmail, + githubProfile: githubURL + } + ); +} + var passportOptions = { emailOptional: true, profileToUser: function(provider, profile) { @@ -53,9 +87,9 @@ var passportOptions = { emails[0].value : null; - var username = (profile.username || profile.id); - username = typeof username === 'string' ? username.toLowerCase() : - username; + // create random username + // username will be assigned when camper signups for Github + var username = 'fcc' + uuid.v4().slice(0, 8); var password = generateKey('password'); var userObj = { username: username, @@ -65,8 +99,9 @@ var passportOptions = { if (email) { userObj.email = email; } + if (provider === 'github-login') { - userObj.isGithubCool = true; + setProfileFromGithub(userObj, profile, profile._json); } return userObj; } diff --git a/server/views/account/account.jade b/server/views/account/account.jade index b1204b264e..3e95b67dc6 100644 --- a/server/views/account/account.jade +++ b/server/views/account/account.jade @@ -5,67 +5,74 @@ block content .panel.panel-info(ng-controller="profileValidationController") .panel-heading.text-center Update your portfolio here: .panel-body - if (!user.github) + if (!user.isGithubCool) + .row .col-xs-12 - a.btn.btn-lg.btn-block.btn-github.btn-link-social(href='/auth/github') - i.fa.fa-github - | Link GitHub with my account - .col-xs-12 - form.form-horizontal(action='/account/profile', method='POST', novalidate='novalidate', name='profileForm' ng-show="asyncComplete") - input(type='hidden', name='_csrf', value=_csrf) - .form-group - label.col-sm-3.col-sm-offset-1.control-label(for='bio') Bio (140 characters) - .col-sm-4 - input.form-control(type='text', name='bio', autocomplete="off", ng-model='user.bio', ng-maxlength='140', id='bio') - .col-sm-4.col-sm-offset-5(ng-cloak, ng-show='profileForm.bio.$error.maxlength && !profileForm.bio.$pristine') - alert(type='danger') - span.ion-close-circled - | Your bio must be fewer than 140 characters. - .form-group - label.col-sm-3.col-sm-offset-1.control-label(for='email') Twitter - .col-sm-4 - .input-group.twitter-input - span.input-group-addon @ - input.form-control(type='text', name='twitterHandle', autocomplete="off", id='twitterHandle', ng-model='user.twitterHandle', ng-maxlength='15', ng-pattern="/^[A-z0-9_]+$/") - .col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.twitterHandle.$error.pattern") - alert(type='danger') + .text-left.btn-info.btn.btn-block.btn-link-social(href='http://www.freecodecamp.com/challenges/waypoint-join-our-chat-room') + span Link your account to GitHub to update your portfolio page. Click here if you don’t have a GitHub account yet. + .row + .col-xs-12 + a.btn.btn-lg.btn-block.btn-github.btn-link-social(href='/auth/github') + i.fa.fa-github + | Link GitHub with my account + + .row + .col-xs-12 + form.form-horizontal(action='/account/profile', method='POST', novalidate='novalidate', name='profileForm' ng-show="asyncComplete") + input(type='hidden', name='_csrf', value=_csrf) + .form-group + label.col-sm-3.col-sm-offset-1.control-label(for='bio') Bio (140 characters) + .col-sm-4 + input.form-control(type='text', name='bio', autocomplete="off", ng-model='user.bio', ng-maxlength='140', id='bio') + .col-sm-4.col-sm-offset-5(ng-cloak, ng-show='profileForm.bio.$error.maxlength && !profileForm.bio.$pristine') + alert(type='danger') span.ion-close-circled - | Your Twitter handle should only contain letters, numbers and underscores (az10_). - .col-sm-4.col-sm-offset-5(ng-cloak, ng-show='profileForm.twitterHandle.$error.maxlength && !profileForm.twitterHandle.$pristine') - alert(type='danger') + | Your bio must be fewer than 140 characters. + .form-group + label.col-sm-3.col-sm-offset-1.control-label(for='email') Twitter + .col-sm-4 + .input-group.twitter-input + span.input-group-addon @ + input.form-control(type='text', name='twitterHandle', autocomplete="off", id='twitterHandle', ng-model='user.twitterHandle', ng-maxlength='15', ng-pattern="/^[A-z0-9_]+$/") + .col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.twitterHandle.$error.pattern") + alert(type='danger') + span.ion-close-circled + | Your Twitter handle should only contain letters, numbers and underscores (az10_). + .col-sm-4.col-sm-offset-5(ng-cloak, ng-show='profileForm.twitterHandle.$error.maxlength && !profileForm.twitterHandle.$pristine') + alert(type='danger') + span.ion-close-circled + | Your name must be fewer than 15 characters. + + .form-group + label.col-sm-3.col-sm-offset-1.control-label(for='email') CodePen + .col-sm-4 + input.form-control(type='url', name='codepenProfile', id='codepenProfile', autocomplete="off", ng-model='user.codepenProfile', placeholder='http://') + .col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.codepenProfile.$error.url && !profileForm.codepenProfile.$pristine") + alert(type='danger') span.ion-close-circled - | Your name must be fewer than 15 characters. + | Please enter a valid URL format (http://www.example.com). - .form-group - label.col-sm-3.col-sm-offset-1.control-label(for='email') CodePen - .col-sm-4 - input.form-control(type='url', name='codepenProfile', id='codepenProfile', autocomplete="off", ng-model='user.codepenProfile', placeholder='http://') - .col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.codepenProfile.$error.url && !profileForm.codepenProfile.$pristine") - alert(type='danger') - span.ion-close-circled - | Please enter a valid URL format (http://www.example.com). + .form-group + label.col-sm-3.col-sm-offset-1.control-label(for='email') LinkedIn + .col-sm-4 + input.form-control(type='url', name='linkedinProfile', id='linkedinProfile', autocomplete="off", ng-model='user.linkedinProfile', placeholder='http://') + .col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.linkedinProfile.$error.url && !profileForm.linkedinProfile.$pristine") + alert(type='danger') + span.ion-close-circled + | Please enter a valid URL format (http://www.example.com). - .form-group - label.col-sm-3.col-sm-offset-1.control-label(for='email') LinkedIn - .col-sm-4 - input.form-control(type='url', name='linkedinProfile', id='linkedinProfile', autocomplete="off", ng-model='user.linkedinProfile', placeholder='http://') - .col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.linkedinProfile.$error.url && !profileForm.linkedinProfile.$pristine") - alert(type='danger') - span.ion-close-circled - | Please enter a valid URL format (http://www.example.com). + .form-group + label.col-sm-3.col-sm-offset-1.control-label(for='email') Facebook + .col-sm-4 + input.form-control(type='url', name='facebookProfile', id='facebookProfile', autocomplete="off", ng-model='user.facebookProfile', placeholder='http://') + .col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.facebookProfile.$error.url && !profileForm.facebookProfile.$pristine") + alert(type='danger') + span.ion-close-circled + | Please enter a valid URL format (http://www.example.com). - .form-group - label.col-sm-3.col-sm-offset-1.control-label(for='email') Facebook - .col-sm-4 - input.form-control(type='url', name='facebookProfile', id='facebookProfile', autocomplete="off", ng-model='user.facebookProfile', placeholder='http://') - .col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.facebookProfile.$error.url && !profileForm.facebookProfile.$pristine") - alert(type='danger') - span.ion-close-circled - | Please enter a valid URL format (http://www.example.com). - - button.btn.btn-lg.btn-block.btn-primary.btn-link-social(type='submit', ng-disabled='profileForm.$invalid') - span.ion-edit - | Update my info + button.btn.btn-lg.btn-block.btn-primary.btn-link-social(type='submit', ng-disabled='!user.isGithubCool || profileForm.$invalid') + span.ion-edit + | Update my info .panel.panel-info .panel-heading.text-center Manage your account here: diff --git a/server/views/account/email-signup.jade b/server/views/account/email-signup.jade index c270b2470c..965eca605e 100644 --- a/server/views/account/email-signup.jade +++ b/server/views/account/email-signup.jade @@ -19,25 +19,6 @@ block content alert(type='danger') span.ion-close-circled | That email address is already in use. - .form-group - .col-sm-6.col-sm-offset-3 - input.form-control(type='text', name='username', ng-keypress='', autocomplete="off", id='username', placeholder='username', ng-model='username', unique-username='', required, ng-minlength=5, ng-maxlength=20, ng-pattern="/^[A-z0-9_]+$/") - .col-sm-6.col-sm-offset-3(ng-cloak, ng-show="profileForm.username.$error.pattern && !signupForm.username.$pristine") - alert(type='danger') - span.ion-close-circled - | Your username should only contain letters, numbers and underscores (az10_). - .col-sm-6.col-sm-offset-3(ng-cloak, ng-show="signupForm.username.$error.unique && !signupForm.username.$pristine") - alert(type='danger') - span.ion-close-circled - | This username is taken. - .col-sm-6.col-sm-offset-3(ng-cloak, ng-show="signupForm.username.$error.minlength && !signupForm.username.$pristine") - alert(type='danger') - span.ion-close-circled - | Your username must be at least 5 characters long. - .col-sm-6.col-sm-offset-3(ng-cloak, ng-show="signupForm.username.$error.maxlength && !signupForm.username.$pristine") - alert(type='danger') - span.ion-close-circled - | Your usernames must be 20 characters or fewer. .form-group .col-sm-6.col-sm-offset-3 input.form-control(type='password', ng-model='password', name='password', id='password', placeholder='password', required, ng-minlength=8) diff --git a/server/views/partials/navbar.jade b/server/views/partials/navbar.jade index 8d310d8846..a306151639 100644 --- a/server/views/partials/navbar.jade +++ b/server/views/partials/navbar.jade @@ -21,15 +21,7 @@ nav.navbar.navbar-default.navbar-fixed-top.nav-height a.btn.signup-btn.signup-btn-nav(href='/login') Sign in else li - if (user.username) - a(href='/' + user.username) [ #{user.progressTimestamps.length} ] - - else - a(href='/account') [ #{user.progressTimestamps.length} ] + a(href='/account') [ #{user.progressTimestamps.length} ] .hidden-xs.hidden-sm - if (user.username) - a(href='/' + user.username) - img.profile-picture.float-right(src='#{user.picture}') - else - a(href='/account') - img.profile-picture.float-right(src='#{user.picture}') + a(href='/account') + img.profile-picture.float-right(src='#{user.picture}')