UX improvements + address validation

* error/success flash notification support
* disable spellcheck for address editbox
* validate user's Ethereum address when saving
* update button state styles
This commit is contained in:
Teemu Patja 2017-02-21 21:43:35 +02:00
parent 1663669163
commit 4e627be1e0
No known key found for this signature in database
GPG Key ID: F5B7035E6580FD4C
9 changed files with 113 additions and 49 deletions

View File

@ -1,4 +1,4 @@
{ {
:test true :test true
:jdbc-database-url "jdbc:postgresql://localhost/commiteth_test?user=commiteth&password=commiteth" :jdbc-database-url "jdbc:postgresql://localhost/commiteth?user=commiteth&password=commiteth"
} }

View File

@ -9,6 +9,7 @@
[commiteth.db.repositories :as repositories] [commiteth.db.repositories :as repositories]
[commiteth.db.bounties :as bounties-db] [commiteth.db.bounties :as bounties-db]
[commiteth.bounties :as bounties] [commiteth.bounties :as bounties]
[commiteth.eth.core :as eth]
[commiteth.github.core :as github] [commiteth.github.core :as github]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[commiteth.eth.core :as eth] [commiteth.eth.core :as eth]
@ -97,11 +98,9 @@
:description "commitETH API"}}}} :description "commitETH API"}}}}
(context "/api" [] (context "/api" []
(context "/bounties" [] (context "/bounties" []
(GET "/all" [] (GET "/all" []
(ok (bounties-db/list-all-bounties)))) (ok (bounties-db/list-all-bounties))))
(context "/user" [] (context "/user" []
(GET "/" [] (GET "/" []
:auth-rules authenticated? :auth-rules authenticated?
@ -111,10 +110,15 @@
:auth-rules authenticated? :auth-rules authenticated?
:body-params [user-id :- Long, address :- String] :body-params [user-id :- Long, address :- String]
:summary "Update user address" :summary "Update user address"
(let [result (users/update-user-address user-id address)] (if-not (eth/valid-address? address)
(if (= 1 result) {:status 400
(ok) :body "Invalid Ethereum address"}
(internal-server-error)))) (let [result (users/update-user-address
user-id
address)]
(if (= 1 result)
(ok)
(internal-server-error)))))
(GET "/repositories" [] (GET "/repositories" []
:auth-rules authenticated? :auth-rules authenticated?
:current-user user :current-user user
@ -138,8 +142,7 @@
:auth-rules authenticated? :auth-rules authenticated?
:current-user user :current-user user
(ok (map #(conj % (let [balance (:balance %)] (ok (map #(conj % (let [balance (:balance %)]
{:balance-eth (eth/hex->eth balance 6) {:balance-eth (eth/hex->eth balance 6)}))
:balance-wei (eth/hex->big-integer balance)}))
(bounties-db/list-owner-bounties (:id user))))) (bounties-db/list-owner-bounties (:id user)))))
(POST "/repository/toggle" {:keys [params]} (POST "/repository/toggle" {:keys [params]}
:auth-rules authenticated? :auth-rules authenticated?

View File

@ -9,12 +9,13 @@
:value @val-ratom :value @val-ratom
:on-change #(reset! val-ratom (-> % .-target .-value))})])) :on-change #(reset! val-ratom (-> % .-target .-value))})]))
(defn dropdown [title val-ratom items] (defn dropdown [props title val-ratom items]
(fn [] (fn []
(if (= 1 (count items)) (if (= 1 (count items))
(reset! val-ratom (first items))) (reset! val-ratom (first items)))
[:select.ui.basic.selection.dropdown [:select.ui.basic.selection.dropdown
{:on-change #(reset! val-ratom (-> % .-target .-value))} (merge props {:on-change
#(reset! val-ratom (-> % .-target .-value))})
(doall (for [item items] (doall (for [item items]
^{:key item} [:option ^{:key item} [:option
{:value item} {:value item}

View File

@ -22,6 +22,16 @@
[re-frisk.core :refer [enable-re-frisk!]]) [re-frisk.core :refer [enable-re-frisk!]])
(:import goog.History)) (:import goog.History))
(defn flash-message-pane []
(let [flash-message (rf/subscribe [:flash-message])]
(fn []
(when-let [[type message] @flash-message]
[:div.flash-message {:class (name type)}
[:i.close.icon {:on-click #(rf/dispatch [:clear-flash-message])}]
[:p message]]))
))
(def user-dropdown-open? (r/atom false)) (def user-dropdown-open? (r/atom false))
(defn user-dropdown [user items] (defn user-dropdown [user items]
@ -38,12 +48,13 @@
[:div.item [:div.item
[:span.dropdown.icon]] [:span.dropdown.icon]]
(into menu (into menu
(for [[target caption] items] (for [[target caption props] items]
^{:key target} [:div.item ^{:key target} [:div.item
[:a [:a
(if (keyword? target) (merge props
{:on-click #(rf/dispatch [target])} (if (keyword? target)
{:href target}) {:on-click #(rf/dispatch [target])}
{:href target}))
caption]]))])) caption]]))]))
@ -53,8 +64,8 @@
(if @user (if @user
[:div.ui.text.menu.user-component [:div.ui.text.menu.user-component
[:div.item [:div.item
[user-dropdown @user [[:update-address "Update address"] [user-dropdown @user [[:update-address "Update address" {}]
["/logout" "Sign out"]]]]] ["/logout" "Sign out" {:class "logout-link"}]]]]]
[:a.ui.button.small {:href js/authorizeUrl} "Sign in"])))) [:a.ui.button.small {:href js/authorizeUrl} "Sign in"]))))
(defn tabs [] (defn tabs []
@ -74,7 +85,8 @@
(defn page-header [] (defn page-header []
(let [user (rf/subscribe [:user])] (let [user (rf/subscribe [:user])
flash-message (rf/subscribe [:flash-message])]
(fn [] (fn []
[:div.vertical.segment.commiteth-header [:div.vertical.segment.commiteth-header
[:div.ui.grid.container [:div.ui.grid.container
@ -82,7 +94,9 @@
[:div.ui.image [:div.ui.image
[:img.left.aligned {:src "/img/logo.svg"}]]] [:img.left.aligned {:src "/img/logo.svg"}]]]
[:div.four.wide.column [:div.four.wide.column
[user-component @user]] (if @flash-message
[flash-message-pane]
[user-component @user])]
(when-not @user (when-not @user
[:div.ui.text.content [:div.ui.text.content
[:div.ui.divider.hidden] [:div.ui.divider.hidden]
@ -117,7 +131,6 @@
(fn [] (fn []
[:div.ui.pusher [:div.ui.pusher
[page-header] [page-header]
;; [error-pane]
[:div.ui.vertical.segment [:div.ui.vertical.segment
[:div.ui.container [:div.ui.container
[:div.ui.grid.stackable [:div.ui.grid.stackable

View File

@ -25,14 +25,14 @@
(assoc-in db path value))) (assoc-in db path value)))
(reg-event-db (reg-event-db
:set-error :set-flash-message
(fn [db [_ text]] (fn [db [_ type text]]
(assoc db :error text))) (assoc db :flash-message [type text])))
(reg-event-db (reg-event-db
:clear-error :clear-flash-message
(fn [db _] (fn [db _]
(dissoc db :error))) (dissoc db :flash-message)))
(reg-event-db (reg-event-db
:set-active-page :set-active-page
@ -181,9 +181,21 @@
:save-user-address :save-user-address
(fn [{:keys [db]} [_ user-id address]] (fn [{:keys [db]} [_ user-id address]]
(prn "save-user-address" user-id address) (prn "save-user-address" user-id address)
{:db db {:db (assoc db :updating-address true)
:http {:method POST :http {:method POST
:url "/api/user/address" :url "/api/user/address"
:on-success #(println "on-success" %) :on-success #(do
:on-error #(println "on-error" %) (dispatch [:assoc-in [:user [:address] address]])
(dispatch [:set-flash-message
:success
"Address saved"]))
:on-error #(dispatch [:set-flash-message
:error
(:response %)])
:finally #(dispatch [:clear-updating-address])
:params {:user-id user-id :address address}}})) :params {:user-id user-id :address address}}}))
(reg-event-db
:clear-updating-address
(fn [db _]
(dissoc db :updating-address)))

View File

@ -22,9 +22,9 @@
(:repos db))) (:repos db)))
(reg-sub (reg-sub
:error :flash-message
(fn [db _] (fn [db _]
(:error db))) (:flash-message db)))
(reg-sub (reg-sub
:all-bounties :all-bounties

View File

@ -7,6 +7,7 @@
(defn update-address-page [] (defn update-address-page []
(let [user (rf/subscribe [:user]) (let [user (rf/subscribe [:user])
updating-address (rf/subscribe [:get-in [:updating-address]])
web3 (.-web3 js/window) web3 (.-web3 js/window)
web3-accounts (into [] (when-not (nil? web3) (-> web3 web3-accounts (into [] (when-not (nil? web3) (-> web3
.-eth .-eth
@ -21,7 +22,7 @@
[:p "Placeholder text for explaining what an Ethereum address is."] [:p "Placeholder text for explaining what an Ethereum address is."]
[:div.field [:div.field
(if-not (empty? web3-accounts) (if-not (empty? web3-accounts)
[dropdown "Select address" [dropdown {:class "address-input"} "Select address"
address address
(into [] (into []
(for [acc web3-accounts] (for [acc web3-accounts]
@ -29,9 +30,13 @@
[:div.ui.input.address-input [:div.ui.input.address-input
[input address {:placeholder "0x0000000000000000000000000000000000000000" [input address {:placeholder "0x0000000000000000000000000000000000000000"
:auto-complete "off" :auto-complete "off"
:auto-correct "off"
:spell-check "false"
:max-length 42}]])] :max-length 42}]])]
[:button.ui.button {:on-click [:button.ui.button (merge {:on-click
#(rf/dispatch [:save-user-address #(rf/dispatch [:save-user-address
(:id @user) (:id @user)
@address])} @address])}
(when @updating-address
{:class "busy loading"}))
"Update"]]]))) "Update"]]])))

View File

@ -13,9 +13,16 @@
.ui.button { .ui.button {
color: #2f3f44;
background-color: #b0f1ee; background-color: #b0f1ee;
white-space: nowrap; white-space: nowrap;
padding: 0.9em 1.2em 0.9em; padding: 0.9em 1.2em 0.9em;
&:hover {
background-color: #bdf8f5;
}
&:active,&:focus {
background-color: #97ebe7;
}
} }
.ui.small.button { .ui.small.button {
font-size: 15px!important; font-size: 15px!important;
@ -87,7 +94,6 @@ span.dropdown.icon {
.ui.dropdown .item { .ui.dropdown .item {
color: #fff!important; color: #fff!important;
opacity: 0.98; opacity: 0.98;
} }
.ui.menu .ui.dropdown .menu>.item:hover, .ui.menu .ui.dropdown .menu>.selected.item { .ui.menu .ui.dropdown .menu>.item:hover, .ui.menu .ui.dropdown .menu>.selected.item {
@ -95,30 +101,33 @@ span.dropdown.icon {
} }
.ui.input.address-input { .ui.input.address-input {
color: #e7e7e7!important;
font-size: .8em; font-size: .8em;
width: 360px!important; width: 360px!important;
} }
.ui.selection.dropdown.address-input option {
color: #e7e7e7;
background-color: yellow!important;
}
.ui.menu.transition.visible { .ui.menu.transition.visible {
font-family: 'postgrotesk'; font-family: 'postgrotesk';
font-size: 1em; font-size: 1em;
border-radius: 8px; border-radius: 8px;
border: none; border: none;
padding: .5em; padding: .5em;
.item { .item>a {
:hover { &:hover {
color: #e96868; color:#1bb5c1;
}
a {
&:hover {
color: #e96868;
}
color: #474951;
} }
color: #474951;
} }
} }
.logout-link {
color: #e96868!important;
}
.ui.attached.tabular { .ui.attached.tabular {
background-color: #2f3f44; background-color: #2f3f44;
border-bottom: none; border-bottom: none;
@ -189,9 +198,6 @@ span.dropdown.icon {
padding: 0.8em 1em 1.1em!important; padding: 0.8em 1em 1.1em!important;
flex-direction: row!important; flex-direction: row!important;
// .ui.mini.circular.image {
// margin: 0;
// }
} }
.ui.cards>.card { .ui.cards>.card {
@ -229,6 +235,28 @@ span.dropdown.icon {
color: #a8aab1; color: #a8aab1;
} }
.flash-message {
&.success {
background-color: #61deb0;
}
&.error {
background-color: #e96868;
}
color: #fff;
border-radius: 8px;
z-index: 100;
padding: 1.4em 1em 1.4em;
i {
position: absolute;
margin: 0;
right: 1.5em;
top: 1.5em;
cursor: pointer;
}
}
.ui { .ui {
font-family: 'postgrotesk'!important; font-family: 'postgrotesk'!important;
} }

View File

@ -24,6 +24,7 @@
{:id 1 {:id 1
:login "torvalds" :login "torvalds"
:name "Linus Torvalds" :name "Linus Torvalds"
:avatar_url ""
:email nil :email nil
:token "not null" :token "not null"
:address "address" :address "address"
@ -32,6 +33,7 @@
:login "torvalds" :login "torvalds"
:name "Linus Torvalds" :name "Linus Torvalds"
:email nil :email nil
:avatar_url ""
:token "not null" :token "not null"
:address "address" :address "address"
:created nil} :created nil}