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:
parent
1663669163
commit
4e627be1e0
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"]]])))
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Reference in New Issue