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

View File

@ -9,12 +9,13 @@
:value @val-ratom
:on-change #(reset! val-ratom (-> % .-target .-value))})]))
(defn dropdown [title val-ratom items]
(defn dropdown [props title val-ratom items]
(fn []
(if (= 1 (count items))
(reset! val-ratom (first items)))
[: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]
^{:key item} [:option
{:value item}

View File

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

View File

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

View File

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

View File

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

View File

@ -13,9 +13,16 @@
.ui.button {
color: #2f3f44;
background-color: #b0f1ee;
white-space: nowrap;
padding: 0.9em 1.2em 0.9em;
&:hover {
background-color: #bdf8f5;
}
&:active,&:focus {
background-color: #97ebe7;
}
}
.ui.small.button {
font-size: 15px!important;
@ -87,7 +94,6 @@ span.dropdown.icon {
.ui.dropdown .item {
color: #fff!important;
opacity: 0.98;
}
.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 {
color: #e7e7e7!important;
font-size: .8em;
width: 360px!important;
}
.ui.selection.dropdown.address-input option {
color: #e7e7e7;
background-color: yellow!important;
}
.ui.menu.transition.visible {
font-family: 'postgrotesk';
font-size: 1em;
border-radius: 8px;
border: none;
padding: .5em;
.item {
:hover {
color: #e96868;
}
a {
&:hover {
color: #e96868;
}
color: #474951;
.item>a {
&:hover {
color:#1bb5c1;
}
color: #474951;
}
}
.logout-link {
color: #e96868!important;
}
.ui.attached.tabular {
background-color: #2f3f44;
border-bottom: none;
@ -189,9 +198,6 @@ span.dropdown.icon {
padding: 0.8em 1em 1.1em!important;
flex-direction: row!important;
// .ui.mini.circular.image {
// margin: 0;
// }
}
.ui.cards>.card {
@ -229,6 +235,28 @@ span.dropdown.icon {
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 {
font-family: 'postgrotesk'!important;
}

View File

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