diff --git a/app/locale/en.coffee b/app/locale/en.coffee index ed625d57e..878181026 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -186,6 +186,7 @@ account_profile: edit_settings: "Edit Settings" + done_editing_settings: "Done Editing" profile_for_prefix: "Profile for " profile_for_suffix: "" approved: "Approved" diff --git a/app/models/ThangType.coffee b/app/models/ThangType.coffee index 84fd8e713..b236ba9cd 100644 --- a/app/models/ThangType.coffee +++ b/app/models/ThangType.coffee @@ -16,6 +16,13 @@ module.exports = class ThangType extends CocoModel @on 'sync', @setDefaults @spriteSheets = {} + ## Testing memory clearing + #f = => + # console.info 'resetting raw data' + # @unset 'raw' + # @_previousAttributes.raw = null + #setTimeout f, 40000 + setDefaults: -> @resetRawData() unless @get('raw') diff --git a/app/styles/account/profile.sass b/app/styles/account/profile.sass index 59807aa1c..fdc81e88a 100644 --- a/app/styles/account/profile.sass +++ b/app/styles/account/profile.sass @@ -1,4 +1,8 @@ +@import "app/styles/bootstrap/variables" + #profile-view + $sideBackground: rgb(220, 220, 220) + .profile-control-bar background-color: rgb(78, 78, 78) width: 100% @@ -14,6 +18,7 @@ .main-content-area padding: 0 + background-color: white .flat-button width: 100% @@ -68,7 +73,7 @@ .left-column width: $side-width - 2 * $side-padding padding: $side-padding - background-color: rgb(220, 220, 220) + background-color: $sideBackground .sub-column width: $side-width - 2 * $side-padding @@ -81,7 +86,7 @@ img.profile-photo width: $side-width - 2 * $side-padding border-radius: 6px - + .profile-caption background-color: rgba(0, 0, 0, 0.5) color: white @@ -128,7 +133,7 @@ overflow-wrap: break-word code - background-color: rgb(220, 220, 220) + background-color: $sideBackground color: #555 margin: 2px 0 display: inline-block @@ -161,7 +166,7 @@ .right-column width: $side-width - background-color: rgb(220, 220, 220) + background-color: $sideBackground .sub-column width: $side-width - 2 * $side-padding @@ -176,7 +181,7 @@ li margin-bottom: 10px padding: 5px 3px - border: 2px solid rgb(220, 220, 220) + border: 2px solid $sideBackground transition: .5s ease-in-out position: relative background-color: white @@ -218,3 +223,54 @@ -moz-filter: grayscale(0%) -o-filter: grayscale(0%) filter: grayscale(0%) + + .main-content-area + + .job-profile-container + .editable-section + position: relative + + .editable-form + display: none + background-color: white + padding: 5px 5px 5px 5px + + .editable-icon + display: none + + .job-profile-container.editable-profile + + .full-height-column.deemphasized + background-color: $sideBackground + + .saving + opacity: 0.75 + + .editable-thinner + padding-right: 30px + + .editable-icon + display: block + position: absolute + right: 5px + top: 5px + font-size: 20px + color: $blue + opacity: 0.5 + + .edit-label + color: $blue + + .editable-section.deemphasized, .our-notes-section.deemphasized + opacity: 0.5 + + .editable-section:hover + cursor: pointer + outline: 1px solid $blue + + .editable-icon + opacity: 1.0 + cursor: pointer + + .editable-form + cursor: default diff --git a/app/styles/base.sass b/app/styles/base.sass index 3fbc45e24..eae0d473b 100644 --- a/app/styles/base.sass +++ b/app/styles/base.sass @@ -1,6 +1,5 @@ @import "bootstrap/variables" @import "bootstrap/mixins" -@import "bootstrap/variables" html background-color: #2f261d diff --git a/app/templates/account/profile.jade b/app/templates/account/profile.jade index aedd0d623..fcc3ed0ad 100644 --- a/app/templates/account/profile.jade +++ b/app/templates/account/profile.jade @@ -2,120 +2,214 @@ extends /templates/base block content - if myProfile || (me.isAdmin() && user.get('jobProfile')) + if allowedToEditJobProfile .profile-control-bar - if myProfile - a(href=user.get('jobProfile') ? "/account/settings#job-profile" : "/account/settings") - button.btn.edit-settings-button - i.icon-cog - span(data-i18n="account_profile.edit_settings") Edit Settings + button.btn.edit-settings-button#toggle-editing + i.icon-cog + if editing + span(data-i18n="account_profile.done_editing_settings") Done Editing + else + 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.not_approved').not-approved Not Approved - if user.id != me.id + if !myProfile button.btn.edit-settings-button#enter-espionage-mode 007 if user.get('jobProfile') && allowedToViewJobProfile - var profile = user.get('jobProfile'); - .job-profile-container + div(class="job-profile-container" + (editing ? " editable-profile" : "")) .job-profile-row .left-column.full-height-column .sub-column - .profile-photo-container + .profile-photo-container.editable-section(title="Click to change your photo") + .editable-icon.glyphicon.glyphicon-pencil img.profile-photo(src=user.getPhotoURL(240, true)) .profile-caption= profile.jobTitle || 'Software Developer' - if profileLinks.length - ul.links - each link in profileLinks - if link.link && link.name - 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 + .links-container.editable-section(title="Click to add social and personal links") + .editable-display(title="Click to edit your basic info") + .editable-icon.glyphicon.glyphicon-pencil + if profileLinks.length + ul.links.editable-thinner + each link in profileLinks + if link.link && link.name + 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 editing + h3.edit-label(data-i18n="account_profile.add_links") Add some links + + form.editable-form + .editable-icon.glyphicon.glyphicon-remove + h3 Personal Links + p.help-block Link any other sites or profiles you want to highlight, like your GitHub, your LinkedIn, or your blog. + .editable-array + for link, index in profile.links.concat({}) + .array-item.link-container.well.well-sm + .form-group + label.control-label Link Name + input.form-control(type='link-name', maxlength='30', data-schemaformat='link-name', name='root[links][' + index + '][name]', value=link.name) + if !index + p.help-block What are you linking to? Ex: "Personal Website", "GitHub" + .form-group + label.control-label Link URL + input.form-control(type='url', pattern='^(ht|f)tp(s?)://[0-9a-zA-Z]([-.w]*[0-9a-zA-Z])*(:(0-9)*)*(/?)([a-zA-Z0-9-‌​.?,\'/\+&%$#_=]*)?$', data-schemaformat='url', name='root[links][' + index + '][link]', value=link.link) + if !index + p.help-block Ex.: "https://github.com/nwinter" + button.btn.btn-success.btn-block.save-section(data-i18n="common.save") Save + + .basic-info-container.editable-section + .editable-display(title="Click to edit your basic info") + .editable-icon.glyphicon.glyphicon-pencil + if editing && profile.city == "Defaultsville, CA" + h3.edit-label Update basic info + div= profile.city + ', ' + profile.country + div= profile.visa + div + span(data-i18n="account_profile.looking_for") Looking for: + | #{profile.lookingFor} + div + span(data-i18n="account_profile.last_updated") Last updated: + | #{moment(profile.updated).fromNow()} + + form.editable-form + .editable-icon.glyphicon.glyphicon-remove + .form-group + label.control-label Open to Offers + select.form-control(name='root[active]') + option(value='1', selected=profile.active) Yes, I want interviews + option(value='', selected=!profile.active) No, not right now + p.help-block Want interview offers right now? + .form-group + label.control-label Desired Job Title + input.form-control(type='text', maxlength='50', name='root[jobTitle]', value=profile.jobTitle) + p.help-block + | What role are you looking for? Ex.: "Full Stack Engineer", "Front-End Developer", "iOS Developer" + .form-group + label.control-label City + input.form-control(type='city', maxlength='100', data-schemaformat='city', name='root[city]', value=profile.city) + p.help-block + | City you want to work in (or live in now), like "San Francisco" or "Lubbock, TX". + .form-group + label.control-label Country + input.form-control(type='country', maxlength='100', data-schemaformat='country', name='root[country]', value=profile.country) + p.help-block Country you want to work in (or live in now), like "USA" or "France". + .form-group + label.control-label Looking For + select.form-control(name='root[lookingFor]') + option(value='Full-time', selected=profile.lookingFor == "Full-time") Full-time + option(value='Part-time', selected=profile.lookingFor == "Part-time") Part-time + option(value='Remote', selected=profile.lookingFor == "Remote") Remote + option(value='Contracting', selected=profile.lookingFor == "Contracting") Contracting + option(value='Internship', selected=profile.lookingFor == "Internship") Internship + p.help-block What kind of developer position do you want? + button.btn.btn-success.btn-block.save-section(data-i18n="common.save") Save - div= profile.city + ', ' + profile.country - div= profile.visa - div - span(data-i18n="account_profile.looking_for") Looking for: - | #{profile.lookingFor} - div - span(data-i18n="account_profile.last_updated") Last updated: - | #{moment(profile.updated).fromNow()} - - button#contact-candidate.btn.btn-large.btn-inverse.flat-button + button#contact-candidate.btn.btn-large.btn-inverse.flat-button(disabled=editing) span(data-i18n="account_profile.contact") Contact | #{profile.name.split(' ')[0]} .middle-column.full-height-column .sub-column - h3= profile.name || "Anonymous Developer" - if profile.shortDescription - p= profile.shortDescription + .name-container.editable-section(title="Click to fill in your name") + .editable-icon.glyphicon.glyphicon-pencil + h3= profile.name || (editing ? "Fill in your name" : "Anonymous Developer") + + .short-description-container.editable-section(title="Click to write your short description") + .editable-icon.glyphicon.glyphicon-pencil + if editing && (!profile.shortDescription || profile.shortDescription == jobProfileSchema.properties.shortDescription.default) + h3.edit-label(data-i18n="account_profile.add_short_description") Write a short description of yourself + else if profile.shortDescription + p.editable-thinner= profile.shortDescription + + .skills-container.editable-section.editable-thinner(title="Click to tag your programming skills") + .editable-icon.glyphicon.glyphicon-pencil + if editing && profile.skills.length == 1 && profile.skills[0] == 'javascript' + h3.edit-label Tag your programming skills + else + each skill in profile.skills + code= skill + span + + .long-description-container.editable-section(title="Click to start writing your longer description") + .editable-icon.glyphicon.glyphicon-pencil + if editing && (!profile.longDescription || profile.longDescription == jobProfileSchema.properties.longDescription.default) + h3.edit-label Detail your desired position + else if profile.longDescription + div.long-description.editable-thinner!= marked(profile.longDescription) - each skill in profile.skills - code= skill - span - if profile.longDescription - div.long-description!= marked(profile.longDescription) + .work-container.editable-section(title="Click to add work experience") + .editable-icon.glyphicon.glyphicon-pencil + if profile.work.length + h3.experience-header + img.header-icon(src="/images/pages/account/profile/work.png", alt="") + span(data-i18n="account_profile.work_experience") Work Experience + each job in profile.work + if job.role && job.employer + div.experience-entry + div.duration.pull-right= job.duration + | #{job.role} at #{job.employer} + .clearfix + if job.description + div!= marked(job.description) + else if editing + h3.edit-label Chronicle your work history - if profile.work.length - h3.experience-header - img.header-icon(src="/images/pages/account/profile/work.png", alt="") - span(data-i18n="account_profile.work_experience") Work Experience - each job in profile.work - if job.role && job.employer - div.experience-entry - div.duration.pull-right= job.duration - | #{job.role} at #{job.employer} - .clearfix - if job.description - div!= marked(job.description) - - if profile.education.length - h3.experience-header - img.header-icon(src="/images/pages/account/profile/education.png", alt="") - span(data-i18n="account_profile.education") Education - each school in profile.education - if school.degree && school.school - div.experience-entry - div.duration.pull-right= school.duration - | #{school.degree} at #{school.school} - .clearfix - if school.description - div!= marked(school.description) + .education-container.editable-section(title="Click to add academic experience") + .editable-icon.glyphicon.glyphicon-pencil + if profile.education.length + h3.experience-header + img.header-icon(src="/images/pages/account/profile/education.png", alt="") + span(data-i18n="account_profile.education") Education + each school in profile.education + if school.degree && school.school + div.experience-entry + div.duration.pull-right= school.duration + | #{school.degree} at #{school.school} + .clearfix + if school.description + div!= marked(school.description) + else if editing + h3.edit-label Recount your academic ordeals if user.get('jobProfileNotes') || me.isAdmin() - h3.experience-header(data-i18n="account_profile.our_notes") Our Notes - - var notes = user.get('jobProfileNotes') || ''; - if me.isAdmin() - textarea#job-profile-notes!= notes - button.btn.btn-primary#save-notes-button Save Notes - else - div!= marked(notes) + div(class="our-notes-section" + (editing ? " deemphasized" : "")) + h3.experience-header(data-i18n="account_profile.our_notes") Our Notes + - var notes = user.get('jobProfileNotes') || ''; + if me.isAdmin() + textarea#job-profile-notes!= notes + button.btn.btn-primary#save-notes-button Save Notes + else + div!= marked(notes) .right-column.full-height-column .sub-column - if profile.projects.length - h3(data-i18n="account_profile.projects") Projects - ul.projects - each project in profile.projects - if project.name - li - if project.link && project.link.length && project.link != 'http://example.com' - a(href=project.link) - if project.picture - .project-image(style="background-image: url('/file/" + project.picture + "')") - p= project.name - div!= marked(project.description) + .projects-container.editable-section(title="Click to add your projects") + .editable-icon.glyphicon.glyphicon-pencil + if profile.projects.length + h3(data-i18n="account_profile.projects") Projects + ul.projects + each project in profile.projects + if project.name + li + if project.link && project.link.length && project.link != 'http://example.com' + a(href=project.link) + if project.picture + .project-image(style="background-image: url('/file/" + project.picture + "')") + p= project.name + div!= marked(project.description) + else if editing + h3.edit-label Add 3 projects + else if allowedToViewJobProfile .public-profile-container - h2 Loading... - + h2(data-i18n="common.loading") Loading... else .public-profile-container @@ -127,4 +221,5 @@ block content img.profile-photo(src=user.getPhotoURL(256)) h2 TODO - p Public user profiles are not ready yet. If you are seeing this, we probably have a bug leading to a broken link. \ No newline at end of file + p Public user profiles are not ready yet. If you are seeing this, we probably have a bug leading to a broken link. + diff --git a/app/views/account/profile_view.coffee b/app/views/account/profile_view.coffee index 49249f317..fa113063e 100644 --- a/app/views/account/profile_view.coffee +++ b/app/views/account/profile_view.coffee @@ -6,12 +6,19 @@ JobProfileContactView = require 'views/modal/job_profile_contact_modal' module.exports = class ProfileView extends View id: "profile-view" template: template + editing: false events: + 'click #toggle-editing': 'toggleEditing' 'click #toggle-job-profile-approved': 'toggleJobProfileApproved' 'click save-notes-button': 'onJobProfileNotesChanged' 'click #contact-candidate': 'onContactCandidate' 'click #enter-espionage-mode': 'enterEspionageMode' + 'click .editable-profile .profile-photo': 'onEditProfilePhoto' + 'click .editable-profile .editable-display': 'onEditSection' + 'click .editable-profile .save-section': 'onSaveSection' + 'click .editable-profile .glyphicon-remove': 'onCancelSectionEdit' + 'change .editable-profile .editable-array input': 'onEditArray' constructor: (options, @userID) -> @onJobProfileNotesChanged = _.debounce @onJobProfileNotesChanged, 1000 @@ -27,8 +34,11 @@ module.exports = class ProfileView extends View getRenderData: -> context = super() context.user = @user - context.allowedToViewJobProfile = me.isAdmin() or "employer" in me.get('permissions') context.myProfile = @user.id is context.me.id + context.allowedToViewJobProfile = me.isAdmin() or "employer" in me.get('permissions') or context.myProfile + context.allowedToEditJobProfile = me.isAdmin() or context.myProfile + context.editing = @editing + context.jobProfileSchema = me.schema().properties.jobProfile context.marked = marked context.moment = moment context.iconForLink = @iconForLink @@ -41,15 +51,21 @@ module.exports = class ProfileView extends View afterRender: -> super() @updateProfileApproval() if me.isAdmin() - unless @user.get('jobProfile')?.projects?.length + unless @user.get('jobProfile')?.projects?.length or @editing @$el.find('.right-column').hide() @$el.find('.middle-column').addClass('double-column') + unless @editing + @$el.find('.editable-display').attr('title', '') updateProfileApproval: -> approved = @user.get 'jobProfileApproved' @$el.find('.approved').toggle Boolean(approved) @$el.find('.not-approved').toggle not approved + toggleEditing: -> + @editing = not @editing + @render() + toggleJobProfileApproved: -> approved = not @user.get 'jobProfileApproved' @user.set 'jobProfileApproved', approved @@ -88,3 +104,109 @@ module.exports = class ProfileView extends View onContactCandidate: (e) -> @openModalView new JobProfileContactView recipientID: @user.id + + saveEdits: (e) -> + res = @user.validate() + if res? + console.error "Couldn't save because of validation errors:", res + # TODO: show some sort of problem message here + return + jobProfile = @user.get('jobProfile') + jobProfile.updated = (new Date()).toISOString() + @user.set 'jobProfile', jobProfile + return unless res = @user.save() + res.error -> + errors = JSON.parse(res.responseText) + # TODO: show some sort of problem message here + res.success (model, response, options) => + @render() + + onEditProfilePhoto: (e) -> + filepicker.pick {mimetypes: 'image/*'}, @onProfilePhotoChosen + + onProfilePhotoChosen: (inkBlob) => + filePath = "db/user/#{@user.id}" + body = + url: inkBlob.url + filename: inkBlob.filename + mimetype: inkBlob.mimetype + path: filePath + force: true + + @uploadingPath = [filePath, inkBlob.filename].join('/') + @$el.find('.profile-photo').addClass('saving') + $.ajax '/file', type: 'POST', data: body, success: @onFileUploaded + + onFileUploaded: (e) => + @user.get('jobProfile').photoURL = @uploadingPath + @saveEdits() + + onEditSection: (e) -> + section = $(e.target).closest('.editable-section') + section.find('.editable-form').show() + section.find('.editable-display').hide() + @$el.find('.editable-section').not(section).addClass 'deemphasized' + column = section.closest('.full-height-column') + @$el.find('.full-height-column').not(column).addClass 'deemphasized' + + onCancelSectionEdit: (e) -> + @render() + + onSaveSection: (e) -> + e.preventDefault() + section = $(e.target).closest('.editable-section') + isEmpty = @arrayItemIsEmpty + section.find('.editable-array .array-item').each -> + $(@).remove() if isEmpty @ + resetOnce = false # We have to clear out arrays if we're going to redo them + for field in $(e.target).closest('form').serializeArray() + keyChain = @extractFieldKeyChain field.name + value = @extractFieldValue keyChain[0], field.value + console.log "Should save", keyChain, value + parent = @user.get('jobProfile') + for key, i in keyChain + break if i is keyChain.length - 1 + child = parent[key] + if _.isArray(child) and not resetOnce + child = parent[key] = [] + resetOnce = true + else unless child? + child = parent[key] = {} + parent = child + console.log " Setting", parent, "prop", key, "to", value + parent[key] = value + section.addClass 'saving' + @saveEdits() + + extractFieldKeyChain: (key) -> + # "root[projects][0][name]" -> ["projects", "0", "name"] + key.replace(/^root/, '').replace(/\[(.*?)\]/g, '.$1').replace(/^\./, '').split(/\./) + + extractFieldValue: (key, value) -> + switch key + when 'active' then Boolean value + else value + + arrayItemIsEmpty: (arrayItem) -> + for input in $(arrayItem).find('input') + return false if $(input).val() + true + + onEditArray: (e) -> + array = $(e.target).closest('.editable-array') + arrayItems = array.find('.array-item') + toRemove = [] + for arrayItem, index in arrayItems + empty = @arrayItemIsEmpty arrayItem + if index is arrayItems.length - 1 + lastEmpty = empty + else if empty + toRemove.unshift index + $(arrayItems[emptyIndex]).remove() for emptyIndex in toRemove + unless lastEmpty + clone = $(arrayItem).clone(true) + clone.find('input').each -> $(@).val('') + array.append clone + for arrayItem, index in array.find('.array-item') + for input in $(arrayItem).find('input') + $(input).attr('name', $(input).attr('name').replace(/\[\d+\]/, "[#{index}]")) diff --git a/app/views/account/settings_view.coffee b/app/views/account/settings_view.coffee index 1aae7bc6a..73f952ad6 100644 --- a/app/views/account/settings_view.coffee +++ b/app/views/account/settings_view.coffee @@ -107,6 +107,7 @@ module.exports = class SettingsView extends View @grabData() res = me.validate() if res? + console.error "Couldn't save because of validation errors:", res forms.applyErrorsToForm(@$el, res) return @@ -144,7 +145,7 @@ module.exports = class SettingsView extends View me.set 'name', $('#name', @$el).val() me.set 'email', $('#email', @$el).val() for emailName, enabled of @getSubscriptions() - me.setEmailSubscription emailName, enabled + me.setEmailSubscription emailName, enabled me.set 'photoURL', @pictureTreema.get('/photoURL') adminCheckbox = @$el.find('#admin')