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.
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: