Skip to content

Instantly share code, notes, and snippets.

@Alex-Bakic
Last active March 8, 2020 13:39
Show Gist options
  • Save Alex-Bakic/e23824a92f8bb18adf8fc63a6e283da2 to your computer and use it in GitHub Desktop.
Save Alex-Bakic/e23824a92f8bb18adf8fc63a6e283da2 to your computer and use it in GitHub Desktop.

WorksHub Issue 20: Allow users to update edit their Career history.

This issue goes over the creating of a feature that allows a user to delete and manage their CVs through a more expressive setup, of using buttons and messages , rather than inconspicous behaviour within the "upload cv" button.

A user is allowed one CV file, and one external CV , and there should be a little message below [:h2 "Career History"] that mentions this to the user:

;; so something like this
[:section.profile-section.cv
 [:div.cv__view
  [:h2 "Career History"]
  ;; -> [:p "You can upload a maximum of one cv file and one external cv link."]
  ...  

Now, we can add an "x" , which I'll just have be a replica of the job-card buttons, but with no absolute positioning as it should sit next to the link.

;; in _profile.sass
.cv-link__icon
    width: 24px
    height: 24px
    margin-left: 10px
    fill: #dedede
    cursor: pointer
    transition: fill .2s
    &:hover
      fill: darken(#dedede, 25%)

When we think a bit about how the view should look, the "Upload resume" button, for cv files , will have a choice of three different states:

- currently in the process of uploading a file : button with text "uploading"
- currently disabled as user already has uploaded cv : disabled button with text "upload resume"
- there is no process or uploaded cv, so it should be clickable

And for the cv link button:

- a link is already uploaded, and so the button is disabled...
- no link, so the button should be clickable

Now for all these different possibilities it makes sense to separate them into their own sub-component, with all the states handled below:

(defn cv-section-buttons
  []
  [:div.cv__buttons
   (cond
     (<sub [::subs/cv-uploading?]) [:button.button {:disabled true} "Uploading..."]
     (<sub [::subs/cv-file-uploaded?]) [:button.button {:disabled true} "Upload resume"]
     :else [:label.file-label.cv__upload
            [:input.file-input {:type "file"
                                :name "avatar"
                                :on-change (upload/handler
                                            :launch [::events/cv-upload]
                                            :on-upload-start [::events/cv-upload-start]
                                            :on-success [::events/cv-upload-success]
                                            :on-failure [::events/cv-upload-failure])}]
            [:span.file-cta.button
             [:span.file-label "Upload resume"]]])
   (if (<sub [::subs/cv-link-uploaded?])
       [:button.button {:disabled true} "Add a link to resume"]
       [edit-link :profile-edit-cv :candidate-edit-cv "Add a link to resume" "button"])])

I'm making use of two new subscriptions, ::subs/cv-file-uploaded?, and ::subs/cv-link-uploaded? and these are essentially watchers of the :link and :file keys with a profile sub-db. But before I show you them it is important to say why I introduced new subscriptions instead of referencing the cv-link, cv-path etc already given to the views. As I am essentially overwriting the link and/or file, I still need to provide values with are accepted by the backend and pass spec checks, otherwise I have to reinvent a whole other section of logic, when instead I could simply call pre-existing functions with , say , an empty string for :link and an empty file for :file. Then these subscriptions would watch for an empty string, or an empty file:

;; within logged_in/profile/subs.cljs
(reg-sub
 ::cv-file-uploaded?
 :<- [::profile]
 (fn [profile]
   (let [clear-map {:type nil :name "none" :url "http://"}]
     (not= clear-map (get-in profile [::profile/cv :file])))))

(reg-sub
 ::cv-link-uploaded?
 :<- [::profile]
 (fn [profile]
   (not (empty? (get-in profile [::profile/cv :link])))))

But then this check has to be used throughout the entire cv component, as you can see here:

  (defn cv-section-view
    ([opts] (cv-section-view :owner opts))
    ([user-type {:keys [cv-link cv-filename cv-url]}]
     [:section.profile-section.cv
      [:div.cv__view
       [:h2 "Career History"]
       [:p "You can upload a maximum of one cv file and one external cv link."]
       [error-box]
       (when (<sub [::subs/cv-file-uploaded?])
         [:div (if (owner? user-type) "You uploaded " "Uploaded CV: ")
          [:div.cv__wrapper
           [:a.a--underlined {:href cv-url, :target "_blank" :rel "noopener"}
            (if (owner? user-type)
              cv-filename
              "Click here to download")]
           (when (owner? user-type)
             [icon "close"
              :id (str "cv-file__" cv-url)
              :class "cv-link__icon"
              :on-click #(dispatch [::events/remove-cv])])]])
       (when (<sub [::subs/cv-link-uploaded?])
         [:div (if (owner? user-type) "Your external resume: " "External resume: ")
          [:div.cv__wrapper
           [:a.a--underlined {:href cv-link, :target "_blank", :rel "noopener"}
            cv-link]
           (when (owner? user-type)
             [icon "close"
              :id (str "cv-link__" cv-link)
              :class "cv-link__icon"
              :on-click #(dispatch [::events/remove-link])])]])
       (when-not (or (<sub [::subs/cv-file-uploaded?])
                     (<sub [::subs/cv-link-uploaded?]))
         (if (owner? user-type)
           "You haven't uploaded resume yet."
           "No uploaded resume yet."))]
      (when (owner-or-admin? user-type)
        [cv-section-buttons])]))

The last segment of this feature are those events that the icons trigger, with the one for the icon being simple enough:

;; this is a pre-existing event, and all I'm gonna do is reset the value at :link
(reg-event-db
  ::edit-cv-link
  profile-interceptors
  (fn [db [link]]
    (assoc-in db [::profile/cv :link] link)))

;; little event for the close link icon, as there is no dispatch-n function :/
(reg-event-fx
 ::remove-link
 (fn [{db :db} _]
   {:db db
    :dispatch-n [[::edit-cv-link ""]
                 [::save-cv-info]]}))

::save-cv-info is a crucial bit of this event chain, and it's underlying functionality allows us to simply a lot of edge cases, and this is because it will take the whole map within :wh.logged-in.profile.db/cv , meaning I can also use it within the ::remove-cv event, as it saves the contents of :file to the backend too.

;; use same form , to pass specs and to keep aligned with ::cv-file-uploaded? sub.
(reg-event-fx
 ::remove-cv
 profile-interceptors
 (fn [{db :db} _]
   (let [cleared-map {:type nil :name "none" :url "http://"}]
     {:db (assoc-in db [::profile/cv :file] cleared-map)
      :dispatch [::save-cv-info]})))

Looking at the ::save-cv-info event itself:

(reg-event-fx
  ::save-cv-info
  db/default-interceptors
  (fn [{db :db} [{:keys [type]}]]
    (let [url-path [::profile/sub-db ::profile/cv :link]
          cv-link (get-in db url-path)
          valid-cv-link? (or (= type :upload-cv)
                             (s/valid? ::specs/url cv-link))]
      (cond
        (or valid-cv-link? (empty? cv-link))
        {:db       db
         :graphql  {:query      graphql/update-user-mutation--approval
                    :variables  {:update_user (graphql-cv-update db)}
                    :on-success [::save-success]
                    :on-failure [::save-failure]}
         :dispatch-n [[::pages/set-loader]
                      [:error/close-global]]}
        :else
        {:dispatch [:error/set-global "Resume link is not valid. Please amend and try again."]}))))

I've changed it a bit from the original, and we can see that it would reject any cv link which is blank, and that's why I've wrapped an or clause there, with the empty? function so we can overwrite :link. The subscription checks whether :link is empty, and if it is then it won't be showed in the views, which is the intention of some of the spec checks. graphql-cv-update will take what's in the ::profile/cv sub-db and push it to the server.

Closing Thoughts

Looking at how close the icon is to the cv items themselves it might mean that a user accidentally deletes instead of viewing, so maybe we centralise the :a elements vertically with the buttons, and for the wrapping div to set space-evenly for the items, or maybe it is as simple as doubling the margins, or putting justify-content: space-evenly on the wrapping div.

This is the current look:

cv

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment