WIP: new UX

* semantic UI + less based UI styling
* header, tabs, user-profile component
* store avatar URL in DB, show image in user
* unify :user and :user-profile in app-db for simplicity
This commit is contained in:
Teemu Patja 2017-02-12 22:25:32 +02:00
parent 6fc8702a34
commit 6e646280c2
No known key found for this signature in database
GPG Key ID: F5B7035E6580FD4C
20 changed files with 324 additions and 110 deletions

View File

@ -0,0 +1,2 @@
ALTER TABLE public.users
ADD avatar_url VARCHAR(255);

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 42 (36781) - http://www.bohemiancoding.com/sketch -->
<title>C09316C0-28C3-4D2D-AF57-806E1D6C1FE6</title>
<desc>Created with sketchtool.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="commiteth-desktop_repositories" transform="translate(-1098.000000, -32.000000)" fill="#FFFFFF">
<g id="header">
<g id="user" transform="translate(956.000000, 24.000000)">
<g id="icon_dropdown" transform="translate(142.000000, 8.000000)">
<path d="M7.76596419,11.5564843 C8.07705085,11.6115965 8.40862642,11.5177766 8.64937808,11.277025 L13.3038251,6.62257794 C13.6942764,6.23212667 13.695553,5.59768507 13.3050287,5.20716078 C12.9117818,4.81391382 12.2808006,4.81717535 11.8896116,5.20836438 L7.76949482,9.32848112 L3.64937808,5.20836438 C3.25892681,4.81791311 2.62448521,4.81663648 2.23396092,5.20716078 C1.84071396,5.60040773 1.84397549,6.23138892 2.23516452,6.62257794 L6.88961157,11.277025 C7.12783981,11.5152532 7.45689724,11.6086052 7.76596419,11.5564843 L7.76596419,11.5564843 Z" id="shape"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -3,19 +3,20 @@
-- :name create-user! :<! :1 -- :name create-user! :<! :1
-- :doc creates a new user record -- :doc creates a new user record
INSERT INTO users INSERT INTO users
(id, login, name, email, token, address, created) (id, login, name, email, avatar_url, token, address, created)
SELECT SELECT
:id, :id,
:login, :login,
:name, :name,
:email, :email,
:avatar_url,
:token, :token,
:address, :address,
:created :created
WHERE NOT exists(SELECT 1 WHERE NOT exists(SELECT 1
FROM users FROM users
WHERE id = :id) WHERE id = :id)
RETURNING id, login, name, email, token, address, created; RETURNING id, login, name, email, avatar_url, token, address, created;
-- :name update-user! :! :n -- :name update-user! :! :n
-- :doc updates an existing user record -- :doc updates an existing user record

View File

@ -19,15 +19,11 @@
<body> <body>
<div id="app"> <div id="app">
<div class="container-fluid">
</div>
</div> </div>
<!-- scripts and styles --> <!-- scripts and styles -->
{% style "//cdn.jsdelivr.net/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" %} {% style "https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.1.8/semantic.min.css" %}
{% style "//cdn.jsdelivr.net/bootstrap-social/5.1.1/bootstrap-social.css" %} {% style "/css/style.css" %}
{% style "//cdn.jsdelivr.net/fontawesome/4.7.0/css/font-awesome.min.css" %}
{% style "/css/main.css" %}
<script type="text/javascript"> <script type="text/javascript">
var context = "{{servlet-context}}"; var context = "{{servlet-context}}";

View File

@ -4,7 +4,7 @@
(:import [java.util Date])) (:import [java.util Date]))
(defn create-user (defn create-user
[user-id login name email token] [user-id login name email avatar-url token]
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(db/create-user! con-db (db/create-user! con-db
{:id user-id {:id user-id
@ -12,6 +12,7 @@
:name name :name name
:email email :email email
:token token :token token
:avatar_url avatar-url
:address nil :address nil
:created (new Date)}))) :created (new Date)})))

View File

@ -93,6 +93,7 @@
(defn add-webhook (defn add-webhook
[full-repo token] [full-repo token]
(log/debug "adding webhook" full-repo token) (log/debug "adding webhook" full-repo token)
; TODO: use secret key in config param
(let [[user repo] (str/split full-repo #"/")] (let [[user repo] (str/split full-repo #"/")]
(repos/create-hook user repo "web" (repos/create-hook user repo "web"
{:url (str (server-address) "/webhook") {:url (str (server-address) "/webhook")

View File

@ -3,7 +3,6 @@
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[commiteth.layout :refer [*app-context* error-page]] [commiteth.layout :refer [*app-context* error-page]]
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]] [ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
[ring.middleware.webjars :refer [wrap-webjars]]
[ring.middleware.format :refer [wrap-restful-format]] [ring.middleware.format :refer [wrap-restful-format]]
[commiteth.config :refer [env]] [commiteth.config :refer [env]]
[ring.middleware.flash :refer [wrap-flash]] [ring.middleware.flash :refer [wrap-flash]]
@ -83,7 +82,6 @@
(defn wrap-base [handler] (defn wrap-base [handler]
(-> ((:middleware defaults) handler) (-> ((:middleware defaults) handler)
wrap-auth wrap-auth
wrap-webjars
wrap-flash wrap-flash
(wrap-session {:timeout (* 60 60 6) (wrap-session {:timeout (* 60 60 6)
:cookie-attrs {:http-only true}}) :cookie-attrs {:http-only true}})

View File

@ -10,9 +10,9 @@
(defroutes home-routes (defroutes home-routes
(GET "/" {{identity :identity} :session} (GET "/" {{identity :identity} :session}
(home-page identity)) (home-page identity))
(GET "/logout" {session :session} (GET "/logout" {session :session}
(-> (redirect "/") (-> (redirect "/")
(assoc :session (dissoc session :identity)))) (assoc :session (dissoc session :identity))))
(GET "/docs" [] (-> (ok (-> "docs/docs.md" io/resource slurp)) (GET "/docs" [] (-> (ok (-> "docs/docs.md" io/resource slurp))
(header "Content-Type" "text/plain; charset=utf-8")))) (header "Content-Type" "text/plain; charset=utf-8"))))

View File

@ -16,9 +16,10 @@
(defn- create-user [token user] (defn- create-user [token user]
(let [{name :name (let [{name :name
login :login login :login
user-id :id} user user-id :id
avatar-url :avatar_url} user
email (github/get-user-email token)] email (github/get-user-email token)]
(users/create-user user-id login name email token))) (users/create-user user-id login name email avatar-url token)))
(defn- get-or-create-user (defn- get-or-create-user
[token] [token]

View File

@ -59,7 +59,8 @@
:current-user user :current-user user
(ok {:repositories (->> (github/list-repos (:token user)) (ok {:repositories (->> (github/list-repos (:token user))
(map #(select-keys % (map #(select-keys %
[:id :html_url :name :full_name :description])))})) [:id :html_url
:name :full_name :description])))}))
(GET "/enabled-repositories" [] (GET "/enabled-repositories" []
:auth-rules authenticated? :auth-rules authenticated?
:current-user user :current-user user

View File

@ -0,0 +1,6 @@
(ns commiteth.activity
(:require [re-frame.core :as rf]))
(defn activity-page []
(fn []
[:div "This will be the activity view"]))

View File

@ -9,27 +9,16 @@
[commiteth.ajax :refer [load-interceptors!]] [commiteth.ajax :refer [load-interceptors!]]
[commiteth.handlers] [commiteth.handlers]
[commiteth.subscriptions] [commiteth.subscriptions]
[commiteth.activity :refer [activity-page]]
[commiteth.manage :refer [manage-page]] [commiteth.manage :refer [manage-page]]
[commiteth.issues :refer [issues-page]] [commiteth.issues :refer [issues-page]]
[commiteth.common :refer [input checkbox]] [commiteth.common :refer [input checkbox]]
[commiteth.subscriptions :refer [user-address-path]]
[commiteth.config :as config] [commiteth.config :as config]
[commiteth.svg :as svg] [commiteth.svg :as svg]
[clojure.set :refer [rename-keys]] [clojure.set :refer [rename-keys]]
[re-frisk.core :refer [enable-re-frisk!]]) [re-frisk.core :refer [enable-re-frisk!]])
(:import goog.History)) (:import goog.History))
(defn login-link []
(let [user (rf/subscribe [:user])]
(fn []
(if-let [login (:login @user)]
[:div.tabnav-actions
[:span.profile-link "Signed in as "
[:a {:href (str "https://github.com/" login)} login] " "]
[:a.btn.tabnav-button {:href "/logout"} "Sign out"]]
[:div.tabnav-actions.logged-out
[:a.btn.tabnav-button {:href js/authorizeUrl} "Sign in"]]))))
(defn error-pane (defn error-pane
[] []
(let [error (rf/subscribe [:error])] (let [error (rf/subscribe [:error])]
@ -50,7 +39,7 @@
(defn address-settings [] (defn address-settings []
(let [user (rf/subscribe [:user]) (let [user (rf/subscribe [:user])
user-id (:id @user) user-id (:id @user)
address (rf/subscribe [:get-in user-address-path])] address (rf/subscribe [:get-in [:user :address]])]
(fn [] (fn []
[:div.tabnav-actions.float-right [:div.tabnav-actions.float-right
[:div.tabnav-actions.logged-in [:div.tabnav-actions.logged-in
@ -63,59 +52,91 @@
:autoComplete "off", :autoComplete "off",
:size 55 :size 55
:type "text" :type "text"
:value-path user-address-path})] :value-path [:user :address]})]
[svg/octicon-broadcast]]]]))) [svg/octicon-broadcast]]]])))
(defn header []
(let [page (rf/subscribe [:page])
user (rf/subscribe [:user])]
(fn []
[:header.main-header
[:div.container
[:div.flex-table.mt-4.mb-4
[:header.logo]
[:a {:href "/"}
[:img {:src "/img/logo.svg"}
;;[:img {:src "img/logo.png", :alt "commiteth", :width "100"}] (defn user-dropdown [user items]
]] (let [dropdown-open? (r/atom false)]
[:div.flex-table-item.flex-table-item-primary (fn []
[:a {:href "/"}] (let [menu (if @dropdown-open?
[:h1.main-title.lh-condensed "commiteth"] [:div.ui.menu.transition.visible]
[:span.main-link [:div.ui.menu])]
"Earn ETH by committing to open source projects"]] [:div.ui.browse.item.dropdown
[:div.flex-table-item.flex-table-item-primary {:on-click #(swap! dropdown-open? not)}
[login-link]]] (:login user)
[:div.tabnav [:span.dropdown.icon]
(when @user [(address-settings)]) (into menu
[:nav.header-nav.tabnav-tabs (for [[target caption] items]
[:a.tabnav-tab ^{:key target} [:div.item
{:href "#" [:a
:class (when (= :issues @page) "selected")} (if (keyword? target)
[svg/octicon-repo] {:on-click #(rf/dispatch [target])}
"Open Bounties"] {:href target})
(when @user [:a.tabnav-tab caption]]))]))))
{:href "#/manage"
:class (when (= :manage @page) "selected")}
[svg/octicon-organization] (defn user-component [user]
"Manage Transactions"])]]]]))) (if user
(let [login (:login user)]
[:div.ui.text.menu.user-component
[:div.item
[:img.ui.mini.circular.image {:src (:avatar_url user)}]]
[user-dropdown user [[:update-address "Update address"]
["/logout" "Sign out"]]]])
[:a.ui.button.small {:href js/authorizeUrl} "Sign in"]))
(defn tabs []
(let [user (rf/subscribe [:user])
current-page (rf/subscribe [:page])]
(fn []
(let [tabs (apply conj [[:activity "Activity"]]
(when @user
[[:issues "Bounties"]
[:manage "Repositories"]]))]
(into [:div.ui.attached.tabular.menu.tiny]
(for [[page caption] tabs]
(let [props {:class (str "ui item"
(when (= @current-page page) " active"))
:on-click #(rf/dispatch [:set-active-page page])}]
^{:key page} [:div props caption])))))))
(defn page-header []
(let [user (rf/subscribe [:user])]
(fn []
[:div.vertical.segment.commiteth-header
[:div.ui.grid.container
[:div.twelve.wide.column
[:div.ui.image
[:img.left.aligned {:src "/img/logo.svg"}]]]
[:div.four.wide.column
[user-component @user]]
(when-not @user
[:div.ui.text.content
[:div.ui.divider.hidden]
[:h2.ui.header "Commit ETH"]
[:h2.ui.subheader "Earn ETH by committing to open source projects"]
[:div.ui.divider.hidden]])
[tabs]]])))
(def pages (def pages
{:issues #'issues-page {:activity #'activity-page
:issues #'issues-page
:manage #'manage-page}) :manage #'manage-page})
(defn page [] (defn page []
(fn [] (fn []
[:div.app [:div.ui.pusher
[:nav.main-navbar [:div.container]] [page-header]
[header]
[error-pane] [error-pane]
[(pages @(rf/subscribe [:page]))]])) [:div.ui.vertical.segment
[(pages @(rf/subscribe [:page]))]]]))
(secretary/set-config! :prefix "#") (secretary/set-config! :prefix "#")
(secretary/defroute "/" [] (secretary/defroute "/" []
(rf/dispatch [:set-active-page :issues])) (rf/dispatch [:set-active-page :activity]))
(secretary/defroute "/manage" [] (secretary/defroute "/manage" []
(if js/user (if js/user
@ -125,9 +146,9 @@
(defn hook-browser-navigation! [] (defn hook-browser-navigation! []
(doto (History.) (doto (History.)
(events/listen (events/listen
HistoryEventType/NAVIGATE HistoryEventType/NAVIGATE
(fn [event] (fn [event]
(secretary/dispatch! (.-token event)))) (secretary/dispatch! (.-token event))))
(.setEnabled true))) (.setEnabled true)))
(defn mount-components [] (defn mount-components []
@ -135,7 +156,10 @@
(defn load-user [] (defn load-user []
(when-let [login js/user] (when-let [login js/user]
(rf/dispatch [:set-active-user {:login login :id js/userId :token js/token}]))) (rf/dispatch [:set-active-user
{:login login
:id (js/parseInt js/userId)
:token js/token}])))
(defn load-issues [] (defn load-issues []
(rf/dispatch [:load-bounties])) (rf/dispatch [:load-bounties]))

View File

@ -3,7 +3,6 @@
(def default-db (def default-db
{:page :issues {:page :issues
:user nil :user nil
:user-profile nil
:repos-loading? false :repos-loading? false
:repos [] :repos []
:enabled-repos {} :enabled-repos {}

View File

@ -59,6 +59,13 @@
[:load-enabled-repos] [:load-enabled-repos]
[:load-owner-bounties]]})) [:load-owner-bounties]]}))
(reg-event-fx
:sign-out
(fn [{:keys [db]} [_]]
{:db (assoc db :user nil)
:http {:method GET
:url "/logout"}}))
(reg-event-fx (reg-event-fx
:load-bounties :load-bounties
(fn [{:keys [db]} [_]] (fn [{:keys [db]} [_]]
@ -106,7 +113,7 @@
(reg-event-db (reg-event-db
:set-user-profile :set-user-profile
(fn [db [_ user-profile]] (fn [db [_ user-profile]]
(assoc db :user-profile user-profile))) (assoc db :user (:user user-profile))))
(reg-event-db (reg-event-db
:set-user-repos :set-user-repos

View File

@ -1,7 +1,6 @@
(ns commiteth.manage (ns commiteth.manage
(:require [re-frame.core :as rf] (:require [re-frame.core :as rf]
[commiteth.common :refer [input checkbox]] [commiteth.common :refer [input checkbox]]
[commiteth.subscriptions :refer [user-address-path]]
[commiteth.issues :refer [issues-list-table issue-url]] [commiteth.issues :refer [issues-list-table issue-url]]
[clojure.set :refer [rename-keys]])) [clojure.set :refer [rename-keys]]))

View File

@ -45,5 +45,3 @@
:get-in :get-in
(fn [db [_ path]] (fn [db [_ path]]
(get-in db path))) (get-in db path)))
(def user-address-path [:user-profile :user :address])

View File

@ -1,4 +1,4 @@
(ns .commiteth.svg) (ns commiteth.svg)
(defn octicon-broadcast [] (defn octicon-broadcast []
[:svg.octicon.octicon-broadcast [:svg.octicon.octicon-broadcast

123
src/less/style.less Normal file
View File

@ -0,0 +1,123 @@
@font-face {
font-family: 'postgrotesk';
src: url('/fonts/PostGrotesk-Book.woff') format('woff');
font-weight: normal;
}
.ui.button {
background-color: #b0f1ee;
white-space: nowrap;
}
.commiteth-header {
background-color: #2f3f44!important;
border-radius: 0em;
padding-top: 3em;
}
.ui.text.menu .item {
color: #fff;
opacity: 0.98;
}
.ui.mini.circular.image {
height: 35px;
}
.ui.header {
//font-weight: normal;
color: #fff;
}
.ui.subheader {
color: #848e91;
font-weight: normal;
}
.ui.card a {
color: #848e91;
}
span.dropdown.icon {
content: url("/img/icon_dropdown.svg");
}
.user-component {
margin-top: 0px!important;
}
.ui.text.menu {
.item:hover {
color: #fff;
opacity: 0.98;
}
}
.ui.menu .ui.dropdown .menu>.item a {
color: #474951!important;
}
.ui {
font-family: 'postgrotesk';
}
.ui.tabular {
background-color: #2f3f44;
.ui.item {
padding: 1.5em;
color: #fff;
opacity: 0.98;
cursor: pointer;
}
.ui.item.active {
border-radius: .8rem .8rem 0 0!important;
color: #b0f1ee;
background-color: fade(#fff, 10%);
border-width: 0em;
cursor: default;
}
}
.repo-label {
color: #1bb5c1;
font-weight: bold;
}
.repo-description {
color: #474951;
}
.ui.button.repo-added-button {
color: white;
background-color: #61deb0;
cursor: default;
}
.ui.segment {
// margin: 2.5em;
// line-height: 3em;
border: #e7e7e7 solid 0.1em;
box-shadow: none;
border-radius: 0.3em;
h3 {
color: #474951;
}
a:not(.ui) {
color: #1bb5c1;
}
.label {
background-color: #61deb0;
color: #fff;
margin-right: 1em;
}
.time {
color: #a8aab1;
}
}
.time {
color: #a8aab1;
}

View File

@ -12,11 +12,9 @@
(def app-state (r/atom {:repo-state :disabled (def app-state (r/atom {:repo-state :disabled
:active-tab :activity})) :active-tab :activity
:user {:login "foobar"
:profile-image "https://randomuser.me/api/portraits/men/4.jpg"}}))
(deftest tests-can-be-done-here
(is (= 0 0)))
(defn fake-toggle-action [app-state] (defn fake-toggle-action [app-state]
(let [repo-state (:repo-state @app-state)] (let [repo-state (:repo-state @app-state)]
@ -55,10 +53,51 @@
app-state) app-state)
(defn top-hunters-dynamic [state-ratom] (defn dropdown-component [state-ratom]
[:div "TODO"]) (let [menu (if (:dropdown-open? @state-ratom)
[:div.ui.menu.transition.visible]
[:div.ui.menu])]
[:div.ui.right.dropdown.item
{:on-click #(swap! state-ratom update-in [:dropdown-open?] not)}
(:name @state-ratom)
[:i.dropdown.icon]
(into menu
(for [item (:items @state-ratom)]
^{:key item} [:div.item item]))]))
(defn dropdown-component2 [dropdown-open? caption items]
(let [menu (if @dropdown-open?
[:div.ui.menu.transition.visible]
[:div.ui.menu])]
[:div.ui.browse.item.dropdown
{:on-click #(swap! dropdown-open? not)}
caption
[:i.dropdown.icon]
(into menu
(for [item items]
^{:key item} [:div.item item]))]))
(defn user-component [state-ratom]
(if-let [user (get-in @state-ratom [:user])]
(let [login (:login user)]
[:div.ui.text.menu
[:div.item
[:img.ui.mini.circular.image {:src (:profile-image user)}]]
[dropdown-component2 (r/atom false) login ["Update address" "Sign out"]]
#_[:a.browse.item.username-label
;; {:href (str "https://github.com/" login)}
;; login
;; [:i.dropdown.icon]
;; [:a.ui.button.tiny {:href "/logout"} "Sign out"]
]])
[:a.ui.button.tiny {:href "#";;js/authorizeUrl
} "Sign in"]))
(defn activate-tab! [tab] (defn activate-tab! [tab]
(swap! app-state assoc :active-tab tab)) (swap! app-state assoc :active-tab tab))
@ -73,21 +112,21 @@
:on-click #(activate-tab! tab)}] :on-click #(activate-tab! tab)}]
^{:key tab} [:div props caption]))])) ^{:key tab} [:div props caption]))]))
(defn page-header-dynamic [state-ratom] (defn page-header-dynamic [state-ratom]
[:div.ui.grid.container.commiteth-header [:div.ui.grid.commiteth-header
[:div.ui.grid.four.column.container [:div.ui.grid.four.column.container
[:div.column [:div.column
[:img {:src "/img/logo.svg"}]] [:img {:src "/img/logo.svg"}]]
^{:key 1} [:div.column] ^{:key 1} [:div.column]
^{:key 2} [:div.column] ^{:key 2} [:div.column]
[:div.column [:div.column
[:div.ui.button.tiny "Sign in"]]] [user-component state-ratom]]]
[:div.ui.text.content.justified (when-not (:user @state-ratom)
[:div.ui.divider.hidden] [:div.ui.text.content.justified
[:h2.ui.header "Commit ETH"] [:div.ui.divider.hidden]
[:h3.ui.subheader "Earn ETH by committing to open source projects"] [:h2.ui.header "Commit ETH"]
[:div.ui.divider.hidden]] [:h3.ui.subheader "Earn ETH by committing to open source projects"]
[:div.ui.divider.hidden]])
[tabs @state-ratom]]) [tabs @state-ratom]])
(defcard-rg page-header (defcard-rg page-header
@ -95,9 +134,19 @@
app-state app-state
{}) {})
#_(defcard-rg login-button
[login-button {:login "foobar"}])
(defn top-hunters-dynamic [state-ratom]
[:div "TODO"])
#_{:activity-item {:type :bounty-created #_{:activity-item {:type :bounty-created
:issue-id 1 :issue-id 1
:foo 42}} :foo 42}}
(defcard-rg feeditem-bounty-created (defcard-rg feeditem-bounty-created
"An activity feed item with a bounty-created event" "An activity feed item with a bounty-created event"
@ -136,19 +185,6 @@
[:div.time "2h ago"]]]]]]]) [:div.time "2h ago"]]]]]]])
(defn dropdown-component [state-ratom]
(let [menu (if (:dropdown-open? @state-ratom)
[:div.ui.menu.transition.visible]
[:div.ui.menu])]
[:div.ui.right.dropdown.item
{:on-click #(swap! state-ratom update-in [:dropdown-open?] not)}
(:name @state-ratom)
[:i.dropdown.icon]
(into menu
(for [item (:items @state-ratom)]
^{:key item} [:div.item item]))]))
(defcard-rg user-menu (defcard-rg user-menu
"Top right user menu component" "Top right user menu component"
(fn [state _] (fn [state _]
@ -160,3 +196,6 @@
:name "Random User" :name "Random User"
:items ["foo" "bar"]}) :items ["foo" "bar"]})
{:inspect-data true}) {:inspect-data true})
(deftest tests-can-also-be-done-here
(is (= 0 0)))