diff --git a/app/assets/images/pages/account/profile/education.png b/app/assets/images/pages/account/profile/education.png new file mode 100644 index 000000000..dad4914c6 Binary files /dev/null and b/app/assets/images/pages/account/profile/education.png differ diff --git a/app/assets/images/pages/account/profile/icon_facebook.png b/app/assets/images/pages/account/profile/icon_facebook.png new file mode 100644 index 000000000..b775c18fa Binary files /dev/null and b/app/assets/images/pages/account/profile/icon_facebook.png differ diff --git a/app/assets/images/pages/account/profile/icon_github.png b/app/assets/images/pages/account/profile/icon_github.png new file mode 100644 index 000000000..fc1801abc Binary files /dev/null and b/app/assets/images/pages/account/profile/icon_github.png differ diff --git a/app/assets/images/pages/account/profile/icon_gplus.png b/app/assets/images/pages/account/profile/icon_gplus.png new file mode 100644 index 000000000..c2343eb50 Binary files /dev/null and b/app/assets/images/pages/account/profile/icon_gplus.png differ diff --git a/app/assets/images/pages/account/profile/icon_linkedin.png b/app/assets/images/pages/account/profile/icon_linkedin.png new file mode 100644 index 000000000..cdd0ff6c2 Binary files /dev/null and b/app/assets/images/pages/account/profile/icon_linkedin.png differ diff --git a/app/assets/images/pages/account/profile/icon_twitter.png b/app/assets/images/pages/account/profile/icon_twitter.png new file mode 100644 index 000000000..1280ad6df Binary files /dev/null and b/app/assets/images/pages/account/profile/icon_twitter.png differ diff --git a/app/assets/images/pages/account/profile/work.png b/app/assets/images/pages/account/profile/work.png new file mode 100644 index 000000000..72e659071 Binary files /dev/null and b/app/assets/images/pages/account/profile/work.png differ diff --git a/app/lib/auth.coffee b/app/lib/auth.coffee index d46733089..4fa94e61f 100644 --- a/app/lib/auth.coffee +++ b/app/lib/auth.coffee @@ -11,7 +11,6 @@ init = -> me.set 'testGroupNumber', Math.floor(Math.random() * 256) me.save() - me.loadGravatarProfile() if me.get('email') Backbone.listenTo(me, 'sync', Backbone.Mediator.publish('me:synced', {me:me})) module.exports.createUser = (userObject, failure=backboneFailure, nextURL=null) -> @@ -52,4 +51,3 @@ trackFirstArrival = -> storage.save(BEEN_HERE_BEFORE_KEY, true) init() - diff --git a/app/lib/contact.coffee b/app/lib/contact.coffee index fa38fbf69..ade94f2e9 100644 --- a/app/lib/contact.coffee +++ b/app/lib/contact.coffee @@ -1,16 +1,9 @@ - - module.exports.sendContactMessage = (contactMessageObject, modal) -> modal.find('.sending-indicator').show() - jqxhr = $.post '/contact', - email: contactMessageObject.email - message: contactMessageObject.message - , - (response) -> - console.log "Got contact response:", response - modal.find('.sending-indicator').hide() - modal.find('#contact-message').val("Thanks!") - _.delay -> - modal.find('#contact-message').val("") - modal.modal 'hide' - , 1000 + jqxhr = $.post '/contact', contactMessageObject, (response) -> + modal.find('.sending-indicator').hide() + modal.find('#contact-message').val("Thanks!") + _.delay -> + modal.find('#contact-message').val("") + modal.modal 'hide' + , 1000 diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 7d35d42aa..36bbfd9e8 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -3,6 +3,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr loading: "Loading..." saving: "Saving..." sending: "Sending..." + send: "Send" cancel: "Cancel" save: "Save" create: "Create" @@ -111,6 +112,8 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr forum_page: "our forum" forum_suffix: " instead." send: "Send Feedback" + contact_candidate: "Contact Candidate" + recruitment_reminder: "Use this form to get in touch with candidates you are interested in interviewing. Remember that CodeCombat charges 18% of first-year salary for any full-time candidate you hire who stays 90 days, but that part-timers, remote employees, contractors, and interns are free." diplomat_suggestion: title: "Help translate CodeCombat!" @@ -142,9 +145,6 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr password_tab: "Password" emails_tab: "Emails" admin: "Admin" - gravatar_select: "Select which Gravatar photo to use" - gravatar_add_photos: "Add thumbnails and photos to a Gravatar account for your email to choose an image." - gravatar_add_more_photos: "Add more photos to your Gravatar account to access them here." wizard_color: "Wizard Clothes Color" new_password: "New Password" new_password_verify: "Verify" @@ -166,17 +166,6 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr edit_settings: "Edit Settings" profile_for_prefix: "Profile for " profile_for_suffix: "" - profile: "Profile" - user_not_found: "No user found. Check the URL?" - gravatar_not_found_mine: "We couldn't find your profile associated with:" - gravatar_not_found_email_suffix: "." - gravatar_signup_prefix: "Sign up at " - gravatar_signup_suffix: " to get set up!" - gravatar_not_found_other: "Alas, there's no profile associated with this person's email address." - gravatar_contact: "Contact" - gravatar_websites: "Websites" - gravatar_accounts: "As Seen On" - gravatar_profile_link: "Full Gravatar Profile" play_level: level_load_error: "Level could not be loaded: " @@ -345,6 +334,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr results: "Results" description: "Description" or: "or" + subject: "Subject" email: "Email" password: "Password" message: "Message" @@ -616,7 +606,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr bad_input: "Bad input." server_error: "Server error." unknown: "Unknown error." - + resources: your_sessions: "Your Sessions" level: "Level" @@ -626,4 +616,6 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr facebook_friend_sessions: "Facebook Friend Sessions" gplus_friends: "G+ Friends" gplus_friend_sessions: "G+ Friend Sessions" - leaderboard: 'leaderboard' \ No newline at end of file + leaderboard: "Leaderboard" + user_schema: "User Schema" + user_profile: "User Profile" diff --git a/app/models/User.coffee b/app/models/User.coffee index c3bf71146..9f688f46b 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -8,53 +8,25 @@ module.exports = class User extends CocoModel initialize: -> super() - @on 'change:emailHash', -> - @gravatarProfile = null - @loadGravatarProfile() isAdmin: -> permissions = @attributes['permissions'] or [] return 'admin' in permissions - gravatarAvatarURL: -> - avatar_url = GRAVATAR_URL + 'avatar/' - return avatar_url if not @emailHash - return avatar_url + @emailHash - - loadGravatarProfile: -> - emailHash = @get('emailHash') - return if not emailHash - functionName = 'gotProfile'+emailHash - profileUrl = "#{GRAVATAR_URL}#{emailHash}.json?callback=#{functionName}" - script = $("") - $('head').append(script) - window[functionName] = (profile) => - @gravatarProfile = profile - @trigger('change', @) - - func = => @gravatarProfile = null unless @gravatarProfile - setTimeout(func, 1000) - displayName: -> - @get('name') or @gravatarName() or "Anoner" + @get('name') or "Anoner" lang: -> @get('preferredLanguage') or "en-US" - gravatarName: -> - @gravatarProfile?.entry[0]?.name?.formatted or '' - - gravatarPhotoURLs: -> - photos = @gravatarProfile?.entry[0]?.photos - return if not photos - (photo.value for photo in photos) - - getPhotoURL: -> - photoURL = @get('photoURL') - validURLs = @gravatarPhotoURLs() - return @gravatarAvatarURL() unless validURLs and validURLs.length - return validURLs[0] unless photoURL in validURLs - return photoURL + getPhotoURL: (size=80, useJobProfilePhoto=false) -> + photoURL = if useJobProfilePhoto then @get('jobProfile')?.photoURL else null + photoURL ||= @get('photoURL') + if photoURL + prefix = if photoURL.search(/\?/) is -1 then "?" else "&" + return "#{photoURL}#{prefix}s=#{size}" if photoURL.search('http') isnt -1 # legacy + return "/file/#{photoURL}#{prefix}s=#{size}" + return "/db/user/#{@id}/avatar?s=#{size}" @getByID = (id, properties, force) -> {me} = require('lib/auth') @@ -66,7 +38,7 @@ module.exports = class User extends CocoModel success: -> user.loading = false Backbone.Mediator.publish('user:fetched') - user.loadGravatarProfile() + #user.trigger 'sync' # needed? ) cache[id] = user user diff --git a/app/styles/account/profile.sass b/app/styles/account/profile.sass index 2edec8f24..0d0f4e450 100644 --- a/app/styles/account/profile.sass +++ b/app/styles/account/profile.sass @@ -1,15 +1,195 @@ #profile-view - button - float: right - i - margin-right: 5px - - img.img-thumbnail - margin: 20px 0 + .profile-control-bar + background-color: rgb(78, 78, 78) + width: 100% + text-align: center + + button.edit-settings-button + margin: 2px + i + margin-right: 5px - li - list-style: none - - ul - margin: 0 + .approved, .not-approved + display: none + + .main-content-area padding: 0 + + .flat-button + width: 100% + margin-bottom: 10px + background: rgb(78, 78, 78) + border: 0 + border-radius: 0 + padding: 10px + + .public-profile-container + padding: 20px + + img.profile-photo + width: 256px + border-radius: 6px + + .job-profile-container + width: 100% + height: 100% + padding: 0 + display: table + + h1, h2, h3, h4, h5, h6 + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif + color: #555 + + ul.links, ul.projects + margin: 0 + padding: 0 + + li + list-style: none + + .job-profile-row + height: 100% + display: table-row + + .full-height-column + height: 100% + padding: 5px + display: table-cell + vertical-align: top + + h3:first-child + margin: 5px 0 5px 0 + + .left-column + width: 250px + padding: 5px + background-color: rgb(220, 220, 220) + + .profile-photo-container + position: relative + margin-bottom: 10px + + img.profile-photo + width: 240px + border-radius: 6px + + .profile-caption + background-color: rgba(0, 0, 0, 0.5) + color: white + border-bottom-right-radius: 6px + border-bottom-left-radius: 6px + position: absolute + width: 100% + bottom: 0px + text-align: center + + ul.links + li.has-icon + display: inline-block + img + margin: 0 0 10px 0 + li.has-icon:not(:nth-child(5)) + img + margin: 0 10px 10px 0 + + #contact-candidate + margin-top: 20px + background-color: rgb(177, 55, 25) + padding: 15px + font-size: 20px + + .middle-column + width: 524px + background-color: white + padding-left: 20px + padding-right: 20px + + &.double-column + width: 524px + 250px + padding-left: 30px + padding-right: 30px + + code + background-color: rgb(220, 220, 220) + color: #555 + margin: 2px 0 + display: inline-block + text-transform: lowercase + + .long-description + margin-top: 10px + img + max-width: 524px - 60px + max-height: 200px + + .experience-header + margin-top: 25px + + .header-icon + margin-right: 10px + width: 32px + height: 32px + + .duration + margin-left: 10px + margin-bottom: 10px + + #job-profile-notes + width: 100% + height: 100px + + .right-column + width: 250px + background-color: rgb(220, 220, 220) + + > h3:first-child + background-color: white + padding: 5px 5px + margin: 5px 2px 5px 2px + + ul.projects + li + margin-bottom: 10px + padding: 5px 5px + border: 2px solid rgb(220, 220, 220) + transition: .5s ease-in-out + position: relative + background-color: white + + &:hover + border-color: rgb(100, 130, 255) + + a + position: relative + z-index: 2 + + > a + position: absolute + width: 100% + height: 100% + top: 0 + left: 0 + z-index: 1 + + .project-image + width: 230px + height: 115px + background-size: cover + background-repeat: no-repeat + background-position: center + + -webkit-filter: grayscale(100%) + -webkit-transition: .5s ease-in-out + -moz-filter: grayscale(100%) + -moz-transition: .5s ease-in-out + -o-filter: grayscale(100%) + -o-transition: .5s ease-in-out + filter: grayscale(100%) + transition: .5s ease-in-out + + li:hover + .project-image + -webkit-filter: grayscale(0%) + -moz-filter: grayscale(0%) + -o-filter: grayscale(0%) + filter: grayscale(0%) diff --git a/app/styles/account/settings.sass b/app/styles/account/settings.sass index 8751e59ef..b768d69a2 100644 --- a/app/styles/account/settings.sass +++ b/app/styles/account/settings.sass @@ -8,15 +8,20 @@ background: #eee border-radius: 5px - #save-button - float: right + #save-button-container + position: fixed + top: 100px + width: 1000px + z-index: 10 - .thumbnails - text-align: center - .thumbnail - margin-bottom: 30px - margin-right: 20px - float: left + #save-button + float: right + + &.btn-info, &.btn-danger + opacity: 1.0 + + .gravatar-fallback + margin-top: 10px input.range position: relative @@ -37,4 +42,15 @@ font-size: 12px .form - max-width: 600px \ No newline at end of file + max-width: 600px + + #job-profile-treema + background-color: white + + input + width: 790px + + .treema-description + font-size: 14px + line-height: 22px + opacity: 1 diff --git a/app/styles/employers.sass b/app/styles/employers.sass new file mode 100644 index 000000000..2d61d81fa --- /dev/null +++ b/app/styles/employers.sass @@ -0,0 +1,18 @@ +#employers-view + .tablesorter + //img + // display: none + + .tablesorter-header + cursor: pointer + &:hover + color: black + + .tablesorter-headerAsc + background-color: #cfc + + .tablesorter-headerDesc + background-color: #ccf + + tr + cursor: pointer diff --git a/app/templates/account/job_profile.jade b/app/templates/account/job_profile.jade new file mode 100644 index 000000000..a5eff46d3 --- /dev/null +++ b/app/templates/account/job_profile.jade @@ -0,0 +1,8 @@ +h3(data-i18n="account_settings.job_profile") Job Profile + +if me.get('jobProfileApproved') + p.lead(data-i18n="account_settings.job_profile_approved") Your job profile has been approved by CodeCombat. Hungry employers will see it until you mark it inactive or it is stale for two months. +else + p.lead(data-i18n="account_settings.job_profile_explanation") Hi! Fill this out, and if we think we can find you a software developer job, we will get in touch to approve your profile. + +#job-profile-treema \ No newline at end of file diff --git a/app/templates/account/profile.jade b/app/templates/account/profile.jade index 65dc9786b..5ef74256e 100644 --- a/app/templates/account/profile.jade +++ b/app/templates/account/profile.jade @@ -1,72 +1,100 @@ extends /templates/base block content + + if myProfile || (me.isAdmin() && user.get('jobProfile')) + .profile-control-bar + if myProfile + a(href="/account/settings") + button.btn.edit-settings-button + i.icon-cog + span(data-i18n="account_profile.edit_settings") Edit Settings + if me.isAdmin() && user.get('jobProfile') + button.btn.edit-settings-button#toggle-job-profile-approved + i.icon-cog + span(data-i18n='account_profile.approved').approved Approved + span(data-i18n='account_profile.approved').not-approved Not Approved - if myProfile - a(href="/account/settings") - button.btn - i.icon-cog - span(data-i18n="account_profile.edit_settings") Edit Settings - - h2 - if grav && grav.name && grav.name.formatted - span(data-i18n="account_profile.profile_for_prefix") Profile for - span= grav.name.formatted - span(data-i18n="account_profile.profile_for_suffix") - else - span(data-i18n="account_profile.profile") Profile + if user.get('jobProfile') + - var profile = user.get('jobProfile'); + .job-profile-container + .job-profile-row + .left-column.full-height-column + .profile-photo-container + img.profile-photo(src=user.getPhotoURL(240, true)) + .profile-caption= profile.jobTitle || 'Software Developer' - if loadingProfile - p(data-i18n="common.loading") Loading... + if profileLinks.length + ul.links + each link in profileLinks + li(title=profile.name + " on " + link.name, class=link.icon ? "has-icon" : "") + a(href=link.link) + if link.icon + img(src=link.icon.url, alt=link.icon.name) + else + button.btn.btn-large.btn-inverse.flat-button= link.name - else if !user.get('emailHash') - p(data-i18n="account_profile.user_not_found") No user found. Check the URL? + div= profile.city + ', ' + profile.country + div= profile.visa + div Looking for: #{profile.lookingFor} + div Last updated #{moment(profile.updated).fromNow()} - else if !user.gravatarProfile - if myProfile - p - span(data-i18n="account_profile.gravatar_not_found_mine") We couldn't find your profile associated with: - strong "#{me.get('email')}" - span(data-i18n="account_profile.gravatar_not_found_email_suffix") . - span - span(data-i18n="account_profile.gravatar_signup_prefix") Sign up at - a(href="http://en.gravatar.com/") Gravatar - span(data-i18n="account_profile.gravatar_signup_suffix") to get set up! - else - p(data-i18n="account_profile.gravatar_not_found_other") - | Alas, there's no profile associated with this person's email address. + button#contact-candidate.btn.btn-large.btn-inverse.flat-button Contact #{profile.name.split(' ')[0]} + + .middle-column.full-height-column + h3= profile.name + p= profile.shortDescription + + each skill in profile.skills + code= skill + span + div.long-description!= marked(profile.longDescription) + + if profile.work.length + h3.experience-header + img.header-icon(src="/images/pages/account/profile/work.png", alt="") + | Work Experience + each job in profile.work + div.duration.pull-right= job.duration + | #{job.role} at #{job.employer} + .clearfix + + if profile.education.length + h3.experience-header + img.header-icon(src="/images/pages/account/profile/education.png", alt="") + | Education + each school in profile.education + div.duration.pull-right= school.duration + | #{school.degree} at #{school.school} + .clearfix + + if user.get('jobProfileNotes') || me.isAdmin() + h3.experience-header Our Notes + - var notes = user.get('jobProfileNotes') || ''; + if me.isAdmin() + textarea#job-profile-notes!= notes + else + div!= marked(notes) + + .right-column.full-height-column + if profile.projects.length + h3 Projects + ul.projects + each project in profile.projects + li + a(href=project.link) + .project-image(style="background-image: url(/file/" + project.picture + ")") + p= project.name + div!= marked(project.description) else - .container - div.row - div.col-xs-3 - img(src=photoURL).img-thumbnail - - p.about-me #{grav.aboutMe} + .public-profile-container + h2 + span(data-i18n="account_profile.profile_for_prefix") Profile for + span= user.get('name') + span(data-i18n="account_profile.profile_for_suffix") - if grav.emails - div.col-xs-3 - h3(data-i18n="account_profile.gravatar_contact") Contact - ul - each email in grav.emails - li #{email.value} - - if grav.urls && grav.urls.length - div.col-xs-3 - h3(data-i18n="account_profile.gravatar_websites") Websites - ul - each url in grav.urls - li - a(href="#{url.value}") #{url.title} - - if grav.accounts - div.col-xs-3 - h3(data-i18n="account_profile.gravatar_accounts") As Seen On - ul - each account in grav.accounts - li - a(href="#{account.url}") #{account.domain} - - hr - p - a(href="#{grav.profileUrl}", data-i18n="account_profile.gravatar_profile_link") Full Gravatar Profile + img.profile-photo(src=user.getPhotoURL(256)) + + h2 TODO + p Public user profiles are not ready yet. \ No newline at end of file diff --git a/app/templates/account/settings.jade b/app/templates/account/settings.jade index 91b533b1b..a1e829b7e 100644 --- a/app/templates/account/settings.jade +++ b/app/templates/account/settings.jade @@ -8,7 +8,8 @@ block content p(data-i18n="account_settings.not_logged_in") Log in or create an account to change your settings. else - button.btn#save-button.disabled.secret(data-i18n="account_settings.autosave") Changes Save Automatically + #save-button-container + button.btn#save-button.disabled.secret(data-i18n="account_settings.autosave") Changes Save Automatically ul.nav.nav-pills#settings-tabs li @@ -21,6 +22,9 @@ block content a(href="#password-pane", data-toggle="tab", data-i18n="account_settings.password_tab") Password li a(href="#email-pane", data-toggle="tab", data-i18n="account_settings.emails_tab") Emails + if showsJobProfileTab + li + a(href="#job-profile-pane", data-toggle="tab", data-i18n="account_settings.job_profile_tab") Job Profile .tab-content#settings-panes #general-pane.tab-pane @@ -28,7 +32,7 @@ block content .form .form-group label.control-label(for="name", data-i18n="general.name") Name - input#name.form-control(name="name", type="text", value="#{me.get('name')||''}", placeholder="#{gravatarName}") + input#name.form-control(name="name", type="text", value="#{me.get('name') || ''}") .form-group label.control-label(for="email", data-i18n="general.email") Email input#email.form-control(name="email", type="text", value="#{me.get('email')}") @@ -39,23 +43,11 @@ block content #picture-pane.tab-pane - h3(data-i18n="account_settings.gravatar_select") Select which Gravatar photo to use - p - if !photos - span(data-i18n="account_settings.gravatar_add_photos") Add thumbnails and photos to a Gravatar account for your email to choose an image. - - else - .thumbnails - each photo, i in photos - .thumbnail - label(for="photo-#{i}") - img(src=photo) - br - input(type="radio", name="photoURL", value="#{photo}", id="photo-#{i}", checked=photo==chosenPhoto) - .clearfix - p - a(href="http://en.gravatar.com/profiles/edit/?noclose#your-images", target="_blank", data-i18n="account_settings.gravatar_add_more_photos") Add more photos to your Gravatar account to access them here. - + h3(data-i18n="account_settings.upload_picture") Upload a picture + #picture-treema + .gravatar-fallback + img(src=me.getPhotoURL(256), alt="Gravatar", title="Gravatar fallback image") + #wizard-pane.tab-pane #wizard-settings-view @@ -153,3 +145,6 @@ block content span(data-i18n="contribute.ambassador_subscribe_desc").help-block Get emails on support updates and multiplayer developments. button.btn#toggle-all-button(data-i18n="account_settings.email_toggle") Toggle All + + #job-profile-pane.tab-pane + #job-profile-view diff --git a/app/templates/base.jade b/app/templates/base.jade index 8212882af..f31bc2db3 100644 --- a/app/templates/base.jade +++ b/app/templates/base.jade @@ -33,7 +33,7 @@ body if me.get('anonymous') === false button.btn.btn-primary.navbuttontext.header-font#logout-button(data-i18n="login.log_out") Log Out - a.btn.btn-primary.navbuttontext.header-font(href="/account/profile/#{me.id}") + a.btn.btn-primary.navbuttontext.header-font(href=me.get('jobProfile') ? "/account/profile/#{me.id}" : "/account/settings") div.navbuttontext-user-name | #{me.displayName()} i.icon-cog.icon-white.big diff --git a/app/templates/employers.jade b/app/templates/employers.jade index 485d21622..d242a7d77 100644 --- a/app/templates/employers.jade +++ b/app/templates/employers.jade @@ -32,3 +32,54 @@ block content h4 Skill: from interns and entry level to senior developers and management h4 Technologies: just about everything h4 Countries: USA, Canada, Australia, and many more + + if candidates.length + table.table.table-condensed.table-hover.table-responsive.tablesorter + thead + tr + th Name + th Location + th Looking For + th Top 5 Skills + th Yrs Exp + th Last Updated + th Current Job + if me.isAdmin() + th ✓? + + tbody + for candidate, index in candidates + - var profile = candidate.get('jobProfile'); + - var authorized = candidate.id; // If we have the id, then we are authorized. + tr(data-candidate-id=candidate.id) + td + if authorized + img(src=candidate.getPhotoURL(50), alt=profile.name, title=profile.name, width=50) + p= profile.name + else + img(src="/images/pages/contribute/archmage.png", alt="", title="Sign up as an employer to see our candidates", width=50) + p Developer ##{index + 1} + if profile.country == 'USA' + td= profile.city + else + td= profile.country + td= profile.lookingFor + td + each skill in profile.skills.slice(0, 5) + code= skill + span + td= profile.experience + td= moment(profile.updated).fromNow() + if authorized + if profile.work.length + td= profile.work[0].role + ' at ' + profile.work[0].employer + else + td + else + td + em Employer sign-up required. + if me.isAdmin() + if candidate.get('jobProfileApproved') + td ✓ + else + td ✗ \ No newline at end of file diff --git a/app/templates/modal/diplomat_suggestion.jade b/app/templates/modal/diplomat_suggestion.jade index b3d29cb6b..56d98f33a 100644 --- a/app/templates/modal/diplomat_suggestion.jade +++ b/app/templates/modal/diplomat_suggestion.jade @@ -1,7 +1,7 @@ extends /templates/modal/modal_base block modal-header-content - h3(data-i18n="diplomat_suggestion.title") + h3(data-i18n="diplomat_suggestion.title") Help translate CodeCombat! block modal-body-content h4(data-i18n="diplomat_suggestion.sub_heading") We need your language skills. diff --git a/app/templates/modal/employer_signup_modal.jade b/app/templates/modal/employer_signup_modal.jade new file mode 100644 index 000000000..809b26452 --- /dev/null +++ b/app/templates/modal/employer_signup_modal.jade @@ -0,0 +1,9 @@ +extends /templates/modal/modal_base + +block modal-header-content + h3(data-i18n="employer_signup.title") Hire CodeCombat Players + +block modal-body-content + h4(data-i18n="employer_signup.sub_heading") Let us find your next brilliant developers. + + p(data-i18n="employer_signup.pitch_body") When you hire one of our players, you will pay CodeCombat 18% of her first-year salary, payable within 30 days of when she starts working. We will fully refund our placement fee if she leaves or is fired within 90 days. Cool? Email george@codecombat.com to get set up with employer permissions to see our candidates. diff --git a/app/templates/modal/job_profile_contact.jade b/app/templates/modal/job_profile_contact.jade new file mode 100644 index 000000000..87120f033 --- /dev/null +++ b/app/templates/modal/job_profile_contact.jade @@ -0,0 +1,22 @@ +extends /templates/modal/contact + +block modal-header-content + h3(data-i18n="contact.contact_candidate") Contact Candidate + +block modal-body-content + p(data-i18n="contact.recruitment_reminder") Use this form to get in touch with candidates you are interested in interviewing. Remember that CodeCombat charges 18% of first-year salary for any full-time candidate you hire who stays 90 days, but that part-timers, remote employees, contractors, and interns are free. + .form + .form-group + label.control-label(for="contact-email", data-i18n="general.email") Email + input#contact-email.form-control(name="email", type="email", value=me.get('email'), placeholder="Where should the candidate reply?") + .form-group + label.control-label(for="contact-subject", data-i18n="general.subject") Subject + input#contact-subject.form-control(name="subject", type="text", value="Job interest", placeholder="Subject of the email the candidate will receive.") + .form-group + label.control-label(for="contact-message", data-i18n="general.message") Message + textarea#contact-message.form-control(name="message", rows=8) + +block modal-footer-content + span.sending-indicator.pull-left.secret(data-i18n="common.sending") Sending... + a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="common.cancel").btn Cancel + button.btn.btn-primary#contact-submit-button(data-i18n="common.send") Send diff --git a/app/views/account/job_profile_view.coffee b/app/views/account/job_profile_view.coffee new file mode 100644 index 000000000..a39fb6b16 --- /dev/null +++ b/app/views/account/job_profile_view.coffee @@ -0,0 +1,94 @@ +CocoView = require 'views/kinds/CocoView' +template = require 'templates/account/job_profile' +{me} = require('lib/auth') + +module.exports = class JobProfileView extends CocoView + id: 'job-profile-view' + template: template + + editableSettings: [ + 'lookingFor', 'active', 'name', 'city', 'country', 'skills', 'experience', 'shortDescription', 'longDescription', + 'work', 'education', 'visa', 'projects', 'links', 'jobTitle', 'photoURL' + ] + readOnlySettings: [ + 'updated' + ] + + constructor: (options) -> + super options + unless me.schema().loaded + @addSomethingToLoad("user_schema") + @listenToOnce me, 'schema-loaded', => @somethingLoaded 'user_schema' + + afterRender: -> + super() + return if @loading() + @buildJobProfileTreema() + + buildJobProfileTreema: -> + visibleSettings = @editableSettings.concat @readOnlySettings + data = _.pick (me.get('jobProfile') ? {}), (value, key) => key in visibleSettings + data.name ?= (me.get('firstName') + ' ' + me.get('lastName')).trim() if me.get('firstName') + schema = _.cloneDeep me.schema().get('properties').jobProfile + schema.properties = _.pick schema.properties, (value, key) => key in visibleSettings + schema.required = _.intersection schema.required, visibleSettings + for prop in @readOnlySettings + schema.properties[prop].readOnly = true + treemaOptions = + filePath: "db/user/#{me.id}" + schema: schema + data: data + aceUseWrapMode: true + callbacks: {change: @onJobProfileChanged} + nodeClasses: + 'skill': SkillTagNode + 'link-name': LinkNameNode + 'city': CityNode + 'country': CountryNode + + @jobProfileTreema = @$el.find('#job-profile-treema').treema treemaOptions + @jobProfileTreema.build() + @jobProfileTreema.open() + + onJobProfileChanged: (e) => + @hasEditedProfile = true + @trigger 'change' + + getData: -> + return {} unless me.get('jobProfile') or @hasEditedProfile + _.pick @jobProfileTreema.data, (value, key) => key in @editableSettings + + +commonSkills = ['c#', 'java', 'javascript', 'php', 'android', 'jquery', 'python', 'c++', 'html', 'mysql', 'ios', 'asp.net', 'css', 'sql', 'iphone', '.net', 'objective-c', 'ruby-on-rails', 'c', 'ruby', 'sql-server', 'ajax', 'wpf', 'linux', 'database', 'django', 'vb.net', 'windows', 'facebook', 'r', 'html5', 'multithreading', 'ruby-on-rails-3', 'wordpress', 'winforms', 'node.js', 'spring', 'osx', 'performance', 'visual-studio-2010', 'oracle', 'swing', 'algorithm', 'git', 'linq', 'apache', 'web-services', 'perl', 'wcf', 'entity-framework', 'bash', 'visual-studio', 'sql-server-2008', 'hibernate', 'actionscript-3', 'angularjs', 'matlab', 'qt', 'ipad', 'sqlite', 'cocoa-touch', 'cocoa', 'flash', 'mongodb', 'codeigniter', 'jquery-ui', 'css3', 'tsql', 'google-maps', 'silverlight', 'security', 'delphi', 'vba', 'postgresql', 'jsp', 'shell', 'internet-explorer', 'google-app-engine', 'sockets', 'validation', 'scala', 'oop', 'unit-testing', 'xaml', 'parsing', 'twitter-bootstrap', 'google-chrome', 'http', 'magento', 'email', 'android-layout', 'flex', 'rest', 'maven', 'jsf', 'listview', 'date', 'winapi', 'windows-phone-7', 'facebook-graph-api', 'unix', 'url', 'c#-4.0', 'jquery-ajax', 'svn', 'symfony2', 'table', 'cakephp', 'firefox', 'ms-access', 'java-ee', 'jquery-mobile', 'python-2.7', 'tomcat', 'zend-framework', 'opencv', 'visual-c++', 'opengl', 'spring-mvc', 'sql-server-2005', 'authentication', 'search', 'xslt', 'servlets', 'pdf', 'animation', 'math', 'batch-file', 'excel-vba', 'iis', 'mod-rewrite', 'sharepoint', 'gwt', 'powershell', 'visual-studio-2012', 'haskell', 'grails', 'ubuntu', 'networking', 'nhibernate', 'design-patterns', 'testing', 'jpa', 'visual-studio-2008', 'core-data', 'user-interface', 'audio', 'backbone.js', 'gcc', 'mobile', 'design', 'activerecord', 'extjs', 'video', 'stored-procedures', 'optimization', 'drupal', 'image-processing', 'android-intent', 'logging', 'web-applications', 'razor', 'database-design', 'azure', 'vim', 'memory-management', 'model-view-controller', 'cordova', 'c++11', 'selenium', 'ssl', 'assembly', 'soap', 'boost', 'canvas', 'google-maps-api-3', 'netbeans', 'heroku', 'jsf-2', 'encryption', 'hadoop', 'linq-to-sql', 'dll', 'xpath', 'data-binding', 'windows-phone-8', 'phonegap', 'jdbc', 'python-3.x', 'twitter', 'mvvm', 'gui', 'web', 'jquery-plugins', 'numpy', 'deployment', 'ios7', 'emacs', 'knockout.js', 'graphics', 'joomla', 'unicode', 'windows-8', 'android-fragments', 'ant', 'command-line', 'version-control', 'yii', 'github', 'amazon-web-services', 'macros', 'ember.js', 'svg', 'opengl-es', 'django-models', 'solr', 'orm', 'blackberry', 'windows-7', 'ruby-on-rails-4', 'compiler', 'tcp', 'pdo', 'architecture', 'groovy', 'nginx', 'concurrency', 'paypal', 'iis-7', 'express', 'vbscript', 'google-chrome-extension', 'memory-leaks', 'rspec', 'actionscript', 'interface', 'fonts', 'oauth', 'ssh', 'tfs', 'junit', 'struts2', 'd3.js', 'coldfusion', '.net-4.0', 'jqgrid', 'asp-classic', 'https', 'plsql', 'stl', 'sharepoint-2010', 'asp.net-web-api', 'mysqli', 'sed', 'awk', 'internet-explorer-8', 'jboss', 'charts', 'scripting', 'matplotlib', 'laravel', 'clojure', 'entity-framework-4', 'intellij-idea', 'xml-parsing', 'sqlite3', '3d', 'io', 'mfc', 'devise', 'playframework', 'youtube', 'amazon-ec2', 'localization', 'cuda', 'jenkins', 'ssis', 'safari', 'doctrine2', 'vb6', 'amazon-s3', 'dojo', 'air', 'eclipse-plugin', 'android-asynctask', 'crystal-reports', 'cocos2d-iphone', 'dns', 'highcharts', 'ruby-on-rails-3.2', 'ado.net', 'sql-server-2008-r2', 'android-emulator', 'spring-security', 'cross-browser', 'oracle11g', 'bluetooth', 'f#', 'msbuild', 'drupal-7', 'google-apps-script', 'mercurial', 'xna', 'google-analytics', 'lua', 'parallel-processing', 'internationalization', 'java-me', 'mono', 'monotouch', 'android-ndk', 'lucene', 'kendo-ui', 'linux-kernel', 'terminal', 'phpmyadmin', 'makefile', 'ffmpeg', 'applet', 'active-directory', 'coffeescript', 'pandas', 'responsive-design', 'xhtml', 'silverlight-4.0', '.net-3.5', 'jaxb', 'ruby-on-rails-3.1', 'gps', 'geolocation', 'network-programming', 'windows-services', 'laravel-4', 'ggplot2', 'rss', 'webkit', 'functional-programming', 'wsdl', 'telerik', 'maven-2', 'cron', 'mapreduce', 'websocket', 'automation', 'windows-runtime', 'django-forms', 'tkinter', 'android-widget', 'android-activity', 'rubygems', 'content-management-system', 'doctrine', 'django-templates', 'gem', 'fluent-nhibernate', 'seo', 'meteor', 'serial-port', 'glassfish', 'documentation', 'cryptography', 'ef-code-first', 'extjs4', 'x86', 'wordpress-plugin', 'go', 'wix', 'linq-to-entities', 'oracle10g', 'cocos2d', 'selenium-webdriver', 'open-source', 'jtable', 'qt4', 'smtp', 'redis', 'jvm', 'openssl', 'timezone', 'nosql', 'erlang', 'playframework-2.0', 'machine-learning', 'mocking', 'unity3d', 'thread-safety', 'android-actionbar', 'jni', 'udp', 'jasper-reports', 'zend-framework2', 'apache2', 'internet-explorer-7', 'sqlalchemy', 'neo4j', 'ldap', 'jframe', 'youtube-api', 'filesystems', 'make', 'flask', 'gdb', 'cassandra', 'sms', 'g++', 'django-admin', 'push-notification', 'statistics', 'tinymce', 'locking', 'javafx', 'firefox-addon', 'fancybox', 'windows-phone', 'log4j', 'uikit', 'prolog', 'socket.io', 'icons', 'oauth-2.0', 'refactoring', 'sencha-touch', 'elasticsearch', 'symfony1', 'google-api', 'webserver', 'wpf-controls', 'microsoft-metro', 'gtk', 'flex4', 'three.js', 'gradle', 'centos', 'angularjs-directive', 'internet-explorer-9', 'sass', 'html5-canvas', 'interface-builder', 'programming-languages', 'gmail', 'jersey', 'twitter-bootstrap-3', 'arduino', 'requirejs', 'cmake', 'web-development', 'software-engineering', 'startups', 'entrepreneurship', 'social-media-marketing', 'writing', 'marketing', 'web-design', 'graphic-design', 'game-development', 'game-design', 'photoshop', 'illustrator', 'robotics', 'aws', 'devops', 'mathematica', 'bioinformatics', 'data-vis', 'ui', 'embedded-systems', 'codecombat'] + +commonLinkNames = ['GitHub', 'Facebook', 'Twitter', 'G+', 'LinkedIn', 'Personal Website', 'Blog'] + +countries = ['Afghanistan', 'Albania', 'Algeria', 'American Samoa', 'Andorra', 'Angola', 'Anguilla', 'Antarctica', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Aruba', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bermuda', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei Darussalam', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cambodia', 'Cameroon', 'Canada', 'Cape Verde', 'Cayman Islands', 'Central African Republic', 'Chad', 'Chile', 'China', 'Christmas Island', 'Cocos (Keeling) Islands', 'Colombia', 'Comoros', 'Democratic Republic of the Congo (Kinshasa)', 'Congo, Republic of (Brazzaville)', 'Cook Islands', 'Costa Rica', 'Ivory Coast', 'Croatia', 'Cuba', 'Cyprus', 'Czech Republic', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'East Timor', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Ethiopia', 'Falkland Islands', 'Faroe Islands', 'Fiji', 'Finland', 'France', 'French Guiana', 'French Polynesia', 'French Southern Territories', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Gibraltar', 'Great Britain', 'Greece', 'Greenland', 'Grenada', 'Guadeloupe', 'Guam', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hong Kong', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'North Korea', 'South Korea', 'Kosovo', 'Kuwait', 'Kyrgyzstan', 'Lao, People\'s Democratic Republic', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Macau', 'Macedonia, Rep. of', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Martinique', 'Mauritania', 'Mauritius', 'Mayotte', 'Mexico', 'Micronesia, Federal States of', 'Moldova, Republic of', 'Monaco', 'Mongolia', 'Montenegro', 'Montserrat', 'Morocco', 'Mozambique', 'Myanmar, Burma', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'Netherlands Antilles', 'New Caledonia', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'Niue', 'Northern Mariana Islands', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestinian territories', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Pitcairn Island', 'Poland', 'Portugal', 'Puerto Rico', 'Qatar', 'Reunion Island', 'Romania', 'Russian Federation', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Swaziland', 'Sweden', 'Switzerland', 'Syria, Syrian Arab Republic', 'Taiwan', 'Tajikistan', 'Tanzania; officially the United Republic of Tanzania', 'Thailand', 'Tibet', 'Timor-Leste', 'Togo', 'Tokelau', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Turks and Caicos Islands', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'USA', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Vatican City State', 'Venezuela', 'Vietnam', 'Virgin Islands (British)', 'Virgin Islands (U.S.)', 'Wallis and Futuna Islands', 'Western Sahara', 'Yemen', 'Zambia', 'Zimbabwe'] + +commonCities = ['Tokyo', 'Jakarta', 'Seoul', 'Delhi', 'Shanghai', 'Manila', 'Karachi', 'New York', 'Sao Paulo', 'Mexico City', 'Cairo', 'Beijing', 'Osaka', 'Mumbai (Bombay)', 'Guangzhou', 'Moscow', 'Los Angeles', 'Calcutta', 'Dhaka', 'Buenos Aires', 'Istanbul', 'Rio de Janeiro', 'Shenzhen', 'Lagos', 'Paris', 'Nagoya', 'Lima', 'Chicago', 'Kinshasa', 'Tianjin', 'Chennai', 'Bogota', 'Bengaluru', 'London', 'Taipei', 'Ho Chi Minh City (Saigon)', 'Dongguan', 'Hyderabad', 'Chengdu', 'Lahore', 'Johannesburg', 'Tehran', 'Essen', 'Bangkok', 'Hong Kong', 'Wuhan', 'Ahmedabad', 'Chongqung', 'Baghdad', 'Hangzhou', 'Toronto', 'Kuala Lumpur', 'Santiago', 'Dallas-Fort Worth', 'Quanzhou', 'Miami', 'Shenyang', 'Belo Horizonte', 'Philadelphia', 'Nanjing', 'Madrid', 'Houston', 'Xi\'an-Xianyang', 'Milan', 'Luanda', 'Pune', 'Singapore', 'Riyadh', 'Khartoum', 'Saint Petersburg', 'Atlanta', 'Surat', 'Washington', 'Bandung', 'Surabaya', 'Yangoon', 'Alexandria', 'Guadalajara', 'Harbin', 'Boston', 'Zhengzhou', 'Qingdao', 'Abidjan', 'Barcelona', 'Monterrey', 'Ankara', 'Suzhou', 'Phoenix-Mesa', 'Salvador', 'Porto Alegre', 'Rome', 'Accra', 'Sydney', 'Recife', 'Naples', 'Detroit', 'Dalian', 'Fuzhou', 'Medellin', 'San Francisco', 'Silicon Valley', 'Portland', 'Seattle', 'Austin', 'Denver', 'Boulder'] + +autoFocus = true # Not working right now, possibly a Treema bower thing. + +class SkillTagNode extends TreemaNode.nodeMap.string + buildValueForEditing: (valEl) -> + super(valEl) + valEl.find('input').autocomplete(source: commonSkills, minLength: 1, delay: 0, autoFocus: autoFocus) + valEl + +class LinkNameNode extends TreemaNode.nodeMap.string + buildValueForEditing: (valEl) -> + super(valEl) + valEl.find('input').autocomplete(source: commonLinkNames, minLength: 0, delay: 0, autoFocus: autoFocus) + valEl + +class CityNode extends TreemaNode.nodeMap.string + buildValueForEditing: (valEl) -> + super(valEl) + valEl.find('input').autocomplete(source: commonCities, minLength: 1, delay: 0, autoFocus: autoFocus) + valEl + +class CountryNode extends TreemaNode.nodeMap.string + buildValueForEditing: (valEl) -> + super(valEl) + valEl.find('input').autocomplete(source: countries, minLength: 1, delay: 0, autoFocus: autoFocus) + valEl diff --git a/app/views/account/profile_view.coffee b/app/views/account/profile_view.coffee index f61f7f1e2..854f9e985 100644 --- a/app/views/account/profile_view.coffee +++ b/app/views/account/profile_view.coffee @@ -1,36 +1,75 @@ View = require 'views/kinds/RootView' template = require 'templates/account/profile' User = require 'models/User' +JobProfileContactView = require 'views/modal/job_profile_contact_modal' module.exports = class ProfileView extends View id: "profile-view" template: template - loadingProfile: true + + events: + 'click #toggle-job-profile-approved': 'toggleJobProfileApproved' + 'keyup #job-profile-notes': 'onJobProfileNotesChanged' + 'click #contact-candidate': 'onContactCandidate' constructor: (options, @userID) -> + @onJobProfileNotesChanged = _.debounce @onJobProfileNotesChanged, 1000 super options - @user = User.getByID(@userID) - @loadingProfile = false if 'gravatarProfile' of @user - @listenTo(@user, 'change', @userChanged) - @listenTo(@user, 'error', @userError) - - userChanged: (user) -> - @loadingProfile = false if 'gravatarProfile' of user - @render() - - userError: (user) -> - @loadingProfile = false - @render() + if @userID is me.id + @user = me + else + @user = User.getByID(@userID) + @addResourceToLoad @user, 'user_profile' getRenderData: -> context = super() - grav = @user.gravatarProfile - grav = grav.entry[0] if grav - addedContext = - user: @user - loadingProfile: @loadingProfile - myProfile: @user.id is context.me.id - grav: grav - photoURL: @user.getPhotoURL() - context[key] = addedContext[key] for key of addedContext + context.user = @user + context.myProfile = @user.id is context.me.id + context.marked = marked + context.moment = moment + context.iconForLink = @iconForLink + if links = @user.get('jobProfile')?.links + links = ($.extend(true, {}, link) for link in links) + link.icon = @iconForLink link for link in links + context.profileLinks = _.sortBy links, (link) -> not link.icon # icons first context + + afterRender: -> + super() + @updateProfileApproval() if me.isAdmin() + unless @user.get('jobProfile')?.projects?.length + @$el.find('.right-column').hide() + @$el.find('.middle-column').addClass('double-column') + + updateProfileApproval: -> + approved = @user.get 'jobProfileApproved' + @$el.find('.approved').toggle Boolean(approved) + @$el.find('.not-approved').toggle not approved + + toggleJobProfileApproved: -> + approved = not @user.get 'jobProfileApproved' + @user.set 'jobProfileApproved', approved + @user.save() + @updateProfileApproval() + + onJobProfileNotesChanged: (e) => + notes = @$el.find("#job-profile-notes").val() + @user.set 'jobProfileNotes', notes + @user.save() + + iconForLink: (link) -> + icons = [ + {icon: 'facebook', name: 'Facebook', domain: 'facebook.com', match: /facebook/i} + {icon: 'twitter', name: 'Twitter', domain: 'twitter.com', match: /twitter/i} + {icon: 'github', name: 'GitHub', domain: 'github.com', match: /github/i} + {icon: 'gplus', name: 'Google Plus', domain: 'plus.google.com', match: /(google|^g).?(\+|plus)/i} + {icon: 'linkedin', name: 'LinkedIn', domain: 'linkedin.com', match: /(google|^g).?(\+|plus)/i} + ] + for icon in icons + if (link.name.search(icon.match) isnt -1) or (link.link.search(icon.domain) isnt -1) + icon.url = "/images/pages/account/profile/icon_#{icon.icon}.png" + return icon + null + + onContactCandidate: (e) -> + @openModalView new JobProfileContactView recipientID: @user.id diff --git a/app/views/account/settings_view.coffee b/app/views/account/settings_view.coffee index df815b3cb..be4a79c59 100644 --- a/app/views/account/settings_view.coffee +++ b/app/views/account/settings_view.coffee @@ -5,6 +5,7 @@ forms = require('lib/forms') User = require('models/User') WizardSettingsView = require './wizard_settings_view' +JobProfileView = require './job_profile_view' module.exports = class SettingsView extends View id: 'account-settings-view' @@ -19,18 +20,7 @@ module.exports = class SettingsView extends View @save = _.debounce(@save, 200) super options return unless me - @listenTo(me, 'change', @refreshPicturePane) # depends on gravatar load @listenTo(me, 'invalid', (errors) -> forms.applyErrorsToForm(@$el, me.validationError)) - window.f = @getSubscriptions - - refreshPicturePane: -> - h = $(@template(@getRenderData())) - newPane = $('#picture-pane', h) - oldPane = $('#picture-pane') - active = oldPane.hasClass('active') - oldPane.replaceWith(newPane) - newPane.i18n() - newPane.addClass('active') if active afterRender: -> super() @@ -45,9 +35,19 @@ module.exports = class SettingsView extends View ) @chooseTab(location.hash.replace('#','')) - WizardSettingsView = new WizardSettingsView() - @listenTo(WizardSettingsView, 'change', @save) - @insertSubView WizardSettingsView + + wizardSettingsView = new WizardSettingsView() + @listenTo wizardSettingsView, 'change', @save + @insertSubView wizardSettingsView + + @jobProfileView = new JobProfileView() + @listenTo @jobProfileView, 'change', @save + @insertSubView @jobProfileView + + if me.schema().loaded + @buildPictureTreema() + else + @listenToOnce me, 'schema-loaded', @buildPictureTreema chooseTab: (category) -> id = "##{category}-pane" @@ -62,11 +62,9 @@ module.exports = class SettingsView extends View getRenderData: -> c = super() return c unless me - c.gravatarName = c.me?.gravatarName() - c.photos = me.gravatarPhotoURLs() - c.chosenPhoto = me.getPhotoURL() c.subs = {} c.subs[sub] = 1 for sub in c.me.get('emailSubscriptions') or ['announcement', 'notification', 'tester', 'level_creator', 'developer'] + c.showsJobProfileTab = me.isAdmin() or me.get('jobProfile') or location.hash.search('job-profile-') isnt -1 c getSubscriptions: -> @@ -81,6 +79,30 @@ module.exports = class SettingsView extends View $('#email-pane input[type="checkbox"]', @$el).prop('checked', not Boolean(subs.length)) @save() + buildPictureTreema: -> + data = photoURL: me.get('photoURL') + if data.photoURL?.search('gravatar') isnt -1 + # Old style + data.photoURL = null + schema = _.cloneDeep me.schema().attributes + schema.properties = _.pick me.schema().get('properties'), 'photoURL' + schema.required = ['photoURL'] + console.log 'schema is', schema + treemaOptions = + filePath: "db/user/#{me.id}" + schema: schema + data: data + callbacks: {change: @onPictureChanged} + + @pictureTreema = @$el.find('#picture-treema').treema treemaOptions + @pictureTreema.build() + @pictureTreema.open() + @$el.find('.gravatar-fallback').toggle not me.get 'photoURL' + + onPictureChanged: (e) => + @trigger 'change' + @$el.find('.gravatar-fallback').toggle not me.get 'photoURL' + save: -> forms.clearFormAlerts(@$el) @grabData() @@ -94,14 +116,14 @@ module.exports = class SettingsView extends View res = me.save() return unless res save = $('#save-button', @$el).text($.i18n.t('common.saving', defaultValue: 'Saving...')) - .addClass('btn-info').show().removeClass('btn-danger') + .removeClass('btn-danger').addClass('btn-success').show() res.error -> errors = JSON.parse(res.responseText) forms.applyErrorsToForm(@$el, errors) - save.text($.i18n.t('account_settings.error_saving', defaultValue: 'Error Saving')).removeClass('btn-info').addClass('btn-danger') + save.text($.i18n.t('account_settings.error_saving', defaultValue: 'Error Saving')).removeClass('btn-success').addClass('btn-danger', 500) res.success (model, response, options) -> - save.text($.i18n.t('account_settings.saved', defaultValue: 'Changes Saved')).removeClass('btn-info') + save.text($.i18n.t('account_settings.saved', defaultValue: 'Changes Saved')).removeClass('btn-success', 500) grabData: -> @grabPasswordData() @@ -120,12 +142,22 @@ module.exports = class SettingsView extends View me.set('password', password1) grabOtherData: -> - me.set('name', $('#name', @$el).val()) - me.set('email', $('#email', @$el).val()) - me.set('emailSubscriptions', @getSubscriptions()) + me.set 'name', $('#name', @$el).val() + me.set 'email', $('#email', @$el).val() + me.set 'emailSubscriptions', @getSubscriptions() + me.set 'photoURL', @pictureTreema.get('/photoURL') adminCheckbox = @$el.find('#admin') if adminCheckbox.length permissions = [] permissions.push 'admin' if adminCheckbox.prop('checked') me.set('permissions', permissions) + + jobProfile = me.get('jobProfile') ? {} + updated = false + for key, val of @jobProfileView.getData() + updated = updated or jobProfile[key] isnt val + jobProfile[key] = val + if updated + jobProfile.updated = (new Date()).toISOString() + me.set 'jobProfile', jobProfile diff --git a/app/views/admin/level_sessions_view.coffee b/app/views/admin/level_sessions_view.coffee index e66fefc2f..c00fcc2fe 100644 --- a/app/views/admin/level_sessions_view.coffee +++ b/app/views/admin/level_sessions_view.coffee @@ -16,12 +16,12 @@ module.exports = class LevelSessionsView extends View @getLevelSessions() getLevelSessions: -> - @sessions = new LevelSessionCollection + @sessions = new LevelSessionCollection() @sessions.fetch() - @listenTo(@sessions, 'all', @render) + @listenToOnce @sessions, 'all', @render getRenderData: => c = super() c.sessions = @sessions.models c.moment = moment - c \ No newline at end of file + c diff --git a/app/views/editor/level/home.coffee b/app/views/editor/level/home.coffee index ffb1a5ac9..247c9d3ff 100644 --- a/app/views/editor/level/home.coffee +++ b/app/views/editor/level/home.coffee @@ -1,8 +1,8 @@ SearchView = require 'views/kinds/SearchView' -module.exports = class ThangTypeHomeView extends SearchView +module.exports = class EditorSearchView extends SearchView id: "editor-level-home-view" modelLabel: 'Level' model: require 'models/Level' modelURL: '/db/level' - tableTemplate: require 'templates/editor/level/table' \ No newline at end of file + tableTemplate: require 'templates/editor/level/table' diff --git a/app/views/employers_view.coffee b/app/views/employers_view.coffee index d5e2eeb2a..a43bbf70a 100644 --- a/app/views/employers_view.coffee +++ b/app/views/employers_view.coffee @@ -1,6 +1,90 @@ View = require 'views/kinds/RootView' template = require 'templates/employers' +app = require 'application' +User = require 'models/User' +CocoCollection = require 'models/CocoCollection' +employerSignupTemplate = require 'templates/modal/employer_signup_modal' +ModalView = require 'views/kinds/ModalView' + +class CandidatesCollection extends CocoCollection + url: '/db/user/x/candidates' + model: User module.exports = class EmployersView extends View id: "employers-view" template: template + + events: + 'click tbody tr': 'onCandidateClicked' + + constructor: (options) -> + super options + @getCandidates() + + afterRender: -> + super() + @sortTable() if @candidates.models.length + + getRenderData: -> + c = super() + c.candidates = @candidates.models + c.moment = moment + c + + getCandidates: -> + @candidates = new CandidatesCollection() + @candidates.fetch() + # Re-render when we have fetched them, but don't wait and show a progress bar while loading. + @listenToOnce @candidates, 'all', @render + + sortTable: -> + # http://mottie.github.io/tablesorter/docs/example-widget-bootstrap-theme.html + $.extend $.tablesorter.themes.bootstrap, + # these classes are added to the table. To see other table classes available, + # look here: http://twitter.github.com/bootstrap/base-css.html#tables + table: "table table-bordered" + caption: "caption" + header: "bootstrap-header" # give the header a gradient background + footerRow: "" + footerCells: "" + icons: "" # add "icon-white" to make them white; this icon class is added to the in the header + sortNone: "bootstrap-icon-unsorted" + sortAsc: "icon-chevron-up" # glyphicon glyphicon-chevron-up" # we are still using v2 icons + sortDesc: "icon-chevron-down" # glyphicon-chevron-down" # we are still using v2 icons + active: "" # applied when column is sorted + hover: "" # use custom css here - bootstrap class may not override it + filterRow: "" # filter row class + even: "" # odd row zebra striping + odd: "" # even row zebra striping + + # call the tablesorter plugin and apply the uitheme widget + @$el.find(".tablesorter").tablesorter( + theme: "bootstrap" + widthFixed: true + headerTemplate: "{content} {icon}" + # widget code contained in the jquery.tablesorter.widgets.js file + # use the zebra stripe widget if you plan on hiding any rows (filter widget) + widgets: [ + "uitheme" + "zebra" + ] + widgetOptions: + # using the default zebra striping class name, so it actually isn't included in the theme variable above + # this is ONLY needed for bootstrap theming if you are using the filter widget, because rows are hidden + zebra: [ + "even" + "odd" + ] + # reset filters button + filter_reset: ".reset" + ) + + onCandidateClicked: (e) -> + id = $(e.target).closest('tr').data('candidate-id') + if id + url = "/account/profile/#{id}" + app.router.navigate url, {trigger: true} + else + employerSignupModal = new ModalView() + employerSignupModal.template = employerSignupTemplate + @openModalView employerSignupModal diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee index d8272d1b3..01f236154 100644 --- a/app/views/kinds/CocoView.coffee +++ b/app/views/kinds/CocoView.coffee @@ -104,15 +104,15 @@ module.exports = class CocoView extends Backbone.View context afterRender: -> - + # Resource and request loading management for any given view - + addResourceToLoad: (modelOrCollection, name, value=1) -> @loadProgress.resources.push {resource:modelOrCollection, value:value, name:name} @listenToOnce modelOrCollection, 'sync', @updateProgress @listenTo modelOrCollection, 'error', @onResourceLoadFailed @updateProgress() - + addRequestToLoad: (jqxhr, name, retryFunc, value=1) -> @loadProgress.requests.push {request:jqxhr, value:value, name: name, retryFunc: retryFunc} jqxhr.done @updateProgress @@ -152,7 +152,7 @@ module.exports = class CocoView extends Backbone.View num += r.value for r in @loadProgress.requests when r.request.status num += r.value for r in @loadProgress.somethings when r.loaded #console.log 'update progress', @, num, denom, arguments - + progress = if denom then num / denom else 0 # sometimes the denominator isn't known from the outset, so make sure the overall progress only goes up @loadProgress.progress = progress if progress > @loadProgress.progress @@ -160,7 +160,7 @@ module.exports = class CocoView extends Backbone.View if num is denom and not @loaded @loaded = true @onLoaded() - + updateProgressBar: => prog = "#{parseInt(@loadProgress.progress*100)}%" @$el.find('.loading-screen .progress-bar').css('width', prog) @@ -169,7 +169,7 @@ module.exports = class CocoView extends Backbone.View @render() # Error handling for loading - + onResourceLoadFailed: (resource, jqxhr) -> for r, index in @loadProgress.resources break if r.resource is resource @@ -179,12 +179,12 @@ module.exports = class CocoView extends Backbone.View resourceIndex: index, responseText: jqxhr.responseText })).i18n() - + onRetryResource: (e) -> r = @loadProgress.resources[$(e.target).data('resource-index')] r.resource.fetch() $(e.target).closest('.loading-error-alert').remove() - + onRequestLoadFailed: (jqxhr) => for r, index in @loadProgress.requests break if r.request is jqxhr @@ -194,7 +194,7 @@ module.exports = class CocoView extends Backbone.View requestIndex: index, responseText: jqxhr.responseText })) - + onRetryRequest: (e) -> r = @loadProgress.requests[$(e.target).data('request-index')] @[r.retryFunc]?() diff --git a/app/views/kinds/SearchView.coffee b/app/views/kinds/SearchView.coffee index f49eb4994..5f93924c3 100644 --- a/app/views/kinds/SearchView.coffee +++ b/app/views/kinds/SearchView.coffee @@ -8,7 +8,7 @@ class SearchCollection extends Backbone.Collection @url = "#{modelURL}/search?project=true" @url += "&term=#{term}" if @term -module.exports = class ThangTypeHomeView extends View +module.exports = class SearchView extends View template: template className: 'search-view' diff --git a/app/views/modal/contact_modal.coffee b/app/views/modal/contact_modal.coffee index dd3f0c40e..2e313d44a 100644 --- a/app/views/modal/contact_modal.coffee +++ b/app/views/modal/contact_modal.coffee @@ -6,16 +6,15 @@ forms = require 'lib/forms' contactSchema = additionalProperties: false + required: ['email', 'message'] properties: email: - required: true type: 'string' maxLength: 100 minLength: 1 format: 'email' message: - required: true type: 'string' minLength: 1 diff --git a/app/views/modal/job_profile_contact_modal.coffee b/app/views/modal/job_profile_contact_modal.coffee new file mode 100644 index 000000000..236301454 --- /dev/null +++ b/app/views/modal/job_profile_contact_modal.coffee @@ -0,0 +1,41 @@ +ContactView = require 'views/modal/contact_modal' +template = require 'templates/modal/job_profile_contact' + +forms = require 'lib/forms' +{sendContactMessage} = require 'lib/contact' + +contactSchema = + additionalProperties: false + required: ['email', 'message'] + properties: + email: + type: 'string' + maxLength: 100 + minLength: 1 + format: 'email' + + subject: + type: 'string' + minLength: 1 + + message: + type: 'string' + minLength: 1 + + recipientID: + type: 'string' + minLength: 1 + +module.exports = class JobProfileContactView extends ContactView + id: "job-profile-contact-modal" + template: template + + contact: -> + forms.clearFormAlerts @$el + contactMessage = forms.formToObject @$el + contactMessage.recipientID = @options.recipientID + res = tv4.validateMultiple contactMessage, contactSchema + return forms.applyErrorsToForm @$el, res.errors unless res.valid + contactMessage.message += '\n\n\n\n[CodeCombat says: please let us know if you end up accepting this job. Thanks!]' + window.tracker?.trackEvent 'Sent Job Profile Message', message: contactMessage + sendContactMessage contactMessage, @$el diff --git a/bower.json b/bower.json index 4580acd32..b5462543e 100644 --- a/bower.json +++ b/bower.json @@ -24,7 +24,7 @@ "test" ], "dependencies": { - "jquery": "~2.0.3", + "jquery": "~2.1.0", "lodash": "~2.4.1", "backbone": "1.1.0", "jquery-mousewheel": "~3.1.9", @@ -37,7 +37,10 @@ "firebase": "~1.0.2", "catiline": "~2.9.3", "d3": "~3.4.4", - "nanoscroller": "~0.8.0" + "nanoscroller": "~0.8.0", + "jquery.tablesorter": "~2.15.13", + "treema": "~0.0.1", + "bootstrap": "~3.1.1" }, "overrides": { "backbone": { @@ -51,6 +54,22 @@ }, "underscore.string": { "main": "lib/underscore.string.js" + }, + "jquery.tablesorter": { + "main": [ + "js/jquery.tablesorter.js", + "js/jquery.tablesorter.widgets.js", + "css/theme.bootstrap.css" + ] + }, + "bootstrap": { + "main": [ + "./dist/js/bootstrap.js", + "./dist/fonts/glyphicons-halflings-regular.eot", + "./dist/fonts/glyphicons-halflings-regular.svg", + "./dist/fonts/glyphicons-halflings-regular.ttf", + "./dist/fonts/glyphicons-halflings-regular.woff" + ] } } } diff --git a/config.coffee b/config.coffee index 10daa6b93..176701755 100644 --- a/config.coffee +++ b/config.coffee @@ -41,21 +41,11 @@ exports.config = 'test/javascripts/test-vendor.js': /^test[\/\\](?=vendor)/ order: before: [ - 'bower_components/jquery/jquery.js' + 'bower_components/jquery/dist/jquery.js' 'bower_components/lodash/dist/lodash.js' 'bower_components/backbone/backbone.js' # Twitter Bootstrap jquery plugins - 'vendor/scripts/bootstrap/transition.js' - 'vendor/scripts/bootstrap/affix.js' - 'vendor/scripts/bootstrap/alert.js' - 'vendor/scripts/bootstrap/button.js' - 'vendor/scripts/bootstrap/carousel.js' - 'vendor/scripts/bootstrap/collapse.js' - 'vendor/scripts/bootstrap/dropdown.js' - 'vendor/scripts/bootstrap/modal.js' - 'vendor/scripts/bootstrap/scrollspy.js' - 'vendor/scripts/bootstrap/tab.js' - 'vendor/scripts/bootstrap/tooltip.js' + 'bower_components/bootstrap/dist/bootstrap.js' # CreateJS dependencies 'vendor/scripts/easeljs-NEXT.combined.js' 'vendor/scripts/preloadjs-NEXT.combined.js' diff --git a/package.json b/package.json index 665ac0131..fc229cd02 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,6 @@ "css-brunch": "> 1.0 < 1.8", "jade-brunch": "> 1.0 < 1.8", "uglify-js-brunch": "~1.7.4", - "clean-css-brunch": "> 1.0 < 1.8", "auto-reload-brunch": "> 1.0 < 1.8", "brunch": "~1.7.4", "jasmine-node": "1.13.x", diff --git a/server/commons/schemas.coffee b/server/commons/schemas.coffee index 060ff8348..49d97bbfe 100644 --- a/server/commons/schemas.coffee +++ b/server/commons/schemas.coffee @@ -8,6 +8,8 @@ combine = (base, ext) -> return base unless ext? return _.extend(base, ext) +urlPattern = '^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-‌​\.\?\,\'\/\\\+&%\$#_=]*)?$' + # Common schema properties me.object = (ext, props) -> combine {type: 'object', additionalProperties: false, properties: props or {}}, ext me.array = (ext, items) -> combine {type: 'array', items: items or {}}, ext @@ -16,6 +18,7 @@ me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext) me.date = (ext) -> combine({type: 'string', format: 'date-time'}, ext) # should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient me.objectId = (ext) -> schema = combine({type: ['object', 'string'] }, ext) +me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext) PointSchema = me.object {title: "Point", description: "An {x, y} coordinate point.", format: "point2d", required: ["x", "y"]}, x: {title: "x", description: "The x coordinate.", type: "number", "default": 15} diff --git a/server/routes/contact.coffee b/server/routes/contact.coffee index 51aaa78fc..1ceb8d196 100644 --- a/server/routes/contact.coffee +++ b/server/routes/contact.coffee @@ -1,26 +1,38 @@ config = require '../../server_config' log = require 'winston' mail = require '../commons/mail' +User = require '../users/User' module.exports.setup = (app) -> app.post '/contact', (req, res) -> return res.end() unless req.user log.info "Sending mail from #{req.body.email} saying #{req.body.message}" if config.isProduction - options = createMailOptions req.body.email, req.body.message, req.user - mail.transport.sendMail options, (error, response) -> - if error - log.error "Error sending mail: #{error.message or error}" - else - log.info "Mail sent successfully. Response: #{response.message}" + createMailOptions req.body.email, req.body.message, req.user, req.body.recipientID, req.body.subject, (options) -> + mail.transport.sendMail options, (error, response) -> + if error + log.error "Error sending mail: #{error.message or error}" + else + log.info "Mail sent successfully. Response: #{response.message}" return res.end() -createMailOptions = (sender, message, user) -> +createMailOptions = (sender, message, user, recipientID, subject, done) -> # TODO: use email templates here options = from: config.mail.username to: config.mail.username replyTo: sender - subject: "[CodeCombat] Feedback - #{sender}" + subject: "[CodeCombat] #{subject ? ('Feedback - ' + sender)}" text: "#{message}\n\nUsername: #{user.get('name') or 'Anonymous'}\nID: #{user._id}" - #html: message.replace '\n', '
\n' \ No newline at end of file + #html: message.replace '\n', '
\n' + + if recipientID and (user.isAdmin() or ('employer' in (user.permissions ? []))) + User.findById(recipientID, 'email').exec (err, document) -> + if err + log.error "Error looking up recipient to email from #{recipientID}: #{err}" if err + else + options.bcc = options.to + options.to = document.get('email') + done options + else + done options diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index 168f10d91..cd78dad9e 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -9,12 +9,16 @@ errors = require '../commons/errors' async = require 'async' log = require 'winston' LevelSession = require('../levels/sessions/LevelSession') +LevelSessionHandler = require '../levels/sessions/level_session_handler' serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset'] privateProperties = [ 'permissions', 'email', 'firstName', 'lastName', 'gender', 'facebookID', 'gplusID', 'music', 'volume', 'aceConfig' ] +candidateProperties = [ + 'jobProfile', 'jobProfileApproved', 'jobProfileNotes' +] UserHandler = class UserHandler extends Handler modelClass: User @@ -23,7 +27,7 @@ UserHandler = class UserHandler extends Handler 'name', 'photoURL', 'password', 'anonymous', 'wizardColor1', 'volume', 'firstName', 'lastName', 'gender', 'facebookID', 'gplusID', 'emailSubscriptions', 'testGroupNumber', 'music', 'hourOfCode', 'hourOfCodeComplete', 'preferredLanguage', - 'wizard', 'aceConfig', 'autocastDelay', 'lastLevel' + 'wizard', 'aceConfig', 'autocastDelay', 'lastLevel', 'jobProfile' ] jsonSchema: schema @@ -32,21 +36,19 @@ UserHandler = class UserHandler extends Handler super(arguments...) @editableProperties.push('permissions') unless config.isProduction + getEditableProperties: (req, document) -> + props = super req, document + props.push 'jobProfileApproved', 'jobProfileNotes' if req.user.isAdmin() + props + formatEntity: (req, document) -> return null unless document? obj = document.toObject() delete obj[prop] for prop in serverProperties - includePrivates = req.user and (req.user?.isAdmin() or req.user?._id.equals(document._id)) + includePrivates = req.user and (req.user.isAdmin() or req.user._id.equals(document._id)) delete obj[prop] for prop in privateProperties unless includePrivates - - # emailHash is used by gravatar - hash = crypto.createHash('md5') - if document.get('email') - hash.update(_.trim(document.get('email')).toLowerCase()) - else - hash.update(@_id+'') - obj.emailHash = hash.digest('hex') - + includeCandidate = includePrivates or (obj.jobProfileApproved and req.user and ('employer' in (req.user.permissions ? []))) + delete obj[prop] for prop in candidateProperties unless includeCandidate return obj waterfallFunctions: [ @@ -115,7 +117,7 @@ UserHandler = class UserHandler extends Handler getById: (req, res, id) -> if req.user?._id.equals(id) - return @sendSuccess(res, @formatEntity(req, req.user)) + return @sendSuccess(res, @formatEntity(req, req.user, 256)) super(req, res, id) getNamesByIds: (req, res) -> @@ -171,6 +173,7 @@ UserHandler = class UserHandler extends Handler return @getNamesByIds(req, res) if args[1] is 'names' return @nameToID(req, res, args[0]) if args[1] is 'nameToID' return @getLevelSessions(req, res, args[0]) if args[1] is 'level.sessions' + return @getCandidates(req, res) if args[1] is 'candidates' return @sendNotFoundError(res) agreeToCLA: (req, res) -> @@ -191,9 +194,11 @@ UserHandler = class UserHandler extends Handler @sendSuccess(res, {result:'success'}) avatar: (req, res, id) -> - @modelClass.findById(id).exec (err, document) -> + @modelClass.findById(id).exec (err, document) => return @sendDatabaseError(res, err) if err - res.redirect(document?.get('photoURL') or '/images/generic-wizard-icon.png') + photoURL = document?.get('photoURL') + photoURL ||= @buildGravatarURL document + res.redirect photoURL res.end() getLevelSessions: (req, res, userID) -> @@ -205,8 +210,46 @@ UserHandler = class UserHandler extends Handler projection[field] = 1 for field in req.query.project.split(',') LevelSession.find(query).select(projection).exec (err, documents) => return @sendDatabaseError(res, err) if err - documents = (@formatEntity(req, doc) for doc in documents) + documents = (LevelSessionHandler.formatEntity(req, doc) for doc in documents) @sendSuccess(res, documents) + getCandidates: (req, res) -> + authorized = req.user.isAdmin() or ('employer' in req.user.get('permissions')) + since = (new Date((new Date()) - 2 * 30.4 * 86400 * 1000)).toISOString() + #query = {'jobProfileApproved': true, 'jobProfile.active': true, 'jobProfile.updated': {$gt: since}} + query = {'jobProfile.active': true, 'jobProfile.updated': {$gt: since}} # testing + query.jobProfileApproved = true unless req.user.isAdmin() + selection = 'jobProfile' + selection += ' email' if authorized + selection += ' jobProfileApproved' if req.user.isAdmin() + User.find(query).select(selection).exec (err, documents) => + return @sendDatabaseError(res, err) if err + candidates = (@formatCandidate(authorized, doc) for doc in documents) + @sendSuccess(res, candidates) + + formatCandidate: (authorized, document) -> + fields = if authorized then ['jobProfile', 'jobProfileApproved', 'photoURL', '_id'] else ['jobProfile'] + obj = _.pick document.toObject(), fields + obj.photoURL ||= obj.jobProfile.photoURL if authorized + obj.photoURL ||= @buildGravatarURL document if authorized + subfields = ['country', 'city', 'lookingFor', 'skills', 'experience', 'updated'] + if authorized + subfields = subfields.concat ['name', 'work'] + obj.jobProfile = _.pick obj.jobProfile, subfields + obj + + buildGravatarURL: (user) -> + emailHash = @buildEmailHash user + defaultAvatar = "http://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png" + "https://www.gravatar.com/avatar/#{emailHash}?default=#{defaultAvatar}" + + buildEmailHash: (user) -> + # emailHash is used by gravatar + hash = crypto.createHash('md5') + if user.get('email') + hash.update(_.trim(user.get('email')).toLowerCase()) + else + hash.update(user.get('_id') + '') + hash.digest('hex') module.exports = new UserHandler() diff --git a/server/users/user_schema.coffee b/server/users/user_schema.coffee index 18d526de5..c7de194e3 100644 --- a/server/users/user_schema.coffee +++ b/server/users/user_schema.coffee @@ -9,7 +9,7 @@ UserSchema = c.object {}, gender: {type: 'string', 'enum': ['male', 'female']} password: {type: 'string', maxLength: 256, minLength: 2, title:'Password'} passwordReset: {type: 'string'} - photoURL: {type: 'string', format: 'url', required: false} + photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image to serve as your profile picture.'} facebookID: c.shortString({title: 'Facebook ID'}) gplusID: c.shortString({title: 'G+ ID'}) @@ -36,7 +36,6 @@ UserSchema = c.object {}, passwordHash: {type: 'string', maxLength: 256} # client side - #gravatarProfile: {} (should only ever be kept locally) emailHash: {type: 'string'} #Internationalization stuff @@ -56,6 +55,44 @@ UserSchema = c.object {}, simulatedBy: {type: 'integer', minimum: 0, default: 0} simulatedFor: {type: 'integer', minimum: 0, default: 0} + jobProfile: c.object {title: 'Job Profile', required: ['lookingFor', 'jobTitle', 'active', 'name', 'city', 'country', 'skills', 'experience', 'shortDescription', 'longDescription', 'visa', 'work', 'education', 'projects', 'links']}, + lookingFor: {title: 'Looking For', type: 'string', enum: ['Full-time', 'Part-time', 'Remote', 'Contracting', 'Internship'], default: 'Full-time', description: 'What kind of developer position do you want?'} + jobTitle: {type: 'string', maxLength: 50, title: 'Desired Job Title', description: 'What role are you looking for? Ex.: "Full Stack Engineer", "Front-End Developer", "iOS Developer"', default: 'Software Developer'} + active: {title: 'Active', type: 'boolean', description: 'Want interview offers right now?'} + updated: c.date {title: 'Last Updated', description: 'How fresh your profile appears to employers. The fresher, the better. Profiles go inactive after 30 days.'} + name: c.shortString {title: 'Name', description: 'Name you want employers to see, like "Nick Winter".'} + city: c.shortString {title: 'City', description: 'City you want to work in (or live in now), like "San Francisco" or "Lubbock, TX".', default: 'Defaultsville, CA', format: 'city'} + country: c.shortString {title: 'Country', description: 'Country you want to work in (or live in now), like "USA" or "France".', default: 'USA', format: 'country'} + skills: c.array {title: 'Skills', description: 'Tag relevant developer skills in order of proficiency. Employers will see the first five at a glance.', default: ['javascript'], minItems: 1, maxItems: 30, uniqueItems: true}, + {type: 'string', minLength: 1, maxLength: 20, description: 'Ex.: "objective-c", "mongodb", "rails", "android", "javascript"', format: 'skill'} + experience: {type: 'integer', title: 'Years of Experience', minimum: 0, description: 'How many years of professional experience (getting paid) developing software do you have?'} + shortDescription: {type: 'string', maxLength: 140, title: 'Short Description', description: 'Who are you, and what are you looking for? 140 characters max.', default: 'Programmer seeking to build great software.'} + longDescription: {type: 'string', maxLength: 600, title: 'Description', description: 'Describe yourself to potential employers. Keep it short and to the point. We recommend outlining the position that would most interest you. Tasteful markdown okay; 600 characters max.', format: 'markdown', default: '* I write great code.\n* You need great code?\n* Great!'} + visa: c.shortString {title: 'US Work Status', description: 'Are you authorized to work in the US, or do you need visa sponsorship?', enum: ['Authorized to work in the US', 'Need visa sponsorship'], default: 'Authorized to work in the US'} + work: c.array {title: 'Work Experience', description: 'List your relevant work experience, most recent first.'}, + c.object {title: 'Job', description: 'Some work experience you had.', required: ['employer', 'role', 'duration']}, + employer: c.shortString {title: 'Employer', description: 'Name of your employer.'} + role: c.shortString {title: 'Job Title', description: 'What was your job title or role?'} + duration: c.shortString {title: 'Duration', description: 'When did you hold this gig? Ex.: "Feb 2013 - present".'} + education: c.array {title: 'Education', description: 'List your academic ordeals.'}, + c.object {title: 'Ordeal', description: 'Some education that befell you.', required: ['school', 'degree', 'duration']}, + school: c.shortString {title: 'School', description: 'Name of your school.'} + degree: c.shortString {title: 'Degree', description: 'What was your degree and field of study? Ex. Ph.D. Human-Computer Interaction (incomplete)'} + duration: c.shortString {title: 'Dates', description: 'When? Ex.: "Aug 2004 - May 2008".'} + projects: c.array {title: 'Projects', description: 'Highlight your projects to amaze employers.'}, + c.object {title: 'Project', description: 'A project you created.', required: ['name', 'description', 'picture'], default: {name: 'My Project', description: 'A project I worked on.', link: 'http://example.com', picture: ''}}, + name: c.shortString {title: 'Project Name', description: 'What was the project called?', default: 'My Project'} + description: {type: 'string', title: 'Description', description: 'Briefly describe the project.', maxLength: 400, default: 'A project I worked on.', format: 'markdown'} + picture: {type: 'string', title: 'Picture', format: 'image-file', description: 'Upload a 230x115px or larger image showing off the project.'} + link: c.url {title: 'Link', description: 'Link to the project.', default: 'http://example.com'} + links: c.array {title: 'Personal and Social Links', description: 'Link any other sites or profiles you want to highlight, like your GitHub, your LinkedIn, or your blog.'}, + c.object {title: 'Link', description: 'A link to another site you want to highlight, like your GitHub, your LinkedIn, or your blog.', required: ['name', 'link']}, + name: {type: 'string', maxLength: 30, title: 'Link Name', description: 'What are you linking to? Ex: "Personal Website", "Twitter"', format: 'link-name'} + link: c.url {title: 'Link', description: 'The URL.', default: 'http://example.com'} + photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image if you want to show a different profile picture to employers than your normal avatar.'} + + jobProfileApproved: {title: 'Job Profile Approved', type: 'boolean', description: 'Whether your profile has been approved by CodeCombat.'} + jobProfileNotes: {type: 'string', maxLength: 1000, title: 'Our Notes', description: "CodeCombat's notes on the candidate.", format: 'markdown', default: ''} c.extendBasicProperties UserSchema, 'user' module.exports = UserSchema diff --git a/server_setup.coffee b/server_setup.coffee index c06482a85..d8dcd1c03 100644 --- a/server_setup.coffee +++ b/server_setup.coffee @@ -3,6 +3,7 @@ path = require 'path' authentication = require 'passport' useragent = require 'express-useragent' fs = require 'graceful-fs' +log = require('winston') database = require './server/commons/database' baseRoute = require './server/routes/base' @@ -96,7 +97,8 @@ setupFallbackRouteToIndex = (app) -> auth.loginUser(req, res, user, false, next) sendMain = (req, res) -> - fs.readFile path.join(__dirname, 'public', 'main.html'), 'utf8', (err,data) -> + fs.readFile path.join(__dirname, 'public', 'main.html'), 'utf8', (err, data) -> + log.error "Error modifying main.html: #{err}" if err # insert the user object directly into the html so the application can have it immediately data = data.replace('"userObjectTag"', JSON.stringify(UserHandler.formatEntity(req, req.user))) res.send data diff --git a/vendor/scripts/bootstrap/affix.js b/vendor/scripts/bootstrap/affix.js deleted file mode 100644 index 552bffa3f..000000000 --- a/vendor/scripts/bootstrap/affix.js +++ /dev/null @@ -1,126 +0,0 @@ -/* ======================================================================== - * Bootstrap: affix.js v3.0.3 - * http://getbootstrap.com/javascript/#affix - * ======================================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================================== */ - - -+function ($) { "use strict"; - - // AFFIX CLASS DEFINITION - // ====================== - - var Affix = function (element, options) { - this.options = $.extend({}, Affix.DEFAULTS, options) - this.$window = $(window) - .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) - .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) - - this.$element = $(element) - this.affixed = - this.unpin = null - - this.checkPosition() - } - - Affix.RESET = 'affix affix-top affix-bottom' - - Affix.DEFAULTS = { - offset: 0 - } - - Affix.prototype.checkPositionWithEventLoop = function () { - setTimeout($.proxy(this.checkPosition, this), 1) - } - - Affix.prototype.checkPosition = function () { - if (!this.$element.is(':visible')) return - - var scrollHeight = $(document).height() - var scrollTop = this.$window.scrollTop() - var position = this.$element.offset() - var offset = this.options.offset - var offsetTop = offset.top - var offsetBottom = offset.bottom - - if (typeof offset != 'object') offsetBottom = offsetTop = offset - if (typeof offsetTop == 'function') offsetTop = offset.top() - if (typeof offsetBottom == 'function') offsetBottom = offset.bottom() - - var affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? false : - offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? 'bottom' : - offsetTop != null && (scrollTop <= offsetTop) ? 'top' : false - - if (this.affixed === affix) return - if (this.unpin) this.$element.css('top', '') - - this.affixed = affix - this.unpin = affix == 'bottom' ? position.top - scrollTop : null - - this.$element.removeClass(Affix.RESET).addClass('affix' + (affix ? '-' + affix : '')) - - if (affix == 'bottom') { - this.$element.offset({ top: document.body.offsetHeight - offsetBottom - this.$element.height() }) - } - } - - - // AFFIX PLUGIN DEFINITION - // ======================= - - var old = $.fn.affix - - $.fn.affix = function (option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.affix') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.affix', (data = new Affix(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.affix.Constructor = Affix - - - // AFFIX NO CONFLICT - // ================= - - $.fn.affix.noConflict = function () { - $.fn.affix = old - return this - } - - - // AFFIX DATA-API - // ============== - - $(window).on('load', function () { - $('[data-spy="affix"]').each(function () { - var $spy = $(this) - var data = $spy.data() - - data.offset = data.offset || {} - - if (data.offsetBottom) data.offset.bottom = data.offsetBottom - if (data.offsetTop) data.offset.top = data.offsetTop - - $spy.affix(data) - }) - }) - -}(jQuery); diff --git a/vendor/scripts/bootstrap/alert.js b/vendor/scripts/bootstrap/alert.js deleted file mode 100644 index 695ad74d0..000000000 --- a/vendor/scripts/bootstrap/alert.js +++ /dev/null @@ -1,98 +0,0 @@ -/* ======================================================================== - * Bootstrap: alert.js v3.0.3 - * http://getbootstrap.com/javascript/#alerts - * ======================================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================================== */ - - -+function ($) { "use strict"; - - // ALERT CLASS DEFINITION - // ====================== - - var dismiss = '[data-dismiss="alert"]' - var Alert = function (el) { - $(el).on('click', dismiss, this.close) - } - - Alert.prototype.close = function (e) { - var $this = $(this) - var selector = $this.attr('data-target') - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } - - var $parent = $(selector) - - if (e) e.preventDefault() - - if (!$parent.length) { - $parent = $this.hasClass('alert') ? $this : $this.parent() - } - - $parent.trigger(e = $.Event('close.bs.alert')) - - if (e.isDefaultPrevented()) return - - $parent.removeClass('in') - - function removeElement() { - $parent.trigger('closed.bs.alert').remove() - } - - $.support.transition && $parent.hasClass('fade') ? - $parent - .one($.support.transition.end, removeElement) - .emulateTransitionEnd(150) : - removeElement() - } - - - // ALERT PLUGIN DEFINITION - // ======================= - - var old = $.fn.alert - - $.fn.alert = function (option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.alert') - - if (!data) $this.data('bs.alert', (data = new Alert(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - $.fn.alert.Constructor = Alert - - - // ALERT NO CONFLICT - // ================= - - $.fn.alert.noConflict = function () { - $.fn.alert = old - return this - } - - - // ALERT DATA-API - // ============== - - $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) - -}(jQuery); diff --git a/vendor/scripts/bootstrap/bootstrap.js b/vendor/scripts/bootstrap/bootstrap.js deleted file mode 100644 index fee1bda9d..000000000 --- a/vendor/scripts/bootstrap/bootstrap.js +++ /dev/null @@ -1,12 +0,0 @@ -//= require affix -//= require alert -//= require button -//= require carousel -//= require collapse -//= require dropdown -//= require tab -//= require transition -//= require scrollspy -//= require modal -//= require tooltip -//= require popover diff --git a/vendor/scripts/bootstrap/button.js b/vendor/scripts/bootstrap/button.js deleted file mode 100644 index c9fdde5e4..000000000 --- a/vendor/scripts/bootstrap/button.js +++ /dev/null @@ -1,115 +0,0 @@ -/* ======================================================================== - * Bootstrap: button.js v3.0.3 - * http://getbootstrap.com/javascript/#buttons - * ======================================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================================== */ - - -+function ($) { "use strict"; - - // BUTTON PUBLIC CLASS DEFINITION - // ============================== - - var Button = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, Button.DEFAULTS, options) - } - - Button.DEFAULTS = { - loadingText: 'loading...' - } - - Button.prototype.setState = function (state) { - var d = 'disabled' - var $el = this.$element - var val = $el.is('input') ? 'val' : 'html' - var data = $el.data() - - state = state + 'Text' - - if (!data.resetText) $el.data('resetText', $el[val]()) - - $el[val](data[state] || this.options[state]) - - // push to event loop to allow forms to submit - setTimeout(function () { - state == 'loadingText' ? - $el.addClass(d).attr(d, d) : - $el.removeClass(d).removeAttr(d); - }, 0) - } - - Button.prototype.toggle = function () { - var $parent = this.$element.closest('[data-toggle="buttons"]') - var changed = true - - if ($parent.length) { - var $input = this.$element.find('input') - if ($input.prop('type') === 'radio') { - // see if clicking on current one - if ($input.prop('checked') && this.$element.hasClass('active')) - changed = false - else - $parent.find('.active').removeClass('active') - } - if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change') - } - - if (changed) this.$element.toggleClass('active') - } - - - // BUTTON PLUGIN DEFINITION - // ======================== - - var old = $.fn.button - - $.fn.button = function (option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.button') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.button', (data = new Button(this, options))) - - if (option == 'toggle') data.toggle() - else if (option) data.setState(option) - }) - } - - $.fn.button.Constructor = Button - - - // BUTTON NO CONFLICT - // ================== - - $.fn.button.noConflict = function () { - $.fn.button = old - return this - } - - - // BUTTON DATA-API - // =============== - - $(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) { - var $btn = $(e.target) - if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') - $btn.button('toggle') - e.preventDefault() - }) - -}(jQuery); diff --git a/vendor/scripts/bootstrap/carousel.js b/vendor/scripts/bootstrap/carousel.js deleted file mode 100644 index 6391a36df..000000000 --- a/vendor/scripts/bootstrap/carousel.js +++ /dev/null @@ -1,217 +0,0 @@ -/* ======================================================================== - * Bootstrap: carousel.js v3.0.3 - * http://getbootstrap.com/javascript/#carousel - * ======================================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================================== */ - - -+function ($) { "use strict"; - - // CAROUSEL CLASS DEFINITION - // ========================= - - var Carousel = function (element, options) { - this.$element = $(element) - this.$indicators = this.$element.find('.carousel-indicators') - this.options = options - this.paused = - this.sliding = - this.interval = - this.$active = - this.$items = null - - this.options.pause == 'hover' && this.$element - .on('mouseenter', $.proxy(this.pause, this)) - .on('mouseleave', $.proxy(this.cycle, this)) - } - - Carousel.DEFAULTS = { - interval: 5000 - , pause: 'hover' - , wrap: true - } - - Carousel.prototype.cycle = function (e) { - e || (this.paused = false) - - this.interval && clearInterval(this.interval) - - this.options.interval - && !this.paused - && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) - - return this - } - - Carousel.prototype.getActiveIndex = function () { - this.$active = this.$element.find('.item.active') - this.$items = this.$active.parent().children() - - return this.$items.index(this.$active) - } - - Carousel.prototype.to = function (pos) { - var that = this - var activeIndex = this.getActiveIndex() - - if (pos > (this.$items.length - 1) || pos < 0) return - - if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) - if (activeIndex == pos) return this.pause().cycle() - - return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) - } - - Carousel.prototype.pause = function (e) { - e || (this.paused = true) - - if (this.$element.find('.next, .prev').length && $.support.transition.end) { - this.$element.trigger($.support.transition.end) - this.cycle(true) - } - - this.interval = clearInterval(this.interval) - - return this - } - - Carousel.prototype.next = function () { - if (this.sliding) return - return this.slide('next') - } - - Carousel.prototype.prev = function () { - if (this.sliding) return - return this.slide('prev') - } - - Carousel.prototype.slide = function (type, next) { - var $active = this.$element.find('.item.active') - var $next = next || $active[type]() - var isCycling = this.interval - var direction = type == 'next' ? 'left' : 'right' - var fallback = type == 'next' ? 'first' : 'last' - var that = this - - if (!$next.length) { - if (!this.options.wrap) return - $next = this.$element.find('.item')[fallback]() - } - - this.sliding = true - - isCycling && this.pause() - - var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction }) - - if ($next.hasClass('active')) return - - if (this.$indicators.length) { - this.$indicators.find('.active').removeClass('active') - this.$element.one('slid.bs.carousel', function () { - var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) - $nextIndicator && $nextIndicator.addClass('active') - }) - } - - if ($.support.transition && this.$element.hasClass('slide')) { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $next.addClass(type) - $next[0].offsetWidth // force reflow - $active.addClass(direction) - $next.addClass(direction) - $active - .one($.support.transition.end, function () { - $next.removeClass([type, direction].join(' ')).addClass('active') - $active.removeClass(['active', direction].join(' ')) - that.sliding = false - setTimeout(function () { that.$element.trigger('slid.bs.carousel') }, 0) - }) - .emulateTransitionEnd(600) - } else { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $active.removeClass('active') - $next.addClass('active') - this.sliding = false - this.$element.trigger('slid.bs.carousel') - } - - isCycling && this.cycle() - - return this - } - - - // CAROUSEL PLUGIN DEFINITION - // ========================== - - var old = $.fn.carousel - - $.fn.carousel = function (option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.carousel') - var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) - var action = typeof option == 'string' ? option : options.slide - - if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) - if (typeof option == 'number') data.to(option) - else if (action) data[action]() - else if (options.interval) data.pause().cycle() - }) - } - - $.fn.carousel.Constructor = Carousel - - - // CAROUSEL NO CONFLICT - // ==================== - - $.fn.carousel.noConflict = function () { - $.fn.carousel = old - return this - } - - - // CAROUSEL DATA-API - // ================= - - $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { - var $this = $(this), href - var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 - var options = $.extend({}, $target.data(), $this.data()) - var slideIndex = $this.attr('data-slide-to') - if (slideIndex) options.interval = false - - $target.carousel(options) - - if (slideIndex = $this.attr('data-slide-to')) { - $target.data('bs.carousel').to(slideIndex) - } - - e.preventDefault() - }) - - $(window).on('load', function () { - $('[data-ride="carousel"]').each(function () { - var $carousel = $(this) - $carousel.carousel($carousel.data()) - }) - }) - -}(jQuery); diff --git a/vendor/scripts/bootstrap/collapse.js b/vendor/scripts/bootstrap/collapse.js deleted file mode 100644 index 1a079938e..000000000 --- a/vendor/scripts/bootstrap/collapse.js +++ /dev/null @@ -1,179 +0,0 @@ -/* ======================================================================== - * Bootstrap: collapse.js v3.0.3 - * http://getbootstrap.com/javascript/#collapse - * ======================================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================================== */ - - -+function ($) { "use strict"; - - // COLLAPSE PUBLIC CLASS DEFINITION - // ================================ - - var Collapse = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, Collapse.DEFAULTS, options) - this.transitioning = null - - if (this.options.parent) this.$parent = $(this.options.parent) - if (this.options.toggle) this.toggle() - } - - Collapse.DEFAULTS = { - toggle: true - } - - Collapse.prototype.dimension = function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' - } - - Collapse.prototype.show = function () { - if (this.transitioning || this.$element.hasClass('in')) return - - var startEvent = $.Event('show.bs.collapse') - this.$element.trigger(startEvent) - if (startEvent.isDefaultPrevented()) return - - var actives = this.$parent && this.$parent.find('> .panel > .in') - - if (actives && actives.length) { - var hasData = actives.data('bs.collapse') - if (hasData && hasData.transitioning) return - actives.collapse('hide') - hasData || actives.data('bs.collapse', null) - } - - var dimension = this.dimension() - - this.$element - .removeClass('collapse') - .addClass('collapsing') - [dimension](0) - - this.transitioning = 1 - - var complete = function () { - this.$element - .removeClass('collapsing') - .addClass('in') - [dimension]('auto') - this.transitioning = 0 - this.$element.trigger('shown.bs.collapse') - } - - if (!$.support.transition) return complete.call(this) - - var scrollSize = $.camelCase(['scroll', dimension].join('-')) - - this.$element - .one($.support.transition.end, $.proxy(complete, this)) - .emulateTransitionEnd(350) - [dimension](this.$element[0][scrollSize]) - } - - Collapse.prototype.hide = function () { - if (this.transitioning || !this.$element.hasClass('in')) return - - var startEvent = $.Event('hide.bs.collapse') - this.$element.trigger(startEvent) - if (startEvent.isDefaultPrevented()) return - - var dimension = this.dimension() - - this.$element - [dimension](this.$element[dimension]()) - [0].offsetHeight - - this.$element - .addClass('collapsing') - .removeClass('collapse') - .removeClass('in') - - this.transitioning = 1 - - var complete = function () { - this.transitioning = 0 - this.$element - .trigger('hidden.bs.collapse') - .removeClass('collapsing') - .addClass('collapse') - } - - if (!$.support.transition) return complete.call(this) - - this.$element - [dimension](0) - .one($.support.transition.end, $.proxy(complete, this)) - .emulateTransitionEnd(350) - } - - Collapse.prototype.toggle = function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() - } - - - // COLLAPSE PLUGIN DEFINITION - // ========================== - - var old = $.fn.collapse - - $.fn.collapse = function (option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.collapse') - var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) - - if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.collapse.Constructor = Collapse - - - // COLLAPSE NO CONFLICT - // ==================== - - $.fn.collapse.noConflict = function () { - $.fn.collapse = old - return this - } - - - // COLLAPSE DATA-API - // ================= - - $(document).on('click.bs.collapse.data-api', '[data-toggle=collapse]', function (e) { - var $this = $(this), href - var target = $this.attr('data-target') - || e.preventDefault() - || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 - var $target = $(target) - var data = $target.data('bs.collapse') - var option = data ? 'toggle' : $this.data() - var parent = $this.attr('data-parent') - var $parent = parent && $(parent) - - if (!data || !data.transitioning) { - if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed') - $this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed') - } - - $target.collapse(option) - }) - -}(jQuery); diff --git a/vendor/scripts/bootstrap/dropdown.js b/vendor/scripts/bootstrap/dropdown.js deleted file mode 100644 index 13352ef7c..000000000 --- a/vendor/scripts/bootstrap/dropdown.js +++ /dev/null @@ -1,154 +0,0 @@ -/* ======================================================================== - * Bootstrap: dropdown.js v3.0.3 - * http://getbootstrap.com/javascript/#dropdowns - * ======================================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================================== */ - - -+function ($) { "use strict"; - - // DROPDOWN CLASS DEFINITION - // ========================= - - var backdrop = '.dropdown-backdrop' - var toggle = '[data-toggle=dropdown]' - var Dropdown = function (element) { - $(element).on('click.bs.dropdown', this.toggle) - } - - Dropdown.prototype.toggle = function (e) { - var $this = $(this) - - if ($this.is('.disabled, :disabled')) return - - var $parent = getParent($this) - var isActive = $parent.hasClass('open') - - clearMenus() - - if (!isActive) { - if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { - // if mobile we use a backdrop because click events don't delegate - $('