diff --git a/.gitignore b/.gitignore index 31069bed..b74cf67d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ designs/deploy tags temp release +.clj-kondo +.lsp diff --git a/bb.edn b/bb.edn index 693f29df..21e4319a 100644 --- a/bb.edn +++ b/bb.edn @@ -46,7 +46,11 @@ run-server {:doc "Start Node.js API server process" :task (do (println "Starting Ethlance API server") - (shell {:dir "server"} "node out/ethlance_server.js"))} + (shell {:dir "server" + :extra-env {"ETHLANCE_ENV" "dev" + "UI_CONFIG_PATH" "../config/ui-config-dev.edn" + "SERVER_CONFIG_PATH" "../config/server-config-dev.edn"}} + "node out/ethlance_server.js"))} repl {:doc "Start REPL. Usage: bb repl [ui|server|test]" :requires ([clojure.edn :as edn]) :task (let [build-arg (or (first *command-line-args*) "ui") diff --git a/server/src/district/server/async_db.clj b/server/src/district/server/async_db.clj index 7cdec248..c3f48dd1 100644 --- a/server/src/district/server/async_db.clj +++ b/server/src/district/server/async_db.clj @@ -1,36 +1,40 @@ (ns district.server.async-db) -(defmacro with-async-resolver-tx [conn & body] + +(defmacro with-async-resolver-tx + [conn & body] `(js/Promise. - (fn [resolve# reject#] - (.then (district.server.async-db/get-connection) - (fn [~conn] - (district.server.async-db/begin-tx ~conn) - (cljs.core.async/take! (district.shared.async-helpers/safe-go ~@body) - (fn [v-or-err#] - - (if (cljs.core/instance? js/Error v-or-err#) - (do - (district.server.async-db/rollback-tx ~conn) - (district.server.async-db/release-connection ~conn) - (reject# v-or-err#)) - - (do - (district.server.async-db/commit-tx ~conn) - (district.server.async-db/release-connection ~conn) - (resolve# v-or-err#)))))))))) - -(defmacro with-async-resolver-conn [conn & body] + (fn [resolve# reject#] + (.then (district.server.async-db/get-connection) + (fn [~conn] + (district.server.async-db/begin-tx ~conn) + (cljs.core.async/take! (district.shared.async-helpers/safe-go ~@body) + (fn [v-or-err#] + + (if (cljs.core/instance? js/Error v-or-err#) + (do + (district.server.async-db/rollback-tx ~conn) + (district.server.async-db/release-connection ~conn) + (reject# v-or-err#)) + + (do + (district.server.async-db/commit-tx ~conn) + (district.server.async-db/release-connection ~conn) + (resolve# v-or-err#)))))))))) + + +(defmacro with-async-resolver-conn + [conn & body] `(js/Promise. - (fn [resolve# reject#] - (.then (district.server.async-db/get-connection) - (fn [~conn] - (cljs.core.async/take! (district.shared.async-helpers/safe-go ~@body) - (fn [v-or-err#] - - ;; here we have the result so it is safe to release the connection - (district.server.async-db/release-connection ~conn) - - (if (cljs.core/instance? js/Error v-or-err#) - (reject# v-or-err#) - (resolve# v-or-err#))))))))) + (fn [resolve# reject#] + (.then (district.server.async-db/get-connection) + (fn [~conn] + (cljs.core.async/take! (district.shared.async-helpers/safe-go ~@body) + (fn [v-or-err#] + + ;; here we have the result so it is safe to release the connection + (district.server.async-db/release-connection ~conn) + + (if (cljs.core/instance? js/Error v-or-err#) + (reject# v-or-err#) + (resolve# v-or-err#))))))))) diff --git a/server/src/district/server/async_db.cljs b/server/src/district/server/async_db.cljs index a597a192..902b354f 100644 --- a/server/src/district/server/async_db.cljs +++ b/server/src/district/server/async_db.cljs @@ -1,66 +1,80 @@ (ns district.server.async-db (:refer-clojure :exclude [get run!]) - (:require ["pg" :as pg] - [clojure.string :as str] - [district.server.config :refer [config]] - [district.server.logging] - [district.shared.async-helpers :refer [safe-go n munge (str/replace "_STAR_" "*") (str/replace "_PERCENT_" "%"))) -(def transform-result-keys-fn (comp keyword - demunge - #(str/replace % #"_slash_" "_SLASH_"))) -(defn- map-keys [f m] +(def transform-result-keys-fn + (comp keyword + demunge + #(str/replace % #"_slash_" "_SLASH_"))) + + +(defn- map-keys + [f m] (into {} (map (fn [[k v]] [(f k) v]) m))) + (defn get-connection "Returns a db connection from the pool." [] (.connect (:connection-pool @db))) + (defn release-connection "Returns a db connection to the pool." [conn] (.release conn)) + (defn run! "Given a db connection and a honey sql query runs it and returns its result." [conn statement] (safe-go - (let [[query-str & values] (binding [sql-format/*name-transform-fn* sql-name-transform-fn] - (sql/format statement - :parameterizer :postgresql - :allow-namespaced-names? true)) - res (js (or values []))))] - (->> (js->clj (.-rows res)) - (map #(map-keys transform-result-keys-fn %)))))) + (let [[query-str & values] (binding [sql-format/*name-transform-fn* sql-name-transform-fn] + (sql/format statement + :parameterizer :postgresql + :allow-namespaced-names? true)) + res (js (or values []))))] + (->> (js->clj (.-rows res)) + (map #(map-keys transform-result-keys-fn %)))))) + (defn run-raw! "Given a db connection and raw SQL string run & return result" @@ -75,34 +89,42 @@ (->> (js->clj (.-rows res)) (map #(map-keys transform-result-keys-fn %))))))) + (defn all "Given a db connection and a honey sql query runs it and returns resultset rows." [conn q] (run! conn q)) + (defn get "Given a db connection and a honey sql query runs it and returns first resultset row." [conn q] (safe-go - (first (! chan close! go put!] - :include-macros - true] - [district.server.smart-contracts :as contracts])) + (:require + [clojure.core.async + :as + async + :refer + [>! chan close! go put!] + :include-macros + true] + [district.server.smart-contracts :as contracts])) + (defn call "Call the given `contract-address` with the kebab-case formatted @@ -36,14 +38,14 @@ (if (instance? js/Promise result) (-> result (.then - ;; Success - (fn [result] - (put! success-channel result) - (close! error-channel)) - ;; Failure - (fn [error-object] - (put! error-channel error-object) - (close! success-channel)))) + ;; Success + (fn [result] + (put! success-channel result) + (close! error-channel)) + ;; Failure + (fn [error-object] + (put! error-channel error-object) + (close! success-channel)))) ;; No promise, pass the result to the success channel. (do (>! success-channel result) (close! error-channel))))) diff --git a/server/src/ethlance/server/contract/ds_auth.cljs b/server/src/ethlance/server/contract/ds_auth.cljs index b454b3e2..c8bed5a2 100644 --- a/server/src/ethlance/server/contract/ds_auth.cljs +++ b/server/src/ethlance/server/contract/ds_auth.cljs @@ -1,13 +1,16 @@ (ns ethlance.server.contract.ds-auth - (:require [ethlance.server.contract :refer [call]])) + (:require + [ethlance.server.contract :refer [call]])) + (defn owner "Get the owner address from the DSAuth contract defined by `contract-key`." [contract-key] (call - :contract-key contract-key - :method-name :owner [])) + :contract-key contract-key + :method-name :owner [])) + (defn set-owner! "Set the owner address for the DSAuth contract defined by @@ -23,17 +26,19 @@ " [contract-key _ & [opts]] (call - :contract-key contract-key - :method-name :set-owner - :contract-options (merge {:gas 100000} opts))) + :contract-key contract-key + :method-name :set-owner + :contract-options (merge {:gas 100000} opts))) + (defn authority "Get the authority address from the DSAuth contract defined by `contract-key`." [contract-key] (call - :contract-key contract-key - :method-name :authority)) + :contract-key contract-key + :method-name :authority)) + (defn set-authority! "Set the DSAuthority implementation defined by the contract address @@ -41,7 +46,7 @@ `contract-key`." [contract-key new-authority & [opts]] (call - :contract-key contract-key - :method-name :set-authority - :contract-arguments [new-authority] - :contract-options (merge {:gas 100000} opts))) + :contract-key contract-key + :method-name :set-authority + :contract-arguments [new-authority] + :contract-options (merge {:gas 100000} opts))) diff --git a/server/src/ethlance/server/contract/ethlance.cljs b/server/src/ethlance/server/contract/ethlance.cljs index ca665b61..8b3e0cfd 100644 --- a/server/src/ethlance/server/contract/ethlance.cljs +++ b/server/src/ethlance/server/contract/ethlance.cljs @@ -1,15 +1,19 @@ (ns ethlance.server.contract.ethlance - (:require [district.server.smart-contracts :as smart-contracts])) + (:require + [district.server.smart-contracts :as smart-contracts])) -(defn initialize [job-proxy-address] + +(defn initialize + [job-proxy-address] (smart-contracts/contract-send :ethlance :initialize [job-proxy-address] {:gas 6000000})) + (defn create-job ([creator offered-values arbiters ipfs-data] (create-job creator offered-values arbiters ipfs-data {})) ([creator offered-values arbiters ipfs-data merged-opts] - (smart-contracts/contract-send - :ethlance :create-job - [creator offered-values arbiters ipfs-data] - (merge {:gas 6000000} merged-opts)))) + (smart-contracts/contract-send + :ethlance :create-job + [creator offered-values arbiters ipfs-data] + (merge {:gas 6000000} merged-opts)))) diff --git a/server/src/ethlance/server/contract/job.cljs b/server/src/ethlance/server/contract/job.cljs index 5ef38358..5a246a88 100644 --- a/server/src/ethlance/server/contract/job.cljs +++ b/server/src/ethlance/server/contract/job.cljs @@ -1,10 +1,12 @@ (ns ethlance.server.contract.job (:require [cljs.core.async :refer [map (js->clj amounts-from-contract))))) -(defn get-deposits [job-address depositor] + +(defn get-deposits + [job-address depositor] (go (let [amounts-from-contract (map (js->clj amounts-from-contract))))) diff --git a/server/src/ethlance/server/core.cljs b/server/src/ethlance/server/core.cljs index a9630a5a..ac3c97e4 100644 --- a/server/src/ethlance/server/core.cljs +++ b/server/src/ethlance/server/core.cljs @@ -1,18 +1,16 @@ (ns ethlance.server.core (:require ["fs" :as fs] - [alphabase.base58 :as base58] - [alphabase.hex :as hex] - [district.server.config :refer [config]] [district.server.async-db] - [district.server.db.honeysql-extensions] + [district.server.config :refer [config]] [district.server.db] - [ethlance.server.db] - [district.server.web3] + [district.server.db.honeysql-extensions] [district.server.logging] [district.server.smart-contracts] + [district.server.web3] [district.server.web3-events] [district.shared.async-helpers :as async-helpers :refer [safe-go]] + [ethlance.server.db] [ethlance.server.graphql.server] [ethlance.server.ipfs] [ethlance.server.syncer] @@ -20,23 +18,22 @@ [ethlance.shared.smart-contracts-dev :as smart-contracts-dev] [ethlance.shared.smart-contracts-prod :as smart-contracts-prod] [ethlance.shared.smart-contracts-qa :as smart-contracts-qa] + [ethlance.shared.utils :include-macros true :as shared-utils] [mount.core :as mount] - [ethlance.shared.utils :include-macros true :refer [slurp] :as shared-utils] [taoensso.timbre :refer [merge-config!] :as log])) + (def environment (shared-utils/get-environment)) (println "Ethlance server starting in environment:" environment) + (def contracts-var (condp = environment "prod" #'smart-contracts-prod/smart-contracts "qa" #'smart-contracts-qa/smart-contracts - "dev" #'smart-contracts-dev/smart-contracts - )) + "dev" #'smart-contracts-dev/smart-contracts)) -(defn read-edn-sync [path] - (cljs.reader/read-string (.readFileSync fs path "utf8"))) (def default-config {:web3 {:url "ws://127.0.0.1:8549"} @@ -61,8 +58,7 @@ :skip-past-events-replay? false :write-events-into-file? true :load-checkpoint ethlance.server.db/load-processed-events-checkpoint - :save-checkpoint ethlance.server.db/save-processed-events-checkpoint - } + :save-checkpoint ethlance.server.db/save-processed-events-checkpoint} :smart-contracts {:contracts-var contracts-var :contracts-build-path "../resources/public/contracts/build" :print-gas-usage? false @@ -82,6 +78,7 @@ :logging {:level "debug" :console? true}}) + (def config-dev {:ipfs {:host "https://ipfs.infura.io:5001" @@ -90,14 +87,18 @@ :auth {:username "xxx" :password "xxx"}}}) -(defn env-config [env] + +(defn env-config + [env] (shared-utils/deep-merge default-config (if (= env "dev") config-dev shared-config/config))) -(defn -main [& _] + +(defn -main + [& _] (log/info "Initializing Server...") (async-helpers/extend-promises-as-channels!) (merge-config! {:ns-blacklist ["district.server.smart-contracts"]}) @@ -113,7 +114,7 @@ (log/error "Something went wrong when starting the application" {:error e}))))) -; When compiled for a command-line target, whatever function *main-cli-fn* is -; set to will be called with the command-line argv as arguments. -; See: https://cljs.github.io/api/cljs.core/STARmain-cli-fnSTAR +;; When compiled for a command-line target, whatever function *main-cli-fn* is +;; set to will be called with the command-line argv as arguments. +;; See: https://cljs.github.io/api/cljs.core/STARmain-cli-fnSTAR (set! *main-cli-fn* -main) diff --git a/server/src/ethlance/server/db.cljs b/server/src/ethlance/server/db.cljs index 8f1473a3..d4709fc6 100644 --- a/server/src/ethlance/server/db.cljs +++ b/server/src/ethlance/server/db.cljs @@ -1,31 +1,37 @@ (ns ethlance.server.db "Represents the ethlance in-memory sqlite database. Contains a mount component for creating the in-memory database upon initial load." - (:require [clojure.pprint :as pprint] - [clojure.set :as set] - [cljs.core.async :as async :refer [go-loop > (partition 2 r) - (map vec) - (into {:select select-fields})))))) - all-tables (if table - [(name table)] - (->> (> (partition 2 r) + (map vec) + (into {:select select-fields})))))) + all-tables (if table + [(name table)] + (->> (> (:table-columns table-schema) (filter (comp keyword? first)) (filter (comp type-pred second))))) + (defn- get-table-column-names "Retrieves the table column names for a given table schema defined by their `table-name`." [table-name] @@ -499,59 +507,66 @@ (map first) (filter keyword?)))) -(defn filter-tables [table-names schema] + +(defn filter-tables + [table-names schema] (filter #((set table-names) (:table-name %)) schema)) + (defn create-db! "Creates the database with tables defined in the `database-schema`." [conn] (safe-go - (log/info "Creating Sqlite Database...") - (doseq [{:keys [table-name table-columns]} database-schema] - (> auto-increment-columns - (filter (fn [col] (nil? (get item col))))))) - statement {:insert-into table-name - :columns (keys item) - :values [(->> (vals item) - (map #(if (keyword? %) (name %) %)))] - :returning [:*]}] - (not-empty (try - (first (> auto-increment-columns + (filter (fn [col] (nil? (get item col))))))) + statement {:insert-into table-name + :columns (keys item) + :values [(->> (vals item) + (map #(if (keyword? %) (name %) %)))] + :returning [:*]}] + (not-empty (try + (first ( (count list-keys) 0) - (concat - [:and] - (for [list-key list-keys] - [:= list-key (get item list-key)])) - [:= 1 1])] - (not-empty ( (count list-keys) 0) + (concat + [:and] + (for [list-key list-keys] + [:= list-key (get item list-key)])) + [:= 1 1])] + (not-empty ( ( (js checkpoint)) - :created-at (new js/Date)}]} )] + :created-at (new js/Date)}]})] (when (fn? callback) (take! result-chan callback)))))) + (defn ready-state? [] (go-loop [] @@ -1012,19 +1088,21 @@ ( ( ( ( (! chan close! go] - :include-macros - true])) + (:require + [clojure.core.async + :as + async + :refer + [>! chan close! go] + :include-macros + true])) + (def fs (js/require "fs")) + (defn read-file [file-path] (let [success-chan (chan 1) error-chan (chan 1)] (go (.readFile - fs file-path - (fn [error result] - (when error - (>! error-chan error) - (close! success-chan)) - (when result - (>! success-chan result) - (close! error-chan))))) + fs file-path + (fn [error result] + (when error + (>! error-chan error) + (close! success-chan)) + (when result + (>! success-chan result) + (close! error-chan))))) [success-chan error-chan])) diff --git a/server/src/ethlance/server/generator/choice_collections.cljs b/server/src/ethlance/server/generator/choice_collections.cljs index 3ce8cccc..2d0f69f7 100644 --- a/server/src/ethlance/server/generator/choice_collections.cljs +++ b/server/src/ethlance/server/generator/choice_collections.cljs @@ -7,7 +7,7 @@ ["john.doe@gmail.com" "jane.doe@gmail.com" "test@hotmail.com" - "bacon_cheeseburger@yahoo.ca" + "bacon_cheeseburger@yahoo.ca" "xXEthSlayerXx@gmail.com" "2TheMoon@dogemail.com" "2ManyCooks@yahoo.com" diff --git a/server/src/ethlance/server/graphql/authorization.cljs b/server/src/ethlance/server/graphql/authorization.cljs index 037d2196..247c2b32 100644 --- a/server/src/ethlance/server/graphql/authorization.cljs +++ b/server/src/ethlance/server/graphql/authorization.cljs @@ -1,28 +1,38 @@ (ns ethlance.server.graphql.authorization - (:require [cljs.nodejs :as nodejs] - [taoensso.timbre :as log] - [district.shared.error-handling :refer [try-catch]])) + (:require + [cljs.nodejs :as nodejs] + [district.shared.error-handling :refer [try-catch]] + [taoensso.timbre :as log])) + (defonce JsonWebToken (nodejs/require "jsonwebtoken")) (defonce EthSigUtil (nodejs/require "eth-sig-util")) -(defn recover-personal-signature [data data-signature] + +(defn recover-personal-signature + [data data-signature] (js-invoke EthSigUtil "recoverPersonalSignature" #js {:data data :sig data-signature})) -(defn create-jwt [address secret] + +(defn create-jwt + [address secret] (js-invoke JsonWebToken "sign" #js {:userAddress address} secret)) -(defn parse-jwt [token secret] + +(defn parse-jwt + [token secret] (js-invoke JsonWebToken "verify" token secret)) -(defn token->user [access-token sign-in-secret] + +(defn token->user + [access-token sign-in-secret] (try-catch - (cond - (nil? access-token) - (log/info "No access-token header present in request") + (cond + (nil? access-token) + (log/info "No access-token header present in request") - access-token - (let [user {:user/id (aget (parse-jwt access-token sign-in-secret) "userAddress")}] - user) + access-token + (let [user {:user/id (aget (parse-jwt access-token sign-in-secret) "userAddress")}] + user) - :else (log/info "Invalid access-token header")))) + :else (log/info "Invalid access-token header")))) diff --git a/server/src/ethlance/server/graphql/middlewares.cljs b/server/src/ethlance/server/graphql/middlewares.cljs index 93135405..cf0cbec8 100644 --- a/server/src/ethlance/server/graphql/middlewares.cljs +++ b/server/src/ethlance/server/graphql/middlewares.cljs @@ -1,14 +1,17 @@ (ns ethlance.server.graphql.middlewares - (:require [district.graphql-utils :as graphql-utils] - [district.server.config :as config] - [district.shared.async-helpers :as async-helpers] - [ethlance.server.graphql.authorization :as authorization] - [taoensso.timbre :as log] - [clojure.string :as string])) + (:require + [clojure.string :as string] + [district.graphql-utils :as graphql-utils] + [district.server.config :as config] + [district.shared.async-helpers :as async-helpers] + [ethlance.server.graphql.authorization :as authorization] + [taoensso.timbre :as log])) + ;; TODO : root-value->clj middleware -(defn response->gql-middleware [resolve root args context info] +(defn response->gql-middleware + [resolve root args context info] (let [response (resolve root args context info)] (if (async-helpers/promise? response) (-> response @@ -19,10 +22,14 @@ (throw (new js/Error error))))) (graphql-utils/clj->gql response)))) -(defn args->clj-middleware [resolve root args context info] + +(defn args->clj-middleware + [resolve root args context info] (resolve root (graphql-utils/gql->clj args) context info)) -(defn logging-middleware [resolve root args context info] + +(defn logging-middleware + [resolve root args context info] (log/debug "Received graphql request" {:res resolve :root root :args args @@ -30,10 +37,14 @@ :info info}) (resolve root args context info)) -(defn- bearer-token [auth-header] + +(defn- bearer-token + [auth-header] (second (string/split auth-header "Bearer "))) -(defn current-user-express-middleware [req _ next] + +(defn current-user-express-middleware + [req _ next] (let [secret (-> @config/config :graphql :sign-in-secret) headers (js->clj (.-headers req) :keywordize-keys true) auth-header (:authorization headers) diff --git a/server/src/ethlance/server/graphql/resolvers.cljs b/server/src/ethlance/server/graphql/resolvers.cljs index bb7aff7a..4c28ebb4 100644 --- a/server/src/ethlance/server/graphql/resolvers.cljs +++ b/server/src/ethlance/server/graphql/resolvers.cljs @@ -1,5 +1,7 @@ (ns ethlance.server.graphql.resolvers (:require + [camel-snake-kebab.core] + [clojure.string :as string] [district.graphql-utils :as graphql-utils] [district.server.async-db :as db :include-macros true] [district.shared.async-helpers :refer [clj-map [obj] + +(defn js-obj->clj-map + [obj] (let [obj-keys (district.graphql-utils/gql->clj (js-keys obj)) keywordize (fn [k] (graphql-utils/gql-name->kw k)) ; "user_someThing" => :user/some-thing assoc-keywordized (fn [acc js-key] (assoc acc (keywordize js-key) (aget obj js-key)))] (reduce assoc-keywordized {} obj-keys))) + (defn- paged-query [conn query limit offset] (safe-go @@ -40,7 +45,9 @@ :end-cursor end-cursor :has-next-page (< end-cursor total-count)}))) -(defn- match-all [query {:keys [:join-table :on-column :column :all-values]}] + +(defn- match-all + [query {:keys [:join-table :on-column :column :all-values]}] (reduce-kv (fn [result index value] (let [table-name (-> query :from first name) alias (str "c" index) @@ -57,24 +64,28 @@ query all-values)) -(defn user-search-resolver [_ {:keys [:limit :offset :user/id :user/name :order-by :order-direction] - :as args} _] + +(defn user-search-resolver + [_ {:keys [:limit :offset :user/id :user/name :order-by :order-direction] + :as args} _] (db/with-async-resolver-conn conn - (log/debug "user-search-resolver" args) - (let [query (cond-> {:select [:*] - :from [:Users]} + (log/debug "user-search-resolver" args) + (let [query (cond-> {:select [:*] + :from [:Users]} - id (sql-helpers/merge-where [:= :Users.user/id id]) + id (sql-helpers/merge-where [:= :Users.user/id id]) - name (sql-helpers/merge-where [:= :Users.user/user-name name]) + name (sql-helpers/merge-where [:= :Users.user/user-name name]) - order-by (sql-helpers/merge-order-by [[(get {:date-registered :user/date-registered - :date-updated :user/date-updated} - (graphql-utils/gql-name->kw order-by)) - (or (keyword order-direction) :asc)]]))] - (kw order-by)) + (or (keyword order-direction) :asc)]]))] + (clj parent) @@ -85,225 +96,254 @@ :from [:Users] :where [:ilike user-id :Users.user/id]}))))) -(defn feedback->from-user-resolver [root _ _] + +(defn feedback->from-user-resolver + [root _ _] (let [id (:feedback/from-user-address (graphql-utils/gql->clj root))] (user-resolver nil {:user/id id} nil))) -(defn feedback->to-user-resolver [root _ _] + +(defn feedback->to-user-resolver + [root _ _] (let [id (:feedback/to-user-address (graphql-utils/gql->clj root))] (user-resolver nil {:user/id id} nil))) -(defn user->is-registered-for-role-resolver [role root _ _] + +(defn user->is-registered-for-role-resolver + [role root _ _] (db/with-async-resolver-conn conn - (let [{:keys [:user/id] :as user} (graphql-utils/gql->clj root) - role-table (keyword (clojure.string/capitalize (name role))) - role-column (keyword (str (name role-table) ".user") :id) - res (is-registered-employer-resolver" user) - (not (= 0 (int (:count res))))))) + (let [{:keys [:user/id] :as user} (graphql-utils/gql->clj root) + role-table (keyword (clojure.string/capitalize (name role))) + role-column (keyword (str (name role-table) ".user") :id) + res (is-registered-employer-resolver" user) + (not (= 0 (int (:count res))))))) + (def user->is-registered-candidate-resolver (partial user->is-registered-for-role-resolver :candidate)) (def user->is-registered-employer-resolver (partial user->is-registered-for-role-resolver :employer)) (def user->is-registered-arbiter-resolver (partial user->is-registered-for-role-resolver :arbiter)) -(defn user->languages-resolvers [root _ _] + +(defn user->languages-resolvers + [root _ _] (db/with-async-resolver-conn conn - (let [{:keys [:user/id] :as user} (graphql-utils/gql->clj root)] - (log/debug "user->languages-resolvers" user) - (map :language/id - (employer-query {:select[:*] - :from [:Employer] - :join [:Users [:= :Users.user/id :Employer.user/id] - :Job [:= :Job.job/creator :Employer.user/id]]}) - -(defn job->token-details-resolver [parent args context info] + (let [{:keys [:user/id] :as user} (graphql-utils/gql->clj root)] + (log/debug "user->languages-resolvers" user) + (map :language/id + (employer-query + {:select [:*] + :from [:Employer] + :join [:Users [:= :Users.user/id :Employer.user/id] + :Job [:= :Job.job/creator :Employer.user/id]]}) + + +(defn job->token-details-resolver + [parent _args _context _info] (log/debug "job->token-details-resolver" parent) (db/with-async-resolver-conn conn - (let [clj-parent (js->clj parent) - token-address (get (js->clj parent :keywordize-keys) "job_tokenAddress") - query {:select [:*] - :from [:TokenDetail] - :where [:= :TokenDetail.token-detail/id token-address]}] - (fee-token-details-resolver [parent args context info] + (let [token-address (get (js->clj parent :keywordize-keys) "job_tokenAddress") + query {:select [:*] + :from [:TokenDetail] + :where [:= :TokenDetail.token-detail/id token-address]}] + (fee-token-details-resolver + [parent _args _context _info] (log/debug "job->token-details-resolver" parent) (db/with-async-resolver-conn conn - (let [clj-parent (js->clj parent) - token-address "0x0000000000000000000000000000000000000000" ; arbiter fees are always ETH - query {:select [:*] - :from [:TokenDetail] - :where [:= :TokenDetail.token-detail/id token-address]}] - (user-resolver [parent args context info] + (let [token-address "0x0000000000000000000000000000000000000000" ; arbiter fees are always ETH + query {:select [:*] + :from [:TokenDetail] + :where [:= :TokenDetail.token-detail/id token-address]}] + (user-resolver + [parent _args _context _info] (log/debug "participant->user-resolver") (db/with-async-resolver-conn conn - (let [clj-parent (graphql-utils/gql->clj parent) - user-id (:user/id clj-parent) - query {:select [:*] - :from [:Users] - :where [:ilike :Users.user/id user-id]}] - (clj parent) + user-id (:user/id clj-parent) + query {:select [:*] + :from [:Users] + :where [:ilike :Users.user/id user-id]}] + (employer-resolver [parent args context info] +(defn job->employer-resolver + [parent args _context _info] (db/with-async-resolver-conn conn - (log/debug "job->employer-resolver contract:" (:contract args)) - (let [contract (:job/id (graphql-utils/gql->clj parent)) - query (sql-helpers/merge-where job->employer-query [:= contract :Job.job/id])] - (employer-resolver contract:" (:contract args)) + (let [contract (:job/id (graphql-utils/gql->clj parent)) + query (sql-helpers/merge-where job->employer-query [:= contract :Job.job/id])] + (arbiter-query {:select[:*] - :from [:Arbiter] - :join [:JobArbiter [:= :JobArbiter.user/id :Arbiter.user/id] - :Job [:= :Job.job/id :JobArbiter.job/id]]}) -(defn job->arbiter-resolver [parent args context info] +(def ^:private job->arbiter-query + {:select [:*] + :from [:Arbiter] + :join [:JobArbiter [:= :JobArbiter.user/id :Arbiter.user/id] + :Job [:= :Job.job/id :JobArbiter.job/id]]}) + + +(defn job->arbiter-resolver + [parent args _context _info] (db/with-async-resolver-conn conn - (log/debug "job->arbiter-resolver contract:" (:contract args)) - (let [contract (:job/id (graphql-utils/gql->clj parent)) - query (sql-helpers/merge-where job->arbiter-query [:and - [:= contract :Job.job/id] - [:= "accepted" :JobArbiter.job-arbiter/status]])] - (arbiter-resolver contract:" (:contract args)) + (let [contract (:job/id (graphql-utils/gql->clj parent)) + query (sql-helpers/merge-where job->arbiter-query [:and + [:= contract :Job.job/id] + [:= "accepted" :JobArbiter.job-arbiter/status]])] + (clj raw-parent) - address-from-parent (or - (:employer/id parent) - (:job/creator parent)) - address (or address-from-args address-from-parent)] - (feedback-resolver [root {:keys [:limit :offset] :as args} _] + (log/debug "employer-resolver" args) + (let [address-from-args (:user/id args) + parent (graphql-utils/gql->clj raw-parent) + address-from-parent (or + (:employer/id parent) + (:job/creator parent)) + address (or address-from-args address-from-parent)] + (feedback-resolver + [root {:keys [:limit :offset] :as args} _] (db/with-async-resolver-conn conn - (let [{:keys [:user/id] :as employer} (graphql-utils/gql->clj root) - query (sql-helpers/merge-where user-feedback-query [:ilike id :JobStoryFeedbackMessage.user/id])] - (log/debug "employer->feedback-resolver" {:employer employer :args args}) - (clj root) + query (sql-helpers/merge-where user-feedback-query [:ilike id :JobStoryFeedbackMessage.user/id])] + (log/debug "employer->feedback-resolver" {:employer employer :args args}) + (clj raw-parent) - address-from-parent (or (:arbiter/id parent) (:user/id parent)) - address (or address-from-args address-from-parent)] - (clj raw-parent) + address-from-parent (or (:arbiter/id parent) (:user/id parent)) + address (or address-from-args address-from-parent)] + (clj-map search-params) - categories-and (when (:category search-params) [(:category search-params)]) - categories-or nil ; Not used, switch form ...-and if want this behaviour - skills-and (or (js->clj (:skills search-params)) []) - skills-or nil ; Not used, switch form ...-and if want this behaviour - min-rating (:feedback-min-rating search-params) - max-rating (:feedback-max-rating search-params) - min-fee (:min-fee search-params) - max-fee (:max-fee search-params) - min-num-feedbacks (:min-num-feedbacks search-params) - country (:country search-params) - user-name (:name search-params) - - query (cond-> (merge arbiter-query {:modifiers [:distinct]}) - min-rating (sql-helpers/merge-where [:<= min-rating :Arbiter.arbiter/rating]) - max-rating (sql-helpers/merge-where [:>= max-rating :Arbiter.arbiter/rating]) - (and (nil? min-rating) - (not (nil? max-rating))) (sql-helpers/merge-where :or [:= nil :Arbiter.arbiter/rating]) - id (sql-helpers/merge-where [:ilike :Arbiter.user/id id]) - - country (sql-helpers/merge-where [:= country :Users.user/country]) - - categories-or (sql-helpers/merge-left-join :ArbiterCategory - [:= :ArbiterCategory.user/id :Arbiter.user/id]) - - categories-or (sql-helpers/merge-where [:in :ArbiterCategory.category/id categories-or]) - - categories-and (match-all {:join-table :ArbiterCategory - :on-column :user/id - :column :category/id - :all-values categories-and}) - - skills-or (sql-helpers/merge-left-join :ArbiterSkill - [:= :ArbiterSkill.user/id :Arbiter.user/id]) - - skills-or (sql-helpers/merge-where [:in :ArbiterSkill.skill/id skills-or]) - min-fee (sql-helpers/merge-where [:>= :Arbiter.arbiter/fee min-fee]) - max-fee (sql-helpers/merge-where [:<= :Arbiter.arbiter/fee max-fee]) - min-num-feedbacks (sql-helpers/merge-where - [:<= min-num-feedbacks - {:select [(sql/call :count :*)] - :from [:JobStoryFeedbackMessage] - :where [:= :JobStoryFeedbackMessage.user/id :Arbiter.user/id]}]) - - (not (empty? skills-and)) (match-all {:join-table :ArbiterSkill - :on-column :user/id - :column :skill/id - :all-values skills-and}) - - user-name (sql-helpers/merge-where [:ilike :Users.user/name (sql/raw (str "'%" user-name "%'")) ]) - order-by (sql-helpers/merge-order-by [[(get {:date-registered :user/date-registered - :date-updated :user/date-updated} - (graphql-utils/gql-name->kw order-by)) - (or (keyword order-direction) :asc)]]))] - (feedback-resolver [root {:keys [:limit :offset] :as args} {:keys [conn]}] + (log/debug "arbiter-search-resolver" args) + (let [search-params (js-obj->clj-map search-params) + categories-and (when (:category search-params) [(:category search-params)]) + categories-or nil ; Not used, switch form ...-and if want this behaviour + skills-and (or (js->clj (:skills search-params)) []) + skills-or nil ; Not used, switch form ...-and if want this behaviour + min-rating (:feedback-min-rating search-params) + max-rating (:feedback-max-rating search-params) + min-fee (:min-fee search-params) + max-fee (:max-fee search-params) + min-num-feedbacks (:min-num-feedbacks search-params) + country (:country search-params) + user-name (:name search-params) + + query (cond-> (merge arbiter-query {:modifiers [:distinct]}) + min-rating (sql-helpers/merge-where [:<= min-rating :Arbiter.arbiter/rating]) + max-rating (sql-helpers/merge-where [:>= max-rating :Arbiter.arbiter/rating]) + (and (nil? min-rating) + (not (nil? max-rating))) (sql-helpers/merge-where :or [:= nil :Arbiter.arbiter/rating]) + id (sql-helpers/merge-where [:ilike :Arbiter.user/id id]) + + country (sql-helpers/merge-where [:= country :Users.user/country]) + + categories-or (sql-helpers/merge-left-join :ArbiterCategory + [:= :ArbiterCategory.user/id :Arbiter.user/id]) + + categories-or (sql-helpers/merge-where [:in :ArbiterCategory.category/id categories-or]) + + categories-and (match-all {:join-table :ArbiterCategory + :on-column :user/id + :column :category/id + :all-values categories-and}) + + skills-or (sql-helpers/merge-left-join :ArbiterSkill + [:= :ArbiterSkill.user/id :Arbiter.user/id]) + + skills-or (sql-helpers/merge-where [:in :ArbiterSkill.skill/id skills-or]) + min-fee (sql-helpers/merge-where [:>= :Arbiter.arbiter/fee min-fee]) + max-fee (sql-helpers/merge-where [:<= :Arbiter.arbiter/fee max-fee]) + min-num-feedbacks (sql-helpers/merge-where + [:<= min-num-feedbacks + {:select [(sql/call :count :*)] + :from [:JobStoryFeedbackMessage] + :where [:= :JobStoryFeedbackMessage.user/id :Arbiter.user/id]}]) + + (not (empty? skills-and)) (match-all {:join-table :ArbiterSkill + :on-column :user/id + :column :skill/id + :all-values skills-and}) + + user-name (sql-helpers/merge-where [:ilike :Users.user/name (sql/raw (str "'%" user-name "%'"))]) + order-by (sql-helpers/merge-order-by [[(get {:date-registered :user/date-registered + :date-updated :user/date-updated} + (graphql-utils/gql-name->kw order-by)) + (or (keyword order-direction) :asc)]]))] + (feedback-resolver + [root {:keys [:limit :offset] :as args} {:keys [conn]}] (db/with-async-resolver-conn conn - (let [arbiter (graphql-utils/gql->clj root) - user-id (:user/id arbiter) - query (sql-helpers/merge-where user-feedback-query [:ilike user-id :JobStoryFeedbackMessage.user/id])] - (log/debug "arbiter->feedback-resolver" {:arbiter arbiter :args args}) - (clj root) + user-id (:user/id arbiter) + query (sql-helpers/merge-where user-feedback-query [:ilike user-id :JobStoryFeedbackMessage.user/id])] + (log/debug "arbiter->feedback-resolver" {:arbiter arbiter :args args}) + (user-type-resolver [user-type-column root _ _] + +(defn feedback->user-type-resolver + [user-type-column root _ _] (db/with-async-resolver-conn conn - (let [feedback (graphql-utils/gql->clj root) - message-id (:message/id feedback) - arbiter-status "accepted" - job-story-id (:job-story/id feedback) - where-condition [:and - [:= message-id :jsfm.message/id ] - [:= job-story-id :js.job-story/id] - [:= arbiter-status :ja.job-arbiter/status]] - q (sql-helpers/merge-where feedback-user-type-query where-condition)] - (log/debug "feedback->user-type-resolver" user-type-column feedback) - (user-type-column (clj root) + message-id (:message/id feedback) + arbiter-status "accepted" + job-story-id (:job-story/id feedback) + where-condition [:and + [:= message-id :jsfm.message/id] + [:= job-story-id :js.job-story/id] + [:= arbiter-status :ja.job-arbiter/status]] + q (sql-helpers/merge-where feedback-user-type-query where-condition)] + (log/debug "feedback->user-type-resolver" user-type-column feedback) + (user-type-column (clj raw-parent) - address-from-parent (or - (:candidate/id parent) - (:job-story/candidate parent)) - address-from-proposal (when (:job-story/proposal-message-id parent) - (:message/creator - (proposal-message-resolver [raw-parent args _] + (log/debug "candidate-resolver" {:args args :raw-parent raw-parent}) + (let [address-from-args (:user/id args) + parent (graphql-utils/gql->clj raw-parent) + address-from-parent (or + (:candidate/id parent) + (:job-story/candidate parent)) + address-from-proposal (when (:job-story/proposal-message-id parent) + (:message/creator + (proposal-message-resolver + [raw-parent args _] (db/with-async-resolver-conn conn - (log/debug "job-story->proposal-message-resolver") - (let [address-from-args (:user/id args) - parent (graphql-utils/gql->clj raw-parent) - proposal-message-id (:job-story/proposal-message-id parent) - proposal-message-query {:select [:*] - :from [:Message] - :where [:= :Message.message/id proposal-message-id]} - query-result (proposal-accepted-message-resolver [raw-parent args _] + (log/debug "job-story->proposal-message-resolver") + (let [address-from-args (:user/id args) + parent (graphql-utils/gql->clj raw-parent) + proposal-message-id (:job-story/proposal-message-id parent) + proposal-message-query {:select [:*] + :from [:Message] + :where [:= :Message.message/id proposal-message-id]} + query-result (proposal-accepted-message-resolver + [raw-parent _args _] (db/with-async-resolver-conn conn - (log/debug "job-story->proposal-accepted-message-resolver") - (let [address-from-args (:user/id args) - parent (graphql-utils/gql->clj raw-parent) - job-story-id (:job-story/id parent) - query {:select [:*] - :from [:JobStoryMessage] - :join [:Message [:= :Message.message/id :JobStoryMessage.message/id]] - :where [:and - [:= :JobStoryMessage.job-story/id job-story-id] - [:= :JobStoryMessage.job-story-message/type "accept-proposal"]]} - query-result (invitation-message-resolver [raw-parent args _] + (log/debug "job-story->proposal-accepted-message-resolver") + (let [parent (graphql-utils/gql->clj raw-parent) + job-story-id (:job-story/id parent) + query {:select [:*] + :from [:JobStoryMessage] + :join [:Message [:= :Message.message/id :JobStoryMessage.message/id]] + :where [:and + [:= :JobStoryMessage.job-story/id job-story-id] + [:= :JobStoryMessage.job-story-message/type "accept-proposal"]]} + query-result (invitation-message-resolver + [raw-parent _args _] (db/with-async-resolver-conn conn - (log/debug "job-story->proposal-message-resolver") - (let [address-from-args (:user/id args) - parent (graphql-utils/gql->clj raw-parent) - invitation-message-id (:job-story/invitation-message-id parent) - invitation-message-query {:select [:*] - :from [:Message] - :where [:= :Message.message/id invitation-message-id]} - query-result (invitation-accepted-message-resolver [raw-parent args _] + (log/debug "job-story->proposal-message-resolver") + (let [parent (graphql-utils/gql->clj raw-parent) + invitation-message-id (:job-story/invitation-message-id parent) + invitation-message-query {:select [:*] + :from [:Message] + :where [:= :Message.message/id invitation-message-id]} + query-result (invitation-accepted-message-resolver + [raw-parent _args _] (db/with-async-resolver-conn conn - (log/debug "job-story-invitation-accepted-message-resolver") - (let [address-from-args (:user/id args) - parent (graphql-utils/gql->clj raw-parent) - job-story-id (:job-story/id parent) - query {:select [:*] - :from [:JobStoryMessage] - :join [:Message [:= :Message.message/id :JobStoryMessage.message/id]] - :where [:and - [:= :JobStoryMessage.job-story/id job-story-id] - [:= :JobStoryMessage.job-story-message/type "accept-invitation"]]} - query-result (direct-messages-resolver [raw-parent args {:keys [:current-user :timestamp] :as ctx}] + (log/debug "job-story-invitation-accepted-message-resolver") + (let [parent (graphql-utils/gql->clj raw-parent) + job-story-id (:job-story/id parent) + query {:select [:*] + :from [:JobStoryMessage] + :join [:Message [:= :Message.message/id :JobStoryMessage.message/id]] + :where [:and + [:= :JobStoryMessage.job-story/id job-story-id] + [:= :JobStoryMessage.job-story-message/type "accept-invitation"]]} + query-result (direct-messages-resolver + [raw-parent _args {:keys [:current-user] }] (db/with-async-resolver-conn conn - (log/debug "job-story->direct-message-resolver") - (let [parent (graphql-utils/gql->clj raw-parent) - job-story-id (:job-story/id parent) - user-id (:user/id current-user) - query {:select [:*] - :from [:DirectMessage] - :join [:Message [:= :Message.message/id :DirectMessage.message/id]] - :where [:and - [:= :DirectMessage.job-story/id job-story-id] - [:or - [:= :DirectMessage.direct-message/recipient user-id] - [:= :Message.message/creator user-id]]]}] - (direct-message-resolver") + (let [parent (graphql-utils/gql->clj raw-parent) + job-story-id (:job-story/id parent) + user-id (:user/id current-user) + query {:select [:*] + :from [:DirectMessage] + :join [:Message [:= :Message.message/id :DirectMessage.message/id]] + :where [:and + [:= :DirectMessage.job-story/id job-story-id] + [:or + [:= :DirectMessage.direct-message/recipient user-id] + [:= :Message.message/creator user-id]]]}] + (clj-map search-params) - categories-and (when (:category search-params) [(:category search-params)]) - categories-or nil - skills-and (or (js->clj (:skills search-params)) []) - skills-or nil - min-rating (:feedback-min-rating search-params) - max-rating (:feedback-max-rating search-params) - min-hourly (:min-hourly-rate search-params) - max-hourly (:max-hourly-rate search-params) - min-num-feedbacks (:min-num-feedbacks search-params) - country (:country search-params) - - query (cond-> (merge candidate-query {:modifiers [:distinct]}) - min-rating (sql-helpers/merge-where [:<= min-rating :Candidate.candidate/rating]) - max-rating (sql-helpers/merge-where [:>= max-rating :Candidate.candidate/rating]) - (and (nil? min-rating) - (not (nil? max-rating))) (sql-helpers/merge-where :or [:= nil :Candidate.candidate/rating]) - id (sql-helpers/merge-where [:= :Candidate.user/id id]) - - country (sql-helpers/merge-where [:= country :Users.user/country]) - - categories-or (sql-helpers/merge-left-join :CandidateCategory - [:= :CandidateCategory.user/id :Candidate.user/id]) - - categories-or (sql-helpers/merge-where [:in :CandidateCategory.category/id categories-or]) - - categories-and (match-all {:join-table :CandidateCategory - :on-column :user/id - :column :category/id - :all-values categories-and}) - - skills-or (sql-helpers/merge-left-join :CandidateSkill - [:= :CandidateSkill.user/id :Candidate.user/id]) - - skills-or (sql-helpers/merge-where [:in :CandidateSkill.skill/id skills-or]) - min-hourly (sql-helpers/merge-where [:>= :Candidate.candidate/rate min-hourly]) - max-hourly (sql-helpers/merge-where [:<= :Candidate.candidate/rate max-hourly]) - min-num-feedbacks (sql-helpers/merge-where - [:<= min-num-feedbacks - {:select [(sql/call :count :*)] - :from [:JobStoryFeedbackMessage] - :where [:= :JobStoryFeedbackMessage.user/id :Candidate.user/id]}]) - - (not (empty? skills-and)) (match-all {:join-table :CandidateSkill - :on-column :user/id - :column :skill/id - :all-values skills-and}) - - order-by (sql-helpers/merge-order-by [[(get {:date-registered :user/date-registered - :date-updated :user/date-updated} - (graphql-utils/gql-name->kw order-by)) - (or (keyword order-direction) :asc)]]))] - (candidate-categories-resolver [root _ _] + (log/debug "candidate-search-resolver") + (let [search-params (js-obj->clj-map search-params) + categories-and (when (:category search-params) [(:category search-params)]) + categories-or nil + skills-and (or (js->clj (:skills search-params)) []) + skills-or nil + min-rating (:feedback-min-rating search-params) + max-rating (:feedback-max-rating search-params) + min-hourly (:min-hourly-rate search-params) + max-hourly (:max-hourly-rate search-params) + min-num-feedbacks (:min-num-feedbacks search-params) + country (:country search-params) + + query (cond-> (merge candidate-query {:modifiers [:distinct]}) + min-rating (sql-helpers/merge-where [:<= min-rating :Candidate.candidate/rating]) + max-rating (sql-helpers/merge-where [:>= max-rating :Candidate.candidate/rating]) + (and (nil? min-rating) + (not (nil? max-rating))) (sql-helpers/merge-where :or [:= nil :Candidate.candidate/rating]) + id (sql-helpers/merge-where [:= :Candidate.user/id id]) + + country (sql-helpers/merge-where [:= country :Users.user/country]) + + categories-or (sql-helpers/merge-left-join :CandidateCategory + [:= :CandidateCategory.user/id :Candidate.user/id]) + + categories-or (sql-helpers/merge-where [:in :CandidateCategory.category/id categories-or]) + + categories-and (match-all {:join-table :CandidateCategory + :on-column :user/id + :column :category/id + :all-values categories-and}) + + skills-or (sql-helpers/merge-left-join :CandidateSkill + [:= :CandidateSkill.user/id :Candidate.user/id]) + + skills-or (sql-helpers/merge-where [:in :CandidateSkill.skill/id skills-or]) + min-hourly (sql-helpers/merge-where [:>= :Candidate.candidate/rate min-hourly]) + max-hourly (sql-helpers/merge-where [:<= :Candidate.candidate/rate max-hourly]) + min-num-feedbacks (sql-helpers/merge-where + [:<= min-num-feedbacks + {:select [(sql/call :count :*)] + :from [:JobStoryFeedbackMessage] + :where [:= :JobStoryFeedbackMessage.user/id :Candidate.user/id]}]) + + (not (empty? skills-and)) (match-all {:join-table :CandidateSkill + :on-column :user/id + :column :skill/id + :all-values skills-and}) + + order-by (sql-helpers/merge-order-by [[(get {:date-registered :user/date-registered + :date-updated :user/date-updated} + (graphql-utils/gql-name->kw order-by)) + (or (keyword order-direction) :asc)]]))] + (categories-resolver + [participant-table root _ _] (db/with-async-resolver-conn conn - (let [{:keys [:user/id] :as candidate} (graphql-utils/gql->clj root)] - (log/debug "candidate->candidate-categories-resolver" candidate) - (map :category/id (clj root)] + (log/debug "participant->categories-resolver" participant) + (map :category/id (candidate-skills-resolver [root _ _] - (db/with-async-resolver-conn conn - (let [{:keys [:user/id] :as candidate} (graphql-utils/gql->clj root)] - (log/debug "candidate->candidate-skills-resolver" candidate) - (map :skill/id (categories-resolver [participant-table root _ _] +(defn participant->skills-resolver + [participant-table root _ _] (db/with-async-resolver-conn conn - (let [{:keys [:user/id] :as participant} (graphql-utils/gql->clj root)] - (log/debug "participant->categories-resolver" participant) - (map :category/id (skills-resolver [participant-table root _ _] - (db/with-async-resolver-conn conn - (let [{:keys [:user/id] :as participant} (graphql-utils/gql->clj root)] - (log/debug "participant->skills-resolver" participant) - (map :skill/id (clj root)] + (log/debug "participant->skills-resolver" participant) + (map :skill/id (job-stories-resolver [root {:keys [:limit :offset] :as args} _] + +(defn candidate->job-stories-resolver + [root {:keys [:limit :offset] :as args} _] (db/with-async-resolver-conn conn - (let [address (:user/id (graphql-utils/gql->clj root)) - query (-> candidate-job-stories-query - (sql-helpers/merge-where [:ilike address :JobStory.job-story/candidate]))] - (log/debug "candidate->job-stories-resolver" {:address address :args args}) - (clj root)) + query (-> candidate-job-stories-query + (sql-helpers/merge-where [:ilike address :JobStory.job-story/candidate]))] + (log/debug "candidate->job-stories-resolver" {:address address :args args}) + (job-stories-resolver [root {:keys [:limit :offset] :as args} _] + +(defn employer->job-stories-resolver + [root {:keys [:limit :offset] :as args} _] (db/with-async-resolver-conn conn - (let [address (:user/id (graphql-utils/gql->clj root)) - query (employer-job-stories-query address)] - (log/debug "employer->job-stories-resolver" {:address address :args args}) - (clj root)) + query (employer-job-stories-query address)] + (log/debug "employer->job-stories-resolver" {:address address :args args}) + (arbitrations-resolver [root {:keys [:limit :offset] :as args} _] + +(defn arbiter->arbitrations-resolver + [root {:keys [:limit :offset] :as args} _] (db/with-async-resolver-conn conn - (let [address (:user/id (graphql-utils/gql->clj root)) - query (-> arbitrations-query - (sql-helpers/merge-where [:ilike :JobArbiter.user/id address]) - (sql-helpers/merge-order-by [[:arbitration/date-created :desc]]))] - (log/debug "arbiter->arbitrations-resolver" {:address address :args args}) - (arbitrations-resolver [root {:keys [:arbiter :limit :offset] :as args} _] + (let [address (:user/id (graphql-utils/gql->clj root)) + query (-> arbitrations-query + (sql-helpers/merge-where [:ilike :JobArbiter.user/id address]) + (sql-helpers/merge-order-by [[:arbitration/date-created :desc]]))] + (log/debug "arbiter->arbitrations-resolver" {:address address :args args}) + (arbitrations-resolver + [root {:keys [:arbiter :limit :offset] :as args} _] (db/with-async-resolver-conn conn - (let [address (:job/id (graphql-utils/gql->clj root)) - query (cond-> arbitrations-query - true (sql-helpers/merge-where [:ilike :Job.job/id address]) - arbiter (sql-helpers/merge-where [:ilike :JobArbiter.user/id arbiter]) - true (sql-helpers/merge-order-by [[:arbitration/date-created :desc]]))] - (log/debug "job->arbitrations-resolver" {:address address :args args}) - (feedback-resolver [root {:keys [:limit :offset] :as args} _] + (let [address (:job/id (graphql-utils/gql->clj root)) + query (cond-> arbitrations-query + true (sql-helpers/merge-where [:ilike :Job.job/id address]) + arbiter (sql-helpers/merge-where [:ilike :JobArbiter.user/id arbiter]) + true (sql-helpers/merge-order-by [[:arbitration/date-created :desc]]))] + (log/debug "job->arbitrations-resolver" {:address address :args args}) + (feedback-resolver + [root {:keys [:limit :offset] :as args} _] (db/with-async-resolver-conn conn - (let [{:keys [:user/id] :as candidate} (graphql-utils/gql->clj root) - query (-> user-feedback-query - (sql-helpers/merge-where [:= id :JobStoryFeedbackMessage.user/id]))] - (log/debug "candidate->feedback-resolver" {:candidate candidate :args args}) - (clj root) + query (-> user-feedback-query + (sql-helpers/merge-where [:= id :JobStoryFeedbackMessage.user/id]))] + (log/debug "candidate->feedback-resolver" {:candidate candidate :args args}) + (employer-feedback-resolver [root _ _] +(defn job-story->employer-feedback-resolver + [root _ _] (db/with-async-resolver-conn conn - (let [{job-story-id :job-story/id} (graphql-utils/gql->clj root) - query (-> user-feedback-query - (sql-helpers/merge-where ,,, [:= job-story-id :JobStory.job-story/id]) - (sql-helpers/merge-where ,,, [:and - [:= :JobStoryFeedbackMessage.job-story/id job-story-id] - [:= :Job.job/creator :JobStoryFeedbackMessage.user/id]]))] - (log/debug "job-story->employer-feedback-resolver") - (candidate-feedback-resolver [root _ _] + (let [{job-story-id :job-story/id} (graphql-utils/gql->clj root) + query (-> user-feedback-query + (sql-helpers/merge-where ,,, [:= job-story-id :JobStory.job-story/id]) + (sql-helpers/merge-where ,,, [:and + [:= :JobStoryFeedbackMessage.job-story/id job-story-id] + [:= :Job.job/creator :JobStoryFeedbackMessage.user/id]]))] + (log/debug "job-story->employer-feedback-resolver") + (candidate-feedback-resolver + [root _ _] (db/with-async-resolver-conn conn - (let [{job-story-id :job-story/id} (graphql-utils/gql->clj root)] - (log/debug "job-story->candidate-feedback-resolver") - ( user-feedback-query - (sql-helpers/merge-where [:= job-story-id :JobStory.job-story/id]) - (sql-helpers/merge-where [:ilike :JobStory.job-story/candidate :JobStoryFeedbackMessage.user/id]))))))) + (let [{job-story-id :job-story/id} (graphql-utils/gql->clj root)] + (log/debug "job-story->candidate-feedback-resolver") + ( user-feedback-query + (sql-helpers/merge-where [:= job-story-id :JobStory.job-story/id]) + (sql-helpers/merge-where [:ilike :JobStory.job-story/candidate :JobStoryFeedbackMessage.user/id]))))))) -(defn job-story->arbiter-feedback-resolver [root _ _] + +(defn job-story->arbiter-feedback-resolver + [root _ _] (db/with-async-resolver-conn conn - (let [{job-story-id :job-story/id} (graphql-utils/gql->clj root) - query (-> user-feedback-query - (sql-helpers/merge-join ,,, :JobArbiter [:= :JobArbiter.job/id :Job.job/id]) - (sql-helpers/merge-where ,,, [:= job-story-id :JobStory.job-story/id]) - (sql-helpers/merge-where ,,, [:and - [:= :JobStoryFeedbackMessage.job-story/id job-story-id] - [:= :JobArbiter.user/id :JobStoryFeedbackMessage.user/id]]))] - (log/debug "job-story->arbiter-feedback-resolver") - (feedbacks-resolver [root _ _] + (let [{job-story-id :job-story/id} (graphql-utils/gql->clj root) + query (-> user-feedback-query + (sql-helpers/merge-join ,,, :JobArbiter [:= :JobArbiter.job/id :Job.job/id]) + (sql-helpers/merge-where ,,, [:= job-story-id :JobStory.job-story/id]) + (sql-helpers/merge-where ,,, [:and + [:= :JobStoryFeedbackMessage.job-story/id job-story-id] + [:= :JobArbiter.user/id :JobStoryFeedbackMessage.user/id]]))] + (log/debug "job-story->arbiter-feedback-resolver") + (feedbacks-resolver + [root _ _] (db/with-async-resolver-conn conn - (let [{job-story-id :job-story/id} (graphql-utils/gql->clj root) - query (-> user-feedback-query - (sql-helpers/merge-where ,,, [:= job-story-id :JobStory.job-story/id]) - (sql-helpers/merge-where ,,, [:= :JobStoryFeedbackMessage.job-story/id job-story-id]))] - (log/debug "job-story->feedbacks-resolver job-story-id" job-story-id) - (clj root) + query (-> user-feedback-query + (sql-helpers/merge-where ,,, [:= job-story-id :JobStory.job-story/id]) + (sql-helpers/merge-where ,,, [:= :JobStoryFeedbackMessage.job-story/id job-story-id]))] + (log/debug "job-story->feedbacks-resolver job-story-id" job-story-id) + (clj root)] - (log/debug "message-resolver") - (clj root)] + (log/debug "message-resolver") + (sub-message-resolver [invoice-message-column root args _] - (db/with-async-resolver-conn conn - (let [root-obj (graphql-utils/gql->clj root) - job-story-id (:job-story/id root-obj) - invoice-id (:invoice/id root-obj) - invoice-message-column (keyword :JobStoryInvoiceMessage.invoice invoice-message-column) - message-id (:message/id root-obj)] - (log/debug (str "invoice->sub-message-resolver for " invoice-message-column " job-story-id:") job-story-id) - (sub-message-resolver + [invoice-message-column root _args _] + (db/with-async-resolver-conn conn + (let [root-obj (graphql-utils/gql->clj root) + job-story-id (:job-story/id root-obj) + invoice-id (:invoice/id root-obj) + invoice-message-column (keyword :JobStoryInvoiceMessage.invoice invoice-message-column)] + (log/debug (str "invoice->sub-message-resolver for " invoice-message-column " job-story-id:") job-story-id) + ( employer-query - - id (sql-helpers/merge-where [:= id :Employer.user/id]) - - professional-title (sql-helpers/merge-where [:= professional-title :Employer.employer/professional-title]) - - order-by (sql-helpers/merge-order-by [[(get {:date-registered :user/date-registered - :date-updated :user/date-updated} - (graphql-utils/gql-name->kw order-by)) - (or (keyword order-direction) :asc)]]))] - ( employer-query + + id (sql-helpers/merge-where [:= id :Employer.user/id]) + + professional-title (sql-helpers/merge-where [:= professional-title :Employer.employer/professional-title]) + + order-by (sql-helpers/merge-order-by [[(get {:date-registered :user/date-registered + :date-updated :user/date-updated} + (graphql-utils/gql-name->kw order-by)) + (or (keyword order-direction) :asc)]]))] + (clj parent)) - contract-from-args (:job/id args) - contract-address (or contract-from-parent contract-from-args) - job (required-skills-resolver [parent args _] + (log/debug "job-resolver") + (let [contract-from-parent (:job/id (graphql-utils/gql->clj parent)) + contract-from-args (:job/id args) + contract-address (or contract-from-parent contract-from-args) + job (required-skills-resolver + [parent _args _] (db/with-async-resolver-conn conn - (let [job-id (:job/id (graphql-utils/gql->clj parent))] - (map :skill/id (clj parent))] + (map :skill/id (clj-map search-params) - max-rating (:feedback-max-rating search-params) - min-rating (:feedback-min-rating search-params) - skills (or (js->clj (:skills search-params)) []) - category (:category search-params) - min-hourly-rate (:min-hourly-rate search-params) - max-hourly-rate (:max-hourly-rate search-params) - min-num-feedbacks (:min-num-feedbacks search-params) - creator (:creator search-params) - arbiter (:arbiter search-params) - payment-type (:payment-type search-params) - status (:status search-params) - - experience-level (:experience-level search-params) - ordered-experience-levels ["beginner" "intermediate" "expert"] - suitable-levels (drop-while #(not (= experience-level %)) ordered-experience-levels) - - query (cond-> (merge job-search-query {:modifiers [:distinct]}) - min-rating (sql-helpers/merge-where [:<= min-rating :Employer.employer/rating]) - max-rating (sql-helpers/merge-where [:>= max-rating :Employer.employer/rating]) - (and (nil? min-rating) - (not (nil? max-rating))) (sql-helpers/merge-where :or [:= nil :Employer.employer/rating]) - - creator (sql-helpers/merge-where [:ilike creator :Job.job/creator]) - arbiter (sql-helpers/merge-where [:ilike arbiter :JobArbiter.user/id]) - - ; The case for OR-ing the skills - ; (not (empty? skills)) (sql-helpers/merge-where [:in :JobSkill.skill/id skills]) - - ; The case for AND-ing the skills - (not (empty? skills)) (match-all {:join-table :JobSkill - :on-column :job/id - :column :skill/id - :all-values skills}) - category (sql-helpers/merge-where [:= :Job.job/category category]) - - min-hourly-rate (sql-helpers/merge-where [:<= min-hourly-rate :JobStory.job-story/proposal-rate]) - max-hourly-rate (sql-helpers/merge-where [:>= max-hourly-rate :JobStory.job-story/proposal-rate]) - min-num-feedbacks (sql-helpers/merge-where - [:<= min-num-feedbacks - {:select [(sql/call :count :*)] - :from [:JobStoryFeedbackMessage] - :where [:= :JobStoryFeedbackMessage.user/id :Job.job/creator]}]) - payment-type (sql-helpers/merge-where [:= :Job.job/bid-option payment-type]) - status (sql-helpers/merge-where [:= :Job.job/status status]) - experience-level (sql-helpers/merge-where [:in :Job.job/required-experience-level suitable-levels]) - order-by (sql-helpers/merge-order-by [[(get {:date-created :job/date-created - :date-updated :job/date-updated} - (graphql-utils/gql-name->kw order-by)) - (or (keyword order-direction) :desc)]]))] - (job-stories-resolver [root {:keys [:limit :offset] :as args} _] + (let [search-params (js-obj->clj-map search-params) + max-rating (:feedback-max-rating search-params) + min-rating (:feedback-min-rating search-params) + skills (or (js->clj (:skills search-params)) []) + category (:category search-params) + min-hourly-rate (:min-hourly-rate search-params) + max-hourly-rate (:max-hourly-rate search-params) + min-num-feedbacks (:min-num-feedbacks search-params) + creator (:creator search-params) + arbiter (:arbiter search-params) + payment-type (:payment-type search-params) + status (:status search-params) + + experience-level (:experience-level search-params) + ordered-experience-levels ["beginner" "intermediate" "expert"] + suitable-levels (drop-while #(not (= experience-level %)) ordered-experience-levels) + + query (cond-> (merge job-search-query {:modifiers [:distinct]}) + min-rating (sql-helpers/merge-where [:<= min-rating :Employer.employer/rating]) + max-rating (sql-helpers/merge-where [:>= max-rating :Employer.employer/rating]) + (and (nil? min-rating) + (not (nil? max-rating))) (sql-helpers/merge-where :or [:= nil :Employer.employer/rating]) + + creator (sql-helpers/merge-where [:ilike creator :Job.job/creator]) + arbiter (sql-helpers/merge-where [:ilike arbiter :JobArbiter.user/id]) + + ;; The case for OR-ing the skills + ;; (not (empty? skills)) (sql-helpers/merge-where [:in :JobSkill.skill/id skills]) + + ;; The case for AND-ing the skills + (not (empty? skills)) (match-all {:join-table :JobSkill + :on-column :job/id + :column :skill/id + :all-values skills}) + category (sql-helpers/merge-where [:= :Job.job/category category]) + + min-hourly-rate (sql-helpers/merge-where [:<= min-hourly-rate :JobStory.job-story/proposal-rate]) + max-hourly-rate (sql-helpers/merge-where [:>= max-hourly-rate :JobStory.job-story/proposal-rate]) + min-num-feedbacks (sql-helpers/merge-where + [:<= min-num-feedbacks + {:select [(sql/call :count :*)] + :from [:JobStoryFeedbackMessage] + :where [:= :JobStoryFeedbackMessage.user/id :Job.job/creator]}]) + payment-type (sql-helpers/merge-where [:= :Job.job/bid-option payment-type]) + status (sql-helpers/merge-where [:= :Job.job/status status]) + experience-level (sql-helpers/merge-where [:in :Job.job/required-experience-level suitable-levels]) + order-by (sql-helpers/merge-order-by [[(get {:date-created :job/date-created + :date-updated :job/date-updated} + (graphql-utils/gql-name->kw order-by)) + (or (keyword order-direction) :desc)]]))] + (job-stories-resolver + [root {:keys [:limit :offset] :as args} _] (db/with-async-resolver-conn conn - (let [{:keys [:job/id] :as job} (graphql-utils/gql->clj root)] - (log/debug "job->job-stories-resolver" {:job job :args args}) - (clj root)] + (log/debug "job->job-stories-resolver" {:job job :args args}) + (clj root)) - job-story-from-args (:job-story/id args) - job-story-id (or job-story-from-args job-story-from-root)] - ( job-story-query - (sql-helpers/merge-where [:= job-story-id :JobStory.job-story/id]))))))) - -(defn job-story-list-resolver [parent args _] + (log/debug "job-story-resolver" args) + (let [job-story-from-root (:job-story/id (graphql-utils/gql->clj root)) + job-story-from-args (:job-story/id args) + job-story-id (or job-story-from-args job-story-from-root)] + ( job-story-query + (sql-helpers/merge-where [:= job-story-id :JobStory.job-story/id]))))))) + + +(defn job-story-list-resolver + [_parent args _] (db/with-async-resolver-conn conn - (log/debug "job-story-list-resolver" args) - (let [contract (:job-contract args) - query {:select [:*] - :from [:JobStory] - :where [:= :JobStory.job/id contract]}] - (clj-map search-params) - job-id (:job search-params) - candidate-id (:candidate search-params) - employer-id (:employer search-params) - status (:status search-params) - status-val (if (= "finished" status) - ["finished" "job-ended"] - [(:status search-params)]) - base-query {:select [:JobStory.*] - :from [:JobStory] - :join [:Job [:= :Job.job/id :JobStory.job/id]]} - query (cond-> base-query - job-id (sql-helpers/merge-where [:ilike :JobStory.job/id job-id]) - employer-id (sql-helpers/merge-where [:ilike :Job.job/creator employer-id]) - candidate-id (sql-helpers/merge-where [:ilike :JobStory.job-story/candidate candidate-id]) - status (sql-helpers/merge-where [:in :JobStory.job-story/status status-val]) - order-by (sql-helpers/merge-order-by [[(get {:date-created :job-story/date-created - :date-updated :job-story/date-updated} - (graphql-utils/gql-name->kw order-by)) - (or (keyword order-direction) :desc)]])) - limit (:limit args) - offset (:offset args)] - (clj-map search-params) + job-id (:job search-params) + candidate-id (:candidate search-params) + employer-id (:employer search-params) + status (:status search-params) + status-val (if (= "finished" status) + ["finished" "job-ended"] + [(:status search-params)]) + base-query {:select [:JobStory.*] + :from [:JobStory] + :join [:Job [:= :Job.job/id :JobStory.job/id]]} + query (cond-> base-query + job-id (sql-helpers/merge-where [:ilike :JobStory.job/id job-id]) + employer-id (sql-helpers/merge-where [:ilike :Job.job/creator employer-id]) + candidate-id (sql-helpers/merge-where [:ilike :JobStory.job-story/candidate candidate-id]) + status (sql-helpers/merge-where [:in :JobStory.job-story/status status-val]) + order-by (sql-helpers/merge-order-by [[(get {:date-created :job-story/date-created + :date-updated :job-story/date-updated} + (graphql-utils/gql-name->kw order-by)) + (or (keyword order-direction) :desc)]])) + limit (:limit args) + offset (:offset args)] + ( invoice-query - employer (sql-helpers/merge-where [:ilike :Job.job/creator employer]) - candidate (sql-helpers/merge-where [:ilike :JobStory.job-story/candidate candidate]) - status (sql-helpers/merge-where [:in :JobStoryInvoiceMessage.invoice/status statuses]) - true (sql-helpers/merge-order-by [[:invoice/date-requested :desc]]))] - ( invoice-query + employer (sql-helpers/merge-where [:ilike :Job.job/creator employer]) + candidate (sql-helpers/merge-where [:ilike :JobStory.job-story/candidate candidate]) + status (sql-helpers/merge-where [:in :JobStoryInvoiceMessage.invoice/status statuses]) + true (sql-helpers/merge-order-by [[:invoice/date-requested :desc]]))] + ( invoice-query - (sql-helpers/merge-where [:and - [:= job-id :Job.job/id] - [:= invoice-id :JobStoryInvoiceMessage.invoice/ref-id]])))))) - -(def ^:private dispute-query {:modifiers [:distinct-on - :JobStory.job-story/id - :JobStoryInvoiceMessage.invoice/ref-id - :JobStoryInvoiceMessage.invoice/date-requested] - :select [[(sql/call :concat :JobStory.job/id (sql/raw "'-'") :invoice/ref-id) :id] - [:JobStoryInvoiceMessage.invoice/ref-id :invoice/id] - :JobStoryInvoiceMessage.message/id - :Job.job/id - :JobStory.job-story/id - - :JobStoryInvoiceMessage.invoice/amount-requested - :JobStoryInvoiceMessage.invoice/amount-paid - [:JobStoryInvoiceMessage.invoice/status :dispute/status] - - [:JobStory.job-story/candidate :candidate/id] - [:Job.job/creator :employer/id] - [:JobArbiter.user/id :arbiter/id] - - [:raised-message.message/text :dispute/reason] - [:resolved-message.message/text :dispute/resolution] - [:raised-message.message/date-created :dispute/date-created] - [:resolved-message.message/date-created :dispute/date-resolved] - ] - :from [:JobStoryInvoiceMessage] - :join [:JobStory [:= :JobStory.job-story/id :JobStoryInvoiceMessage.job-story/id] - :Job [:= :Job.job/id :JobStory.job/id] - :JobArbiter [:ilike :Job.job/id :JobArbiter.job/id] - [:Message :raised-message] [:= :raised-message.message/id :JobStoryInvoiceMessage.invoice/dispute-raised-message-id]] - :left-join [[:Message :resolved-message] [:= :resolved-message.message/id :JobStoryInvoiceMessage.invoice/dispute-resolved-message-id]]}) - -(defn dispute-search-resolver [_ {:keys [:arbiter :employer :candidate :status :limit :offset] :as args} _] + (log/debug "invoice-resolver" {:args args}) + ( invoice-query + (sql-helpers/merge-where [:and + [:= job-id :Job.job/id] + [:= invoice-id :JobStoryInvoiceMessage.invoice/ref-id]])))))) + + +(def ^:private dispute-query + {:modifiers [:distinct-on + :JobStory.job-story/id + :JobStoryInvoiceMessage.invoice/ref-id + :JobStoryInvoiceMessage.invoice/date-requested] + :select [[(sql/call :concat :JobStory.job/id (sql/raw "'-'") :invoice/ref-id) :id] + [:JobStoryInvoiceMessage.invoice/ref-id :invoice/id] + :JobStoryInvoiceMessage.message/id + :Job.job/id + :JobStory.job-story/id + + :JobStoryInvoiceMessage.invoice/amount-requested + :JobStoryInvoiceMessage.invoice/amount-paid + [:JobStoryInvoiceMessage.invoice/status :dispute/status] + + [:JobStory.job-story/candidate :candidate/id] + [:Job.job/creator :employer/id] + [:JobArbiter.user/id :arbiter/id] + + [:raised-message.message/text :dispute/reason] + [:resolved-message.message/text :dispute/resolution] + [:raised-message.message/date-created :dispute/date-created] + [:resolved-message.message/date-created :dispute/date-resolved]] + :from [:JobStoryInvoiceMessage] + :join [:JobStory [:= :JobStory.job-story/id :JobStoryInvoiceMessage.job-story/id] + :Job [:= :Job.job/id :JobStory.job/id] + :JobArbiter [:ilike :Job.job/id :JobArbiter.job/id] + [:Message :raised-message] [:= :raised-message.message/id :JobStoryInvoiceMessage.invoice/dispute-raised-message-id]] + :left-join [[:Message :resolved-message] [:= :resolved-message.message/id :JobStoryInvoiceMessage.invoice/dispute-resolved-message-id]]}) + + +(defn dispute-search-resolver + [_ {:keys [:arbiter :employer :candidate :status :limit :offset] :as args} _] (db/with-async-resolver-conn conn - (log/debug "dispute-search-resolver" {:args args}) - (let [query (cond-> dispute-query - employer (sql-helpers/merge-where [:ilike :Job.job/creator employer]) - candidate (sql-helpers/merge-where [:ilike :JobStory.job-story/candidate candidate]) - arbiter (sql-helpers/merge-where [:ilike :JobArbiter.user/id arbiter]) - status (sql-helpers/merge-where [:= :JobStoryInvoiceMessage.invoice/status status]) - true (sql-helpers/merge-order-by [[:JobStoryInvoiceMessage.invoice/date-requested :desc]]))] - (invoices-resolver [root {:keys [:statuses :limit :offset] :as args} _] + (log/debug "dispute-search-resolver" {:args args}) + (let [query (cond-> dispute-query + employer (sql-helpers/merge-where [:ilike :Job.job/creator employer]) + candidate (sql-helpers/merge-where [:ilike :JobStory.job-story/candidate candidate]) + arbiter (sql-helpers/merge-where [:ilike :JobArbiter.user/id arbiter]) + status (sql-helpers/merge-where [:= :JobStoryInvoiceMessage.invoice/status status]) + true (sql-helpers/merge-order-by [[:JobStoryInvoiceMessage.invoice/date-requested :desc]]))] + (invoices-resolver + [root {:keys [:statuses :limit :offset] :as args} _] (db/with-async-resolver-conn conn - (let [parsed-root (graphql-utils/gql->clj root) - job-story-id (:job-story/id (graphql-utils/gql->clj root)) - snake-statuses (map camel-snake-kebab.core/->kebab-case statuses) - query (cond-> invoice-query - job-story-id (sql-helpers/merge-where [:= job-story-id :JobStory.job-story/id]) - statuses (sql-helpers/merge-where [:in :JobStoryInvoiceMessage.invoice/status snake-statuses])) - result-pages (invoices-resolver RESULT-PAGES" job-story-id " | statuses" statuses) - result-pages))) - -(defn job->invoices-resolver [root {:keys [:limit :offset] :as args} _] + (let [parsed-root (graphql-utils/gql->clj root) + job-story-id (:job-story/id (graphql-utils/gql->clj root)) + snake-statuses (map camel-snake-kebab.core/->kebab-case statuses) + query (cond-> invoice-query + job-story-id (sql-helpers/merge-where [:= job-story-id :JobStory.job-story/id]) + statuses (sql-helpers/merge-where [:in :JobStoryInvoiceMessage.invoice/status snake-statuses])) + result-pages (invoices-resolver RESULT-PAGES" job-story-id " | statuses" statuses) + result-pages))) + + +(defn job->invoices-resolver + [root {:keys [:limit :offset]} _] (db/with-async-resolver-conn conn - (let [parsed-root (graphql-utils/gql->clj root) - job-id (:job/id (graphql-utils/gql->clj root)) - query (-> invoice-query - (sql-helpers/merge-where [:= job-id :Job.job/id])) - result-pages (invoices-resolver RESULT-PAGES" job-id " | " result-pages) - result-pages))) - -(defn job->balance-resolver [root {:keys [:limit :offset] :as args} _] + (let [parsed-root (graphql-utils/gql->clj root) + job-id (:job/id parsed-root) + query (-> invoice-query + (sql-helpers/merge-where [:= job-id :Job.job/id])) + result-pages (invoices-resolver RESULT-PAGES" job-id " | " result-pages) + result-pages))) + + +(defn job->balance-resolver + [root _args _] (db/with-async-resolver-conn conn - (let [parsed-root (graphql-utils/gql->clj root) - job-id (:job/id (graphql-utils/gql->clj root)) - query {:select [(sql/call :sum :job-funding/amount)] - :from [:JobFunding] - :where [:= :JobFunding.job/id job-id]} - result (balance-resolver " job-id " | " result) - (:sum result)))) - -(defn sign-in-mutation [_ {:keys [:data :data-signature] :as input} {:keys [config]}] + (let [parsed-root (graphql-utils/gql->clj root) + job-id (:job/id parsed-root) + query {:select [(sql/call :sum :job-funding/amount)] + :from [:JobFunding] + :where [:= :JobFunding.job/id job-id]} + result (balance-resolver " job-id " | " result) + (:sum result)))) + + +(defn sign-in-mutation + [_ {:keys [:data :data-signature] :as input} {:keys [config]}] (try-catch-throw (let [sign-in-secret (-> config :graphql :sign-in-secret) user-address (authorization/recover-personal-signature data data-signature) @@ -977,301 +1076,304 @@ {:jwt jwt :user/id user-address}))) -(defn send-message-mutation [_ message-params {:keys [:current-user :timestamp]}] +(defn send-message-mutation + [_ message-params {:keys [:current-user :timestamp]}] (db/with-async-resolver-tx conn - (log/debug "send-message-mutation" ) - (let [job-story-id (:job-story/id message-params) - recipient (:to message-params) - text (:text message-params) - job-story-message-type (graphql-utils/gql-name->kw (:job-story-message/type message-params)) - message-type (or (graphql-utils/gql-name->kw (:message/type message-params)) :direct-message)] - (kw (:job-story-message/type message-params)) + message-type (or (graphql-utils/gql-name->kw (:message/type message-params)) :direct-message)] + (clj-map (:user params)) - candidate (js-obj->clj-map (:candidate params)) - employer (js-obj->clj-map (:employer params)) - arbiter (js-obj->clj-map (:arbiter params)) - upsert-args (cond-> {} - (not (empty? user)) - (assoc ,,, :user (assoc user :user/id user-id)) + (let [user-id (:user/id params) + user (js-obj->clj-map (:user params)) + candidate (js-obj->clj-map (:candidate params)) + employer (js-obj->clj-map (:employer params)) + arbiter (js-obj->clj-map (:arbiter params)) + upsert-args (cond-> {} + (not (empty? user)) + (assoc ,,, :user (assoc user :user/id user-id)) + + (not (empty? candidate)) + (assoc ,,, :candidate (assoc candidate :user/id user-id)) - (not (empty? candidate)) - (assoc ,,, :candidate (assoc candidate :user/id user-id)) + (not (empty? employer)) + (assoc ,,, :employer (assoc employer :user/id user-id)) - (not (empty? employer)) - (assoc ,,, :employer (assoc employer :user/id user-id)) + (not (empty? arbiter)) + (assoc ,,, :arbiter (assoc arbiter :user/id user-id)))] + (log/debug "update-user-mutation") + (clj-map (:input gql-params)) - message-params {:message/type :job-story-message - :job-story-message/type :proposal - :job/id (:contract input) - :message/date-created timestamp - :message/creator (:user/id current-user) - :message/text (:text input) - :job-story/proposal-rate (:rate input) - :job-story/proposal-rate-currency-id (:rate-currency-id input)} - job-story-id (:job-story/id (clj-map (:input gql-params)) + message-params {:message/type :job-story-message + :job-story-message/type :proposal + :job/id (:contract input) + :message/date-created timestamp + :message/creator (:user/id current-user) + :message/text (:text input) + :job-story/proposal-rate (:rate input) + :job-story/proposal-rate-currency-id (:rate-currency-id input)} + job-story-id (:job-story/id (js {:url "https://github.com/login/oauth/access_token" - :method :post - :headers {"Content-Type" "application/json" - "Accept" "application/json"} - :data (js/JSON.stringify (clj->js {:client_id client-id - :client_secret client-secret - :scope "user" - :code code}))}))) - {:keys [data]} (js->clj response :keywordize-keys true) - access-token (-> data (string/split "&") first (string/split "=") second) - response - (js {:url "https://api.github.com/user" - :method :get - :headers {"Authorization" (str "token " access-token) - "Content-Type" "application/json" - "Accept" "application/json"}}))) - {:keys [name login email location] :as gh-resp} (:data (js->clj response :keywordize-keys true)) - _ (log/debug "github response" gh-resp) - user {:user/id (:user/id current-user) - :user/name name - :user/github-username login - :user/email email - :user/country location}] - - (js {:url "https://github.com/login/oauth/access_token" + :method :post + :headers {"Content-Type" "application/json" + "Accept" "application/json"} + :data (js/JSON.stringify (clj->js {:client_id client-id + :client_secret client-secret + :scope "user" + :code code}))}))) + {:keys [data]} (js->clj response :keywordize-keys true) + access-token (-> data (string/split "&") first (string/split "=") second) + response + (js {:url "https://api.github.com/user" + :method :get + :headers {"Authorization" (str "token " access-token) + "Content-Type" "application/json" + "Accept" "application/json"}}))) + {:keys [name login email location] :as gh-resp} (:data (js->clj response :keywordize-keys true)) + _ (log/debug "github response" gh-resp) + user {:user/id (:user/id current-user) + :user/name name + :user/github-username login + :user/email email + :user/country location}] + + (js {:url "https://www.linkedin.com/oauth/v2/accessToken" - :method :post - :headers {"Content-Type" "application/x-www-form-urlencoded"} - :data - (.stringify querystring (clj->js {:grant_type "authorization_code" - :code code - :redirect_uri redirect-uri - :client_id client-id - :client_secret client-secret}))}))) - {:keys [data]} (js->clj response :keywordize-keys true) - access-token (:access_token data) - {:keys [id localizedLastName localizedFirstName] :as resp1} (-> (js {:url "https://api.linkedin.com/v2/me" + (let [{:keys [code redirect-uri]} input + {:keys [client-id client-secret]} (:linkedin config) + response + (js {:url "https://www.linkedin.com/oauth/v2/accessToken" + :method :post + :headers {"Content-Type" "application/x-www-form-urlencoded"} + :data + (.stringify querystring (clj->js {:grant_type "authorization_code" + :code code + :redirect_uri redirect-uri + :client_id client-id + :client_secret client-secret}))}))) + {:keys [data]} (js->clj response :keywordize-keys true) + access-token (:access_token data) + {:keys [id localizedLastName localizedFirstName] :as resp1} (-> (js {:url "https://api.linkedin.com/v2/me" + :method :get + :headers {"Authorization" (str "Bearer " access-token) + "Accept" "application/json"}}))) + (js->clj :keywordize-keys true) + :data) + {email "emailAddress" :as resp2} (-> (js {:url "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))" :method :get :headers {"Authorization" (str "Bearer " access-token) "Accept" "application/json"}}))) - (js->clj :keywordize-keys true) - :data) - {email "emailAddress" :as resp2} (-> (js {:url "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))" - :method :get - :headers {"Authorization" (str "Bearer " access-token) - "Accept" "application/json"}}))) - js->clj - (get-in ["data" "elements"]) - first - (get "handle~")) - _ (log/debug "linkedin response" (merge resp1 resp2)) - user {:user/id (:user/id current-user) - :user/name (str localizedFirstName " " localizedLastName) - :user/linkedin-username id - :user/email email}] - - (clj + (get-in ["data" "elements"]) + first + (get "handle~")) + _ (log/debug "linkedin response" (merge resp1 resp2)) + user {:user/id (:user/id current-user) + :user/name (str localizedFirstName " " localizedLastName) + :user/linkedin-username id + :user/email email}] + + (job-stories-resolver - :job_employer job->employer-resolver - :job_arbiter job->arbiter-resolver - :arbitrations job->arbitrations-resolver - :tokenDetails job->token-details-resolver - :invoices job->invoices-resolver - :invoice invoice-resolver - :balance job->balance-resolver - :job_requiredSkills job->required-skills-resolver} - :JobStory {:jobStory_employerFeedback job-story->employer-feedback-resolver - :jobStory_candidateFeedback job-story->candidate-feedback-resolver - :jobStory_arbiterFeedback job-story->arbiter-feedback-resolver - :feedbacks job-story->feedbacks-resolver - :jobStory_invoices job-story->invoices-resolver - :job job-resolver - :candidate candidate-resolver - :proposalMessage job-story->proposal-message-resolver - :proposalAcceptedMessage job-story->proposal-accepted-message-resolver - :directMessages (require-auth job-story->direct-messages-resolver) - :invitationMessage job-story->invitation-message-resolver - :invitationAcceptedMessage job-story->invitation-accepted-message-resolver} - :User {:user_languages user->languages-resolvers - :user_isRegisteredCandidate user->is-registered-candidate-resolver - :user_isRegisteredEmployer user->is-registered-employer-resolver - :user_isRegisteredArbiter user->is-registered-arbiter-resolver} - :Candidate {:candidate_feedback candidate->feedback-resolver - :candidate_categories (partial participant->categories-resolver :CandidateCategory) - :candidate_skills (partial participant->skills-resolver :CandidateSkill) - :jobStories candidate->job-stories-resolver - :user participant->user-resolver} - :Employer {:employer_feedback employer->feedback-resolver - :jobStories employer->job-stories-resolver - :user participant->user-resolver} - :Arbiter {:arbiter_feedback arbiter->feedback-resolver - :arbiter_categories (partial participant->categories-resolver :ArbiterCategory) - :arbiter_skills (partial participant->skills-resolver :ArbiterSkill) - :arbitrations arbiter->arbitrations-resolver - :user participant->user-resolver} - :Arbitration {:job job-resolver - :arbiter arbiter-resolver - :feeTokenDetails arbitration->fee-token-details-resolver} - :Dispute {:job job-resolver - :jobStory job-story-resolver - :candidate candidate-resolver - :employer employer-resolver - :arbiter arbiter-resolver} - - :Feedback {:feedback_toUserType (partial feedback->user-type-resolver :to-user-type) - :feedback_toUser feedback->to-user-resolver - :feedback_fromUserType (partial feedback->user-type-resolver :from-user-type) - :feedback_fromUser feedback->from-user-resolver - :message message-resolver} - :JobStoryMessage {:creator user-resolver} - :DirectMessage {:creator user-resolver} - :Invoice {:jobStory job-story-resolver - :creationMessage message-resolver - :paymentMessage (partial invoice->sub-message-resolver :payment-message-id) - :disputeRaisedMessage (partial invoice->sub-message-resolver :dispute-raised-message-id) - :disputeResolvedMessage (partial invoice->sub-message-resolver :dispute-resolved-message-id)} - :Mutation {:signIn sign-in-mutation - :sendMessage (require-auth send-message-mutation) - :leaveFeedback (require-auth leave-feedback-mutation) - ;; TODO : do require auth - :updateUser (require-auth update-user-mutation) - :createJobProposal (require-auth create-job-proposal-mutation) - :removeJobProposal (require-auth remove-job-proposal-mutation) - :replayEvents replay-events - :githubSignUp (require-auth github-signup-mutation) - :linkedinSignUp (require-auth linkedin-signup-mutation)} - ; :Date ; TODO: https://www.apollographql.com/docs/apollo-server/schema/custom-scalars/#example-the-date-scalar - }) + +(def resolvers-map + {:Query {:user user-resolver + :userSearch user-search-resolver + :candidate candidate-resolver + :candidateSearch candidate-search-resolver + :employer employer-resolver + :employerSearch employer-search-resolver + :arbiter arbiter-resolver + :arbiterSearch arbiter-search-resolver + :job job-resolver + :jobSearch job-search-resolver + :jobStory job-story-resolver + :jobStoryList job-story-list-resolver + :jobStorySearch job-story-search-resolver + :invoiceSearch invoice-search-resolver + :disputeSearch dispute-search-resolver} + :Job {:jobStories job->job-stories-resolver + :job_employer job->employer-resolver + :job_arbiter job->arbiter-resolver + :arbitrations job->arbitrations-resolver + :tokenDetails job->token-details-resolver + :invoices job->invoices-resolver + :invoice invoice-resolver + :balance job->balance-resolver + :job_requiredSkills job->required-skills-resolver} + :JobStory {:jobStory_employerFeedback job-story->employer-feedback-resolver + :jobStory_candidateFeedback job-story->candidate-feedback-resolver + :jobStory_arbiterFeedback job-story->arbiter-feedback-resolver + :feedbacks job-story->feedbacks-resolver + :jobStory_invoices job-story->invoices-resolver + :job job-resolver + :candidate candidate-resolver + :proposalMessage job-story->proposal-message-resolver + :proposalAcceptedMessage job-story->proposal-accepted-message-resolver + :directMessages (require-auth job-story->direct-messages-resolver) + :invitationMessage job-story->invitation-message-resolver + :invitationAcceptedMessage job-story->invitation-accepted-message-resolver} + :User {:user_languages user->languages-resolvers + :user_isRegisteredCandidate user->is-registered-candidate-resolver + :user_isRegisteredEmployer user->is-registered-employer-resolver + :user_isRegisteredArbiter user->is-registered-arbiter-resolver} + :Candidate {:candidate_feedback candidate->feedback-resolver + :candidate_categories (partial participant->categories-resolver :CandidateCategory) + :candidate_skills (partial participant->skills-resolver :CandidateSkill) + :jobStories candidate->job-stories-resolver + :user participant->user-resolver} + :Employer {:employer_feedback employer->feedback-resolver + :jobStories employer->job-stories-resolver + :user participant->user-resolver} + :Arbiter {:arbiter_feedback arbiter->feedback-resolver + :arbiter_categories (partial participant->categories-resolver :ArbiterCategory) + :arbiter_skills (partial participant->skills-resolver :ArbiterSkill) + :arbitrations arbiter->arbitrations-resolver + :user participant->user-resolver} + :Arbitration {:job job-resolver + :arbiter arbiter-resolver + :feeTokenDetails arbitration->fee-token-details-resolver} + :Dispute {:job job-resolver + :jobStory job-story-resolver + :candidate candidate-resolver + :employer employer-resolver + :arbiter arbiter-resolver} + + :Feedback {:feedback_toUserType (partial feedback->user-type-resolver :to-user-type) + :feedback_toUser feedback->to-user-resolver + :feedback_fromUserType (partial feedback->user-type-resolver :from-user-type) + :feedback_fromUser feedback->from-user-resolver + :message message-resolver} + :JobStoryMessage {:creator user-resolver} + :DirectMessage {:creator user-resolver} + :Invoice {:jobStory job-story-resolver + :creationMessage message-resolver + :paymentMessage (partial invoice->sub-message-resolver :payment-message-id) + :disputeRaisedMessage (partial invoice->sub-message-resolver :dispute-raised-message-id) + :disputeResolvedMessage (partial invoice->sub-message-resolver :dispute-resolved-message-id)} + :Mutation {:signIn sign-in-mutation + :sendMessage (require-auth send-message-mutation) + :leaveFeedback (require-auth leave-feedback-mutation) + ;; TODO : do require auth + :updateUser (require-auth update-user-mutation) + :createJobProposal (require-auth create-job-proposal-mutation) + :removeJobProposal (require-auth remove-job-proposal-mutation) + :replayEvents replay-events + :githubSignUp (require-auth github-signup-mutation) + :linkedinSignUp (require-auth linkedin-signup-mutation)} + ;; :Date ; TODO: https://www.apollographql.com/docs/apollo-server/schema/custom-scalars/#example-the-date-scalar + }) diff --git a/server/src/ethlance/server/graphql/server.cljs b/server/src/ethlance/server/graphql/server.cljs index 99b58a6a..0701147f 100644 --- a/server/src/ethlance/server/graphql/server.cljs +++ b/server/src/ethlance/server/graphql/server.cljs @@ -1,15 +1,17 @@ (ns ethlance.server.graphql.server - (:require [cljs.nodejs :as nodejs] - [cljs.reader :refer [read-string]] - [district.server.config :as config] - [district.shared.async-helpers :refer [promise->]] - [ethlance.server.graphql.middlewares :as middlewares] - [ethlance.server.graphql.resolvers :as resolvers] - [ethlance.server.ui-config :as ui-config] - [ethlance.shared.graphql.schema :as schema] - [ethlance.shared.utils :as shared-utils] - [mount.core :as mount :refer [defstate]] - [taoensso.timbre :as log])) + (:require + [cljs.nodejs :as nodejs] + [cljs.reader :refer [read-string]] + [district.server.config :as config] + [district.shared.async-helpers :refer [promise->]] + [ethlance.server.graphql.middlewares :as middlewares] + [ethlance.server.graphql.resolvers :as resolvers] + [ethlance.server.ui-config :as ui-config] + [ethlance.shared.graphql.schema :as schema] + [ethlance.shared.utils :as shared-utils] + [mount.core :as mount :refer [defstate]] + [taoensso.timbre :as log])) + (nodejs/enable-util-print!) @@ -23,12 +25,15 @@ (declare start stop) + (defstate ^{:on-reload :noop} graphql :start (start (merge (:graphql @config/config) (:graphql (mount/args)))) :stop (stop graphql)) -(defn start [opts] + +(defn start + [opts] (let [executable-schema (makeExecutableSchema (clj->js {:typeDefs (gql schema/schema) :resolvers resolvers/resolvers-map})) schema-with-middleware (applyMiddleware executable-schema @@ -51,7 +56,8 @@ :current-user user :timestamp timestamp}))}))] - (js-invoke app "get" "/config" (fn [req res] ; Add JSON /config endpoint for district-ui-config + (js-invoke app "get" "/config" (fn [_req res] + ;; Add JSON /config endpoint for district-ui-config (.then (ui-config/fetch-config {:env-name "UI_CONFIG_PATH"}) (fn [config] (.setHeader res "Access-Control-Allow-Origin", "*") @@ -62,17 +68,21 @@ (log/info "Graphql with express middleware server started...") (js->clj url :keywordize-keys true))))) -(defn stop [graphql] + +(defn stop + [graphql] (promise-> @graphql (fn [{:keys [server]}] (js/Promise. - (fn [resolve _] - (js-invoke server "close" (fn [] - (log/debug "Graphql server stopped...") - (resolve ::stopped)))))))) + (fn [resolve _] + (js-invoke server "close" (fn [] + (log/debug "Graphql server stopped...") + (resolve ::stopped)))))))) + ;; TODO : implement restart -(defn restart [] +(defn restart + [] (log/debug "restarting graphql server") #_(let [opts (merge (:graphql @config/config) (:graphql (mount/args)))] diff --git a/server/src/ethlance/server/graphql/utils.cljs b/server/src/ethlance/server/graphql/utils.cljs index e134586d..e2c1b443 100644 --- a/server/src/ethlance/server/graphql/utils.cljs +++ b/server/src/ethlance/server/graphql/utils.cljs @@ -1,13 +1,17 @@ (ns ethlance.server.graphql.utils - (:require [cljs.nodejs :as nodejs] - [district.graphql-utils :refer [gql->clj kw->gql-name]] - [graphql-query.core :refer [graphql-query]])) + (:require + [cljs.nodejs :as nodejs] + [district.graphql-utils :refer [gql->clj kw->gql-name]] + [graphql-query.core :refer [graphql-query]])) + (def axios (nodejs/require "axios")) (def graphql (nodejs/require "graphql")) (def parse-graphql (aget graphql "parse")) -(defn parse-query [query] + +(defn parse-query + [query] (cond (string? query) {:query-str query :query (parse-graphql query)} @@ -17,8 +21,10 @@ {:query-str query-str :query (parse-graphql query-str)}))) -(defn run-query [{:keys [:url :type :query :access-token] - :or {type "query"}}] + +(defn run-query + [{:keys [:url :type :query :access-token] + :or {type "query"}}] (let [{:keys [:query-str]} (parse-query {:queries [query]})] (-> (axios (clj->js (cond-> {:url url :method :post diff --git a/server/src/ethlance/server/ipfs.cljs b/server/src/ethlance/server/ipfs.cljs index 57f196eb..24fbc95b 100644 --- a/server/src/ethlance/server/ipfs.cljs +++ b/server/src/ethlance/server/ipfs.cljs @@ -1,20 +1,24 @@ (ns ethlance.server.ipfs (:refer-clojure :exclude [get]) - (:require [cljs-ipfs-api.core :as ipfs-core] - [cljs-ipfs-api.files :as ipfs-files] - [clojure.core.async - :as - async - :refer - [! chan close! go put!] - :include-macros - true] - [clojure.tools.reader.edn :as edn] - [district.server.config :refer [config]] - [mount.core :as mount :refer [defstate]] - [taoensso.timbre :as log])) + (:require + [cljs-ipfs-api.core :as ipfs-core] + [cljs-ipfs-api.files :as ipfs-files] + [clojure.core.async + :as + async + :refer + [! chan close! go put!] + :include-macros + true] + [clojure.tools.reader.edn :as edn] + [district.server.config :refer [config]] + [mount.core :as mount :refer [defstate]] + [taoensso.timbre :as log])) + (def buffer (js/require "buffer")) + + (defn to-buffer "Convert object into buffer used by IPFS `add!`. @@ -25,6 +29,7 @@ (let [Buffer (.-Buffer buffer)] (.from Buffer x))) + (defn start "Start the mount component." [opts] @@ -36,12 +41,14 @@ (log/error "Failed to connect to IPFS node" {:error e}) (throw (js/Error. "Can't connect to IPFS node"))))) + (defstate ipfs :start (start (merge (:ipfs @config) (:ipfs (mount/args)))) :stop (do (log/info "IPFS Instance Stopped...") :stopped)) + (defn add! "Add data to the IPFS network. @@ -54,32 +61,34 @@ (let [success-chan (chan 1) error-chan (chan 1)] (go (ipfs-files/add - data - (fn [error result] - (when error - (put! error-chan error) - (close! success-chan)) - (when result - (put! success-chan (:Hash result)) - (close! error-chan))))) + data + (fn [error result] + (when error + (put! error-chan error) + (close! success-chan)) + (when result + (put! success-chan (:Hash result)) + (close! error-chan))))) [success-chan error-chan])) + (defn get [ipfs-hash] (let [success-chan (chan 1) error-chan (chan 1)] (go (ipfs-files/fget - (str "/ipfs/" ipfs-hash) - {:req-opts {:compress false :json true}} - (fn [error result] - (when error - (put! error-chan error) - (close! success-chan)) - (when result - (put! success-chan result) - (close! error-chan))))) + (str "/ipfs/" ipfs-hash) + {:req-opts {:compress false :json true}} + (fn [error result] + (when error + (put! error-chan error) + (close! success-chan)) + (when result + (put! success-chan result) + (close! error-chan))))) [success-chan error-chan])) + (defn add-edn! "Add a clojure data structure to the IPFS network, where `data` is a clojure data structure. @@ -124,6 +133,7 @@ (log/error (str "EDN Parse Error: " e)) nil))) + (defn get-edn "Get the clojure data structure stored as an EDN value for the resulting `hash`." diff --git a/server/src/ethlance/server/syncer.cljs b/server/src/ethlance/server/syncer.cljs index d0b15963..069972ad 100644 --- a/server/src/ethlance/server/syncer.cljs +++ b/server/src/ethlance/server/syncer.cljs @@ -1,35 +1,41 @@ (ns ethlance.server.syncer - (:require [bignumber.core :as bn] - [camel-snake-kebab.core :as camel-snake-kebab] - [clojure.core.async :as async :refer [flat-map - enum-val->token-type]] - [ethlance.shared.token-utils :as token-utils] - [mount.core :as mount :refer [defstate]] - [taoensso.timbre :as log])) + (:require + [bignumber.core :as bn] + [camel-snake-kebab.core :as camel-snake-kebab] + [cljs.core.async.impl.protocols :refer [ReadPort]] + [clojure.core.async :as async :refer [flat-map + enum-val->token-type]] + [ethlance.shared.token-utils :as token-utils] + [ethlance.shared.utils :as shared-utils] + [honeysql.core :as sql] + [mount.core :as mount :refer [defstate]] + [taoensso.timbre :as log])) + (declare start stop) + (defstate ^{:on-reload :noop} syncer :start (start) :stop (stop)) + (defn get-timestamp ([] (get-timestamp {})) - ([event] (.now js/Date))) + ([_event] (.now js/Date))) + -(defn build-ethlance-job-data-from-ipfs-object [ethlance-job-data] +(defn build-ethlance-job-data-from-ipfs-object + [ethlance-job-data] {:job/title (:job/title ethlance-job-data) :job/description (:job/description ethlance-job-data) :job/category (:job/category ethlance-job-data) @@ -39,158 +45,161 @@ :job/bid-option (:job/bid-option ethlance-job-data) :job/estimated-project-length (:job/estimated-project-length ethlance-job-data) :job/invitation-only? nil ; TODO: where does it come from - :job/required-availability (:job/required-availability ethlance-job-data) - }) + :job/required-availability (:job/required-availability ethlance-job-data)}) -(defn ensure-db-token-details [token-type token-address conn] + +(defn ensure-db-token-details + [token-type token-address conn] (safe-go (let [eth-token-details {:address "0x0000000000000000000000000000000000000000" :name "Ether" :symbol "ETH" :type :eth :decimals 18}] - (if (not (>> Handling event job-created" args)) - (println ">>> ipfs-data | type ipfs-data" {:ipfs-data (:ipfs-data args) :event event}) - (let [ipfs-hash (shared-utils/hex->base58 (:ipfs-data args)) - ipfs-job-content (flat-map (first (:offered-values args))) - token-address (:token-address offered-value) - token-type (enum-val->token-type (:token-type offered-value)) - for-the-db (merge {:job/id (:job args) - :job/status "active" ;; draft -> active -> finished hiring -> closed - :job/creator (:creator args) - :job/date-created (get-timestamp event) - :job/date-updated (get-timestamp event) - - :job/token-type token-type - :job/token-amount (:token-amount offered-value) - :job/token-address token-address - :job/token-id (:token-id offered-value) - :invited-arbiters (get-in args [:invited-arbiters] [])} - (build-ethlance-job-data-from-ipfs-object ipfs-job-content))] - (>> Handling event job-created" args)) + (println ">>> ipfs-data | type ipfs-data" {:ipfs-data (:ipfs-data args) :event event}) + (let [ipfs-hash (shared-utils/hex->base58 (:ipfs-data args)) + ipfs-job-content (flat-map (first (:offered-values args))) + token-address (:token-address offered-value) + token-type (enum-val->token-type (:token-type offered-value)) + for-the-db (merge {:job/id (:job args) + :job/status "active" ; draft -> active -> finished hiring -> closed + :job/creator (:creator args) + :job/date-created (get-timestamp event) + :job/date-updated (get-timestamp event) + + :job/token-type token-type + :job/token-amount (:token-amount offered-value) + :job/token-address token-address + :job/token-id (:token-id offered-value) + :invited-arbiters (get args :invited-arbiters [])} + (build-ethlance-job-data-from-ipfs-object ipfs-job-content))] + (base58 (:ipfs-data args)))) - job-story-id (:job-story/id ipfs-data) - invoicer (:invoicer args) - offered-value (offered-vec->flat-map (first (:invoiced-value args))) - invoice-message {:job-story/id job-story-id - :message/type :job-story-message - :job-story-message/type :invoice - :message/text (:message/text ipfs-data) - :message/creator invoicer - :message/date-created (get-timestamp) - :invoice/date-requested (get-timestamp) - :invoice/status "created" - :invoice/amount-requested (:token-amount offered-value) - :invoice/hours-worked (:invoice/hours-worked ipfs-data) - :invoice/hourly-rate (:invoice/hourly-rate ipfs-data) - :invoice/ref-id (:invoice-id args)}] - (base58 (:ipfs-data args)))) + job-story-id (:job-story/id ipfs-data) + invoicer (:invoicer args) + offered-value (offered-vec->flat-map (first (:invoiced-value args))) + invoice-message {:job-story/id job-story-id + :message/type :job-story-message + :job-story-message/type :invoice + :message/text (:message/text ipfs-data) + :message/creator invoicer + :message/date-created (get-timestamp) + :invoice/date-requested (get-timestamp) + :invoice/status "created" + :invoice/amount-requested (:token-amount offered-value) + :invoice/hours-worked (:invoice/hours-worked ipfs-data) + :invoice/hourly-rate (:invoice/hourly-rate ipfs-data) + :invoice/ref-id (:invoice-id args)}] + (base58 (:ipfs-data args)))) - job-story-id (:job-story/id ipfs-data) - invoicer (:invoicer args) - invoice-message {:job-story/id (:job-story/id ipfs-data) - :invoice/id (or (:invoice/id ipfs-data) (:invoice-id ipfs-data)) - :message/type :job-story-message - :job-story-message/type :payment - :message/creator (:payer ipfs-data) - :message/date-created (get-timestamp) - :message/text "Invoice paid" - :invoice/hours-worked (:invoice/hours-worked ipfs-data) - :invoice/hourly-rate (:invoice/hourly-rate ipfs-data) - :invoice/date-paid (get-timestamp) - :invoice/status "paid"}] - - (base58 (:ipfs-data args)))) + invoice-message {:job-story/id (:job-story/id ipfs-data) + :invoice/id (or (:invoice/id ipfs-data) (:invoice-id ipfs-data)) + :message/type :job-story-message + :job-story-message/type :payment + :message/creator (:payer ipfs-data) + :message/date-created (get-timestamp) + :message/text "Invoice paid" + :invoice/hours-worked (:invoice/hours-worked ipfs-data) + :invoice/hourly-rate (:invoice/hourly-rate ipfs-data) + :invoice/date-paid (get-timestamp) + :invoice/status "paid"}] + + (>> handle-dispute-raised" {:args args :dispute-raised-event dispute-raised-event}) - (let [ipfs-data (base58 (:ipfs-data args)))) - job-id (:job args) - invoice-id (:invoice-id args) - job-story (>> handle-dispute-raised job-story" job-story) - dispute-message {:job-story/id (:job-story/id job-story) - :message/type :job-story-message - :job-story-message/type :raise-dispute - :invoice/id invoice-id - :message/text (:message/text ipfs-data) - :message/creator (:message/creator ipfs-data) - :message/date-created (.now js/Date)}] + (log/info "Handling event dispute-raised") + (let [ipfs-data (base58 (:ipfs-data args)))) + job-id (:job args) + invoice-id (:invoice-id args) + job-story (base58 (:ipfs-data args)))) - ; job-id (:job args) ; FIXME: after re-deploying the contracts can use this added event field to get the job contract address (instead of relying on IPFS) - job-id (:job/id ipfs-data) - invoice-id (:invoice-id args) - offered-value (offered-vec->flat-map (get-in args [:_value-for-invoicer 0])) - job-story (base58 (:ipfs-data args)))) + ;; job-id (:job args) ; FIXME: after re-deploying the contracts can use this added event field to get the job contract address (instead of relying on IPFS) + job-id (:job/id ipfs-data) + invoice-id (:invoice-id args) + offered-value (offered-vec->flat-map (get-in args [:_value-for-invoicer 0])) + job-story (base58 (:ipfs-data args)))) - job-id (:job args) - candidate-id (:candidate args) - job-story (base58 (:ipfs-data args)))) + job-id (:job args) + job-story-message-type (:job-story-message/type ipfs-data) + message {:job-story/id (:job-story/id ipfs-data) + :job/id job-id + :candidate (:candidate ipfs-data) + :message/type :job-story-message + :job-story-message/type job-story-message-type + :message/text (:text ipfs-data) + :message/creator (:message/creator ipfs-data) + :message/date-created (get-timestamp)}] + (>> Handling event quote-for-arbitration-set" args)) + (log/info (str "Handling event quote-for-arbitration-set" args)) (let [quoted-value (offered-vec->flat-map (first (:quote args))) token-type (enum-val->token-type (:token-type quoted-value)) for-the-db {:job/id (:job args) @@ -203,9 +212,12 @@ :job-arbiter/status "quote-set"}] (>> Handling event quote-for-arbitration-accepted" args)) (let [arbiter-id (:arbiter args) @@ -219,26 +231,27 @@ [:= :JobArbiter.job-arbiter/status "accepted"]]} previous-accepted-arbiter (>> Handling event ArbitersInvited" args)) (let [job-id (:job args) arbiters (:arbiters args)] (doseq [arbiter arbiters] - ; Guard against error of adding same arbitrer more than once (can be invited multiple times) + ;; Guard against error of adding same arbitrer more than once (can be invited multiple times) (if (not (:exists (>> handle-arbiters-invited Avoided adding duplicate" {:job job-id :arbiter arbiter})))))) -(defn handle-job-ended [conn _ {:keys [args] :as event}] +(defn handle-job-ended + [conn _ {:keys [args]}] (safe-go - (log/info (str ">>> Handling event job-ended" args)) + (log/info (str "Handling event job-ended" args)) (let [job-id (:job args) job-status "ended" stories (>> HANDLE TEST EVENT args: " args)) -;;;;;;;;;;;;;;;;;; + +;; ;; Syncer Start ;; -;;;;;;;;;;;;;;;;;; +;; -(defn- block-timestamp* [block-number] +(defn- block-timestamp* + [block-number] (let [out-ch (async/promise-chan)] (smart-contracts/wait-for-block block-number (fn [error result] (if error @@ -301,9 +321,11 @@ (async/put! out-ch timestamp))))) out-ch)) + (def block-timestamp (memoize block-timestamp*)) + (defn- build-dispatcher "Dispatcher is a function you can call with a event map and it will process it with syncer." [web3-events-map events-callbacks] @@ -313,43 +335,43 @@ web3-events-map)] (fn [err {:keys [:block-number] :as event}] (safe-go - (let [contract-key (-> event :contract :contract-key) - event-key (-> event :event) - handler (get contract-ev->handler [contract-key event-key]) - conn (>> syncer DISPATCHER handling" {:contract-key contract-key :event-key event-key :handler handler :event event}) - (try - (let [block-timestamp ( event - (update :event camel-snake-kebab/->kebab-case) - (update-in [:args :version] bn/number) - (update-in [:args :timestamp] (fn [timestamp] - (if timestamp - (bn/number timestamp) - block-timestamp)))) - _ (db/begin-tx conn) - res (handler conn err event) - _ (db/commit-tx conn) - ] - ;; Calling a handler can throw or return a go block (when using safe-go) - ;; in the case of async ones, the go block will return the js/Error. - ;; In either cases push the event to the queue, so it can be replayed later - (when (satisfies? ReadPort res) - (let [r ( event :contract :contract-key) + event-key (-> event :event) + handler (get contract-ev->handler [contract-key event-key]) + conn (>> syncer DISPATCHER handling" {:contract-key contract-key :event-key event-key :handler handler :event event}) + (try + (let [block-timestamp ( event + (update :event camel-snake-kebab/->kebab-case) + (update-in [:args :version] bn/number) + (update-in [:args :timestamp] (fn [timestamp] + (if timestamp + (bn/number timestamp) + block-timestamp)))) + _ (db/begin-tx conn) + res (handler conn err event) + _ (db/commit-tx conn)] + ;; Calling a handler can throw or return a go block (when using safe-go) + ;; in the case of async ones, the go block will return the js/Error. + ;; In either cases push the event to the queue, so it can be replayed later + (when (satisfies? ReadPort res) + (let [r ( event :contract :contract-key) (-> event :event)])) + (defmethod process-event :default [_ {:keys [contract] :as event}] (go (log/warn (str/format "Unprocessed Event: %s\n%s" (pr-str (:contract-key contract)) (pp-str event))))) + (defmethod process-event [:ethlance-registry :EthlanceEvent] [_ event] (log/debug "Processing Ethlance Event") (log/debug event) (go ( args :event_name str/keyword))) + (defmethod process-registry-event :default [{:keys [args] :as event}] (go (log/warn (str/format "Unprocessed Registry Event: %s\n%s" diff --git a/server/src/ethlance/server/ui_config.cljs b/server/src/ethlance/server/ui_config.cljs index 6677150d..762806dc 100644 --- a/server/src/ethlance/server/ui_config.cljs +++ b/server/src/ethlance/server/ui_config.cljs @@ -1,13 +1,14 @@ (ns ethlance.server.ui-config (:require - [district.shared.async-helpers :refer [clj (.parse js/JSON string) :keywordize-keys true)) -(defn- parse-edn [string] + +(defn- parse-edn + [string] (cljs.reader/read-string string)) + (defn- parse-meta "Gracefully handles JSON or EDN data from IPFS" [{:keys [:content :on-success :on-error]}] @@ -23,28 +30,30 @@ (catch :default e (on-error e))))))) -(defn get-ipfs-meta [conn meta-hash] + +(defn get-ipfs-meta + [conn meta-hash] (js/Promise. - (fn [resolve reject] - (log/info (str "Downloading: " "/ipfs/" meta-hash) ::get-ipfs-meta) - (ipfs-files/fget (str "/ipfs/" meta-hash) - {:req-opts {:compress false}} - (fn [err content] - (cond - err - (let [err-txt "Error when retrieving metadata from ipfs"] - (log/error err-txt (merge {:meta-hash meta-hash - :connection conn - :error err}) - ::get-ipfs-meta) - (reject (str err-txt " : " err))) - - (empty? content) - (let [err-txt "Empty ipfs content"] - (log/error err-txt {:meta-hash meta-hash - :connection conn} ::get-ipfs-meta) - (reject err-txt)) - - :else (parse-meta {:content content - :on-success resolve - :on-error reject}))))))) + (fn [resolve reject] + (log/info (str "Downloading: " "/ipfs/" meta-hash) ::get-ipfs-meta) + (ipfs-files/fget (str "/ipfs/" meta-hash) + {:req-opts {:compress false}} + (fn [err content] + (cond + err + (let [err-txt "Error when retrieving metadata from ipfs"] + (log/error err-txt (merge {:meta-hash meta-hash + :connection conn + :error err}) + ::get-ipfs-meta) + (reject (str err-txt " : " err))) + + (empty? content) + (let [err-txt "Empty ipfs content"] + (log/error err-txt {:meta-hash meta-hash + :connection conn} ::get-ipfs-meta) + (reject err-txt)) + + :else (parse-meta {:content content + :on-success resolve + :on-error reject}))))))) diff --git a/server/src/tests/contract/ethlance_test.cljs b/server/src/tests/contract/ethlance_test.cljs index 559782b5..5cc41a0b 100644 --- a/server/src/tests/contract/ethlance_test.cljs +++ b/server/src/tests/contract/ethlance_test.cljs @@ -1,25 +1,30 @@ (ns tests.contract.ethlance-test - (:require [bignumber.core :as bn] - [cljs-web3-next.eth :as web3-eth] - [cljs.test :refer-macros [deftest is testing async]] - [district.server.web3 :refer [web3]] - [ethlance.server.contract.ethlance :as ethlance] - [ethlance.shared.contract-constants :as contract-constants] - [ethlance.shared.smart-contracts-qa :as addresses] - [district.server.smart-contracts :as smart-contracts] - [cljs-web3-next.eth] - [cljs.core.async :refer [= approved") (done))))))) + (deftest ethlance-eth-payment (testing "Paying with ETH for a Ethlance job" (async done @@ -69,103 +75,107 @@ :tokenId not-used-for-erc20} :value payment-in-wei}] (wei wei->eth - approx=] :as cofu])) + (:require + [bignumber.core :as bn] + [cljs-web3-next.eth :as web3-eth] + [cljs.core.async :refer [wei wei->eth + approx=] :as cofu])) + (deftest job-contract-methods (testing "setQuoteForArbitration" @@ -34,12 +36,12 @@ received-amount (get-in quote-from-event [0 1]) received-token-address (get-in quote-from-event [0 0 0 1]) received-token-type (get-in quote-from-event [0 0 0 0])] - ; Job related assertions (for debugging & sanity checking) + ;; Job related assertions (for debugging & sanity checking) (is (not (nil? (:job job-data)))) (is (= payment-in-wei value-held-by-job-contract)) (is (= job-address (:job quote-set-event))) - ; setQuoteForArbitration related assertions + ;; setQuoteForArbitration related assertions (is (= arbiter (:arbiter quote-set-event))) (is (= payment-in-wei received-amount)) (is (= quoted-token-address received-token-address)) @@ -50,18 +52,22 @@ (is (nil? tx-receipt) "Expected tx to fail (return nil) because only invited arbiter can call setQuoteForArbitration")) (done)))))) + (defn offer-from-job-data "Fetches the offered value for token-id (contract-constants/token-types) item from array (returned from create-initialized-job)" [job-data token] (first (filter #(= (get-in % [:token :tokenContract :tokenType]) - (contract-constants/token-types token) ) + (contract-constants/token-types token)) (get-in job-data [:offered-values])))) -(defn close-enough= [a b] + +(defn close-enough= + [a b] (< (abs (- a b)) (* 0.001 (max a b)))) + (deftest invoice-flows (testing "invoice flow (create/pay/cancel)" (async done @@ -77,7 +83,7 @@ [invoice-amounts _extra] (fund-in-eth invoice-amount-eth [] {}) _ (eth (bn/- (bn/number worker-eth-balance-after) (bn/number worker-eth-balance-before)))] - (is (= (int (:invoice-id event-pay-invoice)) invoice-1-id)) - (is (close-enough= worker-eth-change invoice-amount-eth))) - - ; Pay ERC20 invoice - (let [erc-20-token-amount 1 - worker-initial-erc20-balance (eth (bn/- (bn/number worker-eth-balance-after) (bn/number worker-eth-balance-before)))] + (is (= (int (:invoice-id event-pay-invoice)) invoice-1-id)) + (is (close-enough= worker-eth-change invoice-amount-eth))) + + ;; Pay ERC20 invoice + (let [erc-20-token-amount 1 + worker-initial-erc20-balance (wei contributor-b-amount)})) - ; 3. Add ERC1155 from account C + ;; 3. Add ERC1155 from account C [[contributor-c-offer-multi-token] _] (clj ( balance-after balance-before) "Withdrawing should have increased A's balance") (is (approx= allowed-error-pct (eth->wei contributor-a-amount) balance-change) "After withdrawal the ETH must end up at A's account")) - ; 5. C withdraws his ERC1155 + ;; 5. C withdraws his ERC1155 (let [_ (wei contributor-b-amount) balance-change) "After withdrawal the ETH must end up at B's account")) (done)))))) + (deftest raise-resolve-disputes (testing "Raising & resolving disputes" (async done (go - ; 1. Job gets set up & funded by A with 2 ERC20 - ; 2. A adds 4 ERC20 - ; 3. B adds 3 ERC20 - ; 4. Worker raises invoice for 9 ERC20 - ; 5. Worker raises dispute - ; 6. Arbiter resolves dispute with TokenValue[] value = 6 (2/3) - ; 7. Payouts (proportional): - ; - Worker (2/3)*9 = *6* (2/3 of the invoice according to arbiter) - ; - A gets (1/3)*(2/3)*9 = *2* (1/3 remaining, his contribution was 2/3) - ; - B gets (1/3)*(1/3)*9 = *1* + ;; 1. Job gets set up & funded by A with 2 ERC20 + ;; 2. A adds 4 ERC20 + ;; 3. B adds 3 ERC20 + ;; 4. Worker raises invoice for 9 ERC20 + ;; 5. Worker raises dispute + ;; 6. Arbiter resolves dispute with TokenValue[] value = 6 (2/3) + ;; 7. Payouts (proportional): + ;; - Worker (2/3)*9 = *6* (2/3 of the invoice according to arbiter) + ;; - A gets (1/3)*(2/3)*9 = *2* (1/3 remaining, his contribution was 2/3) + ;; - B gets (1/3)*(1/3)*9 = *1* (let [[_owner employer worker sponsor arbiter] (js arbiter-quote)] - {:from arbiter :output :receipt-or-error})) + :set-quote-for-arbitration + [(clj->js arbiter-quote)] + {:from arbiter :output :receipt-or-error})) accept-arbiter-quote-tx ( funds, employer can add funds & pay invoice in 1 step" (async done (go @@ -488,29 +500,29 @@ employer-starting-balance (int (map - (js->clj (clj ( ["marmot" "deer" "mammut" "tiger" "lion" "elephant" "bobcat"] shuffle first) " " - (-> ["control" "design" "programming" "aministartion" "development"] shuffle first)) - description (let [from (rand-int 100)] (subs lorem from (+ 20 from))) - category (get job-categories (rand-int 13)) - status (rand-nth ["hiring" "hiring done"]) - date-created (time/minus (time/now) (time/days (rand-int 60))) - date-updated (time/plus date-created (time/days (rand-int 7))) - expertise-level (rand-int 5) - token-type :eth - token-amount 1000 - estimated-length (case (-> (rand-nth [:hours :days :weeks])) - :hours (time/hours (rand-int 24)) - :days (time/days (rand-int 30)) - :weeks (time/weeks (rand-int 100))) - ;; availability (rand-nth ["Part Time" "Full Time"]) - ;; bid-option (rand-nth ["Hourly Rate" "Bounty"]) - ;; number-of-candidates (rand-int 5) - ;; invitation-only? (rand-nth [true false]) - - language (rand-nth languages) - job {:job/id job-id - :job/title title - :job/description description - :job/category category - :job/status status - :job/date-created (time-coerce/to-long date-created) - :job/date-updated (time-coerce/to-long date-updated) - :job/expertise-level expertise-level - :job/token-type token-type - :job/token-amount token-amount - :job/language-id language} - ethlance-job-id job-id - ethlance-job {:ethlance-job/id ethlance-job-id - :ethlance-job/estimated-lenght (time/in-millis estimated-length) - :ethlance-job/invitation-only? (rand-nth [true false]) - :ethlance-job/required-availability (rand-nth [true false]) - :ethlance-job/hire-address nil - :ethlance-job/bid-option 1}] - ( ["marmot" "deer" "mammut" "tiger" "lion" "elephant" "bobcat"] shuffle first) " " + (-> ["control" "design" "programming" "aministartion" "development"] shuffle first)) + description (let [from (rand-int 100)] (subs lorem from (+ 20 from))) + category (get job-categories (rand-int 13)) + status (rand-nth ["hiring" "hiring done"]) + date-created (time/minus (time/now) (time/days (rand-int 60))) + date-updated (time/plus date-created (time/days (rand-int 7))) + expertise-level (rand-int 5) + token-type :eth + token-amount 1000 + estimated-length (case (-> (rand-nth [:hours :days :weeks])) + :hours (time/hours (rand-int 24)) + :days (time/days (rand-int 30)) + :weeks (time/weeks (rand-int 100))) + ;; availability (rand-nth ["Part Time" "Full Time"]) + ;; bid-option (rand-nth ["Hourly Rate" "Bounty"]) + ;; number-of-candidates (rand-int 5) + ;; invitation-only? (rand-nth [true false]) + + language (rand-nth languages) + job {:job/id job-id + :job/title title + :job/description description + :job/category category + :job/status status + :job/date-created (time-coerce/to-long date-created) + :job/date-updated (time-coerce/to-long date-updated) + :job/expertise-level expertise-level + :job/token-type token-type + :job/token-amount token-amount + :job/language-id language} + ethlance-job-id job-id + ethlance-job {:ethlance-job/id ethlance-job-id + :ethlance-job/estimated-lenght (time/in-millis estimated-length) + :ethlance-job/invitation-only? (rand-nth [true false]) + :ethlance-job/required-availability (rand-nth [true false]) + :ethlance-job/hire-address nil + :ethlance-job/bid-option 1}] + ( message (merge {:message/text (or text (let [from (rand-int 200)] @@ -178,78 +196,88 @@ nil) :direct-message {})))) -(defn generate-job-stories [conn stories-ids jobs [employer candidate _]] + +(defn generate-job-stories + [conn stories-ids jobs [employer candidate _]] (safe-go - (doseq [story-id stories-ids] - (let [{:keys [job-id]} (rand-nth jobs) - status (rand-nth ["proposal pending" "active" "finished" "cancelled"]) - date-created (time/minus (time/now) (time/days (rand-int 60))) - job-story {:job-story/id story-id - :job/id job-id - :job-story/status status - :job-story/date-created (time-coerce/to-long date-created) - :job-story/creator candidate}] - ( [:hours :days :weeks] shuffle first) - :hours (time/hours (rand-int 24)) - :days (time/days (rand-int 30)) - :weeks (time/weeks (rand-int 100))) - date-work-ended (time/plus date-work-started work-duration) - date-paid (when (= "paid" status) (time-coerce/to-long (time/plus date-work-ended (time/days (rand-int 7)))))] - ( [:hours :days :weeks] shuffle first) + :hours (time/hours (rand-int 24)) + :days (time/days (rand-int 30)) + :weeks (time/weeks (rand-int 100))) + date-work-ended (time/plus date-work-started work-duration) + date-paid (when (= "paid" status) (time-coerce/to-long (time/plus date-work-ended (time/days (rand-int 7)))))] + ( candidate-query :data :candidate :user/id str/trim))) (is (= 5 (-> candidate-query :data :candidate :candidate/feedback :total-count))) (is (= 5 (-> candidate-query :data :candidate :candidate/feedback :items count))) - ; FIXME: update the generators to set up DB in a way that resolvers get the correct data - ; (is (= "Employer" (-> candidate-query :data :candidate :candidate/feedback :items first :feedback/from-user-type))) - ; (is (= "Candidate" (-> candidate-query :data :candidate :candidate/feedback :items first :feedback/to-user-type))) + ;; FIXME: update the generators to set up DB in a way that resolvers get the correct data + ;; (is (= "Employer" (-> candidate-query :data :candidate :candidate/feedback :items first :feedback/from-user-type))) + ;; (is (= "Candidate" (-> candidate-query :data :candidate :candidate/feedback :items first :feedback/to-user-type))) (is (= 0 (-> candidate-search-query-and :data :candidate-search :total-count))) - ; FIXME: update the generators to set up DB in a way that resolvers get the correct data - ; (is (every? #(= "Employer" %) (-> employer-query :data :employer :employer/feedback :items (#(map :feedback/to-user-type %)) ))) + ;; FIXME: update the generators to set up DB in a way that resolvers get the correct data + ;; (is (every? #(= "Employer" %) (-> employer-query :data :employer :employer/feedback :items (#(map :feedback/to-user-type %)) ))) - (is (every? #(= "Arbiter" %) (-> arbiter-query :data :employer :arbiter/feedback :items (#(map :feedback/to-user-type %)) ))) + (is (every? #(= "Arbiter" %) (-> arbiter-query :data :employer :arbiter/feedback :items (#(map :feedback/to-user-type %))))) (let [employer-feedbacks (-> employer-query :data :employer :employer/feedback :items) employer-feedback (-> job-query :data :job :job/stories :items first :job-story/employer-feedback) candidate-feedbacks (-> candidate-query :data :candidate :candidate/feedback :items) candidate-feedback (-> job-query :data :job :job/stories :items first :job-story/candidate-feedback)] - (is (= (-> (filter (fn [elem] (and (= job-id (:job/id elem)) - (= (:job-story/id employer-feedback) (:job-story/id elem)))) + (is (= (-> (filter (fn [elem] + (and (= job-id (:job/id elem)) + (= (:job-story/id employer-feedback) (:job-story/id elem)))) employer-feedbacks) first) (-> job-query :data :job :job/stories :items first :job-story/employer-feedback))) (is (= (-> job-query :data :job :job/stories :items first :job-story/candidate-feedback) - (-> (filter (fn [elem] (and (= job-id (:job/id elem)) - (= (:job-story/id candidate-feedback) (:job-story/id elem)))) + (-> (filter (fn [elem] + (and (= job-id (:job/id elem)) + (= (:job-story/id candidate-feedback) (:job-story/id elem)))) candidate-feedbacks) first)))) (let [job-story-invoices (-> job-story-query :data :job-story :job-story/invoices :items) job-story-dispute (-> job-story-query :data :job-story :job-story/dispute :items)] (is (= (-> invoice-query :data :invoice) - (first (filter (fn [elem] (and (= job-id (:job/id elem)) - (= 0 (:job-story/id elem)) - (= invoice-id (:invoice/id elem)))) + (first (filter (fn [elem] + (and (= job-id (:job/id elem)) + (= 0 (:job-story/id elem)) + (= invoice-id (:invoice/id elem)))) job-story-invoices)))) (when-not (empty? job-story-dispute) (is (= (-> dispute-query :data :dispute) - (first (filter (fn [elem] (and (= job-id (:job/id elem)) - (= 0 (:job-story/id elem)))) + (first (filter (fn [elem] + (and (= job-id (:job/id elem)) + (= 0 (:job-story/id elem)))) job-story-dispute)))))) (done))))) + (deftest test-mutations - (async done - (go - (let [api-endpoint "http://localhost:4000/graphql" - m1 (wei [eth-amount] + (:require + [bignumber.core :as bn] + [cljs-web3-next.eth :as web3-eth] + [cljs.core.async :refer [wei + [eth-amount] (let [wei-in-eth (bn/number 10e17)] (bn/number (* wei-in-eth (bn/number eth-amount))))) -(defn wei->eth [wei-amount] + +(defn wei->eth + [wei-amount] (let [wei-in-eth (bn/number 10e17)] (/ (bn/number wei-amount) wei-in-eth))) -(defn diff-percent [first second] + +(defn diff-percent + [first second] (/ (Math/abs (- second first)) second)) + (defn approx= "Returns true if second differs from first less than diff-percent (0..1)" [percent first second] (< (diff-percent first second) percent)) + (defn fund-in-eth "Produces data 2 structures that can be used as input for `ethlance/create-job` @@ -44,6 +53,7 @@ additional-opts {:value amount-in-wei}] [(conj offered-values eth-offered-value) (merge create-job-opts additional-opts)]))) + (defn fund-in-erc20 "Mints ERC20 TestToken for recipient and approves them for Ethlance (or address at :approve-for). Returns 2 data structures to be used for ethlance/create-job" @@ -64,6 +74,7 @@ (smart-contracts/contract-send :token :approve [approve-addr funding-amount] {:from recipient}) [(conj offered-values erc-20-value) create-job-opts])))) + (defn fund-in-erc721 [recipient offered-values create-job-opts & {approval :approval :or {approval true}}] (go @@ -78,6 +89,7 @@ (if approval (>> Running tests.repl/-main") (tests.setup/setup-test-env) (js/setInterval #(println "Exiting after waiting") 100000000)) diff --git a/server/src/tests/runner.cljs b/server/src/tests/runner.cljs index 89378dd8..8eed063b 100644 --- a/server/src/tests/runner.cljs +++ b/server/src/tests/runner.cljs @@ -1,16 +1,18 @@ (ns tests.runner (:require - [district.shared.async-helpers :as async-helpers] [cljs-promises.async] [cljs.nodejs :as nodejs] + [district.shared.async-helpers :as async-helpers] [tests.setup])) + (nodejs/enable-util-print!) (async-helpers/extend-promises-as-channels!) -; Tests get run automatically by shadow.test.node/main which runs tests using cljs.test -; To run specific namespace tests, add --tests= + +;; Tests get run automatically by shadow.test.node/main which runs tests using cljs.test +;; To run specific namespace tests, add --tests= (println "tests.runner Running tests") (tests.setup/setup-test-env) diff --git a/server/src/tests/setup.cljs b/server/src/tests/setup.cljs index 5ed73597..ea5a6cdc 100644 --- a/server/src/tests/setup.cljs +++ b/server/src/tests/setup.cljs @@ -1,17 +1,18 @@ (ns tests.setup (:require - [cljs.nodejs :as nodejs] - [district.server.logging] - [district.server.web3] - [tests.contract.ethlance-test] - [tests.contract.job-test] - [ethlance.shared.smart-contracts-qa :refer [smart-contracts]] ; Needs ETHLANCE_ENV=qa during truffle migrate - [district.server.smart-contracts] - [mount.core :as mount] - [taoensso.timbre :as log])) + [district.server.logging] + [district.server.smart-contracts] + [district.server.web3] + [ethlance.shared.smart-contracts-qa :refer [smart-contracts]] + [mount.core :as mount] + [taoensso.timbre :as log] + [tests.contract.ethlance-test] + [tests.contract.job-test])) -(defn setup-test-env [] - (-> (mount/with-args {:web3 {:url "ws://localhost:8550"} ; d0x-vm: "ws://d0x-vm:8549" hostia: "ws://192.168.32.1:7545" + +(defn setup-test-env + [] + (-> (mount/with-args {:web3 {:url "ws://localhost:8550"} :smart-contracts {:contracts-var #'smart-contracts :contracts-build-path "../resources/public/contracts/build"} diff --git a/shared/src/ethlance/shared/async_utils.clj b/shared/src/ethlance/shared/async_utils.clj index deefab9c..47adb5ce 100644 --- a/shared/src/ethlance/shared/async_utils.clj +++ b/shared/src/ethlance/shared/async_utils.clj @@ -11,14 +11,14 @@ "Pulls from success channel, logs error channel." [form] `(cljs.core.async/enum-val [token-type] + +(defn token-type->enum-val + [token-type] (let [str-token-type (if (keyword? token-type) (name token-type) (str token-type))] (get token-types (keyword (clojure.string/lower-case str-token-type)) :not-found))) -(defn enum-val->token-type [enum-val] + +(defn enum-val->token-type + [enum-val] (get (clojure.set/map-invert token-types) enum-val :not-found)) -; Corresponds to the enum OperationType in Ethlance contract + +;; Corresponds to the enum OperationType in Ethlance contract (def operation-type {:one-step-job-creation 0 :two-step-job-creation 1 :add-funds 2}) -; Corresponds to the enum TargetMethod values in Job contract + + +;; Corresponds to the enum TargetMethod values in Job contract (def job-callback-target-method {:accept-quote-for-arbitration 0 :add-funds 1 :add-funds-and-pay-invoice 2}) + (defn token-value-vec->map "Transforms the EthlanceStructs.TokenValue from nested array into Map. Due to implementation issues the response from web3 gets returned as @@ -29,10 +39,11 @@ (let [get-at (partial get-in value-vec) get-int (comp js/parseInt get-at)] {:token {:tokenContract {:tokenType (get-int [0 0 0]) - :tokenAddress (get-at [0 0 1])} + :tokenAddress (get-at [0 0 1])} :tokenId (get-int [0 1])} :value (get-int [1])})) + (defn offered-vec->nested-map "Basically same (without type conversions) as token-value-vec->map" [offered] @@ -43,8 +54,9 @@ {:tokenType (get-in offered [0 1]) :tokenAddress (get-in offered [0 0 1])}}}) -; Parsed structure (EthlanceStructs.OfferedValue) -; [ [ [3 0xE13fD5Ed78f1306B4C7C9c3C96FDB99CFc943C5B] 1] 6] + +;; Parsed structure (EthlanceStructs.OfferedValue) +;; [ [ [3 0xE13fD5Ed78f1306B4C7C9c3C96FDB99CFc943C5B] 1] 6] (defn offered-vec->flat-map "Basically same (without type conversions) as token-value-vec->map" [offered] @@ -53,14 +65,17 @@ :token-address (get-in offered [0 0 1]) :token-id (js/parseInt (get-in offered [0 1]))}) -(defn json-str->clj-abi [json] + +(defn json-str->clj-abi + [json] (as-> json j - (.parse js/JSON j) - (js->clj j) - (get j "abi") - (clj->js j))) + (.parse js/JSON j) + (js->clj j) + (get j "abi") + (clj->js j))) + (def abi - {:erc20 (json-str->clj-abi (shadow.resource/inline "./abis/ERC20.json" )) + {:erc20 (json-str->clj-abi (shadow.resource/inline "./abis/ERC20.json")) :erc721 (json-str->clj-abi (shadow.resource/inline "./abis/ERC721.json")) :erc1155 (json-str->clj-abi (shadow.resource/inline "./abis/ERC1155.json"))}) diff --git a/shared/src/ethlance/shared/enumeration.cljs b/shared/src/ethlance/shared/enumeration.cljs index c8d17494..33bd122d 100644 --- a/shared/src/ethlance/shared/enumeration.cljs +++ b/shared/src/ethlance/shared/enumeration.cljs @@ -1,7 +1,9 @@ (ns ethlance.shared.enumeration "General Enumeration functions." - (:require [bignumber.core :as bn] - [ethlance.shared.spec-utils :refer [strict-conform]])) + (:require + [bignumber.core :as bn] + [ethlance.shared.spec-utils :refer [strict-conform]])) + (defn kw->val "Strict conversion of a keyword to a value within a map representing a diff --git a/shared/src/ethlance/shared/enumeration/availability.cljs b/shared/src/ethlance/shared/enumeration/availability.cljs index 92e01d68..7605e82d 100644 --- a/shared/src/ethlance/shared/enumeration/availability.cljs +++ b/shared/src/ethlance/shared/enumeration/availability.cljs @@ -1,6 +1,6 @@ (ns ethlance.shared.enumeration.availability (:require - [ethlance.shared.enumeration :as enum])) + [ethlance.shared.enumeration :as enum])) (def enum-availability diff --git a/shared/src/ethlance/shared/enumeration/bid_option.cljs b/shared/src/ethlance/shared/enumeration/bid_option.cljs index a7db0557..0b74aeb0 100644 --- a/shared/src/ethlance/shared/enumeration/bid_option.cljs +++ b/shared/src/ethlance/shared/enumeration/bid_option.cljs @@ -1,6 +1,6 @@ (ns ethlance.shared.enumeration.bid-option (:require - [ethlance.shared.enumeration :as enum])) + [ethlance.shared.enumeration :as enum])) (def enum-bid diff --git a/shared/src/ethlance/shared/enumeration/comment_type.cljs b/shared/src/ethlance/shared/enumeration/comment_type.cljs index 0f270c96..42a9eb4c 100644 --- a/shared/src/ethlance/shared/enumeration/comment_type.cljs +++ b/shared/src/ethlance/shared/enumeration/comment_type.cljs @@ -1,6 +1,7 @@ (ns ethlance.shared.enumeration.comment-type (:require - [ethlance.shared.enumeration :as enum])) + [ethlance.shared.enumeration :as enum])) + (def enum-comment-type {::work-contract 0 diff --git a/shared/src/ethlance/shared/enumeration/currency_type.cljs b/shared/src/ethlance/shared/enumeration/currency_type.cljs index a9e4a970..cab86e51 100644 --- a/shared/src/ethlance/shared/enumeration/currency_type.cljs +++ b/shared/src/ethlance/shared/enumeration/currency_type.cljs @@ -1,10 +1,13 @@ (ns ethlance.shared.enumeration.currency-type - (:require [ethlance.shared.enumeration :as enum])) + (:require + [ethlance.shared.enumeration :as enum])) + (def enum-currency {:eth 0 :usd 1}) + (def kw->val #(enum/kw->val enum-currency %)) (def val->kw #(enum/val->kw enum-currency %)) (def assoc-kw->val #(enum/assoc-kw->val enum-currency %1 %2)) diff --git a/shared/src/ethlance/shared/enumeration/payment_type.cljs b/shared/src/ethlance/shared/enumeration/payment_type.cljs index aae1ab00..9fd99882 100644 --- a/shared/src/ethlance/shared/enumeration/payment_type.cljs +++ b/shared/src/ethlance/shared/enumeration/payment_type.cljs @@ -1,7 +1,7 @@ (ns ethlance.shared.enumeration.payment-type "Represents an enumeration type for different types of payment." (:require - [ethlance.shared.enumeration :as enum])) + [ethlance.shared.enumeration :as enum])) (def enum-payment diff --git a/shared/src/ethlance/shared/enumeration/user_type.cljs b/shared/src/ethlance/shared/enumeration/user_type.cljs index 4f90c8dd..03133d90 100644 --- a/shared/src/ethlance/shared/enumeration/user_type.cljs +++ b/shared/src/ethlance/shared/enumeration/user_type.cljs @@ -1,6 +1,7 @@ (ns ethlance.shared.enumeration.user-type (:require - [ethlance.shared.enumeration :as enum])) + [ethlance.shared.enumeration :as enum])) + (def enum-user-type {::guest 0 diff --git a/shared/src/ethlance/shared/graphql/schema.cljs b/shared/src/ethlance/shared/graphql/schema.cljs index 0fd1612f..162b86d9 100644 --- a/shared/src/ethlance/shared/graphql/schema.cljs +++ b/shared/src/ethlance/shared/graphql/schema.cljs @@ -1,5 +1,6 @@ (ns ethlance.shared.graphql.schema) + (def schema "The main GraphQL Schema" " diff --git a/shared/src/ethlance/shared/graphql_mutations_spec.cljs b/shared/src/ethlance/shared/graphql_mutations_spec.cljs index d970fc2e..ff248ad5 100644 --- a/shared/src/ethlance/shared/graphql_mutations_spec.cljs +++ b/shared/src/ethlance/shared/graphql_mutations_spec.cljs @@ -1,5 +1,6 @@ (ns ethlance.shared.graphql-mutations-spec) + {:mutation/sign-in {:data "string" :data-signature "string"} @@ -76,36 +77,4 @@ :mutation/linkedin-sign-up ;; Already implemented - {} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } - - + {}} diff --git a/shared/src/ethlance/shared/ipfs_files_spec.cljs b/shared/src/ethlance/shared/ipfs_files_spec.cljs index d2d8aa95..80142af0 100644 --- a/shared/src/ethlance/shared/ipfs_files_spec.cljs +++ b/shared/src/ethlance/shared/ipfs_files_spec.cljs @@ -1,5 +1,6 @@ (ns ethlance.shared.ipfs-files-spec) + {:ethlance/job-created {:job/title "string, max 100 chars" :job/description "string, max 5000 chars" diff --git a/shared/src/ethlance/shared/mock.cljs b/shared/src/ethlance/shared/mock.cljs index cba87df4..1a9fe4a0 100644 --- a/shared/src/ethlance/shared/mock.cljs +++ b/shared/src/ethlance/shared/mock.cljs @@ -1,8 +1,8 @@ (ns ethlance.shared.mock "Sets of mock data" (:require - [ethlance.shared.random :as random] - [ethlance.shared.constants :as constants])) + [ethlance.shared.constants :as constants] + [ethlance.shared.random :as random])) (def first-names @@ -18,6 +18,7 @@ "John" "Barney"}) + (def last-names #{"Batman" "Doe" @@ -31,6 +32,7 @@ "Covid" "Spiderman"}) + (def first-title-category #{"Graphical" "Programming" @@ -41,12 +43,14 @@ "Business" "Game"}) + (def second-title-category #{"Architect" "Programmer" "Developer" "Engineer"}) + (defn generate-job-title [] (str (rand-nth (vec first-title-category)) @@ -55,6 +59,7 @@ (when (random/pick-rand-by-dist [[25 true] [75 false]]) " Assistant"))) + (defn generate-mock-job [] {:id 1 @@ -67,22 +72,22 @@ :employer nil :payment-type (random/pick-rand-by-dist - [[60 :hourly-rate] - [20 :fixed-price] - [20 :annual-salary]]) + [[60 :hourly-rate] + [20 :fixed-price] + [20 :annual-salary]]) :experience-level (random/pick-rand-by-dist - [[30 :novice] - [60 :professional] - [10 :expert]]) + [[30 :novice] + [60 :professional] + [10 :expert]]) :project-length (random/pick-rand-by-dist - [[5 :unknown] - [15 :months] - [70 :weeks] - [10 :days]]) + [[5 :unknown] + [15 :months] + [70 :weeks] + [10 :days]]) :availability (random/pick-rand-by-dist - [[20 :part-time] - [80 :full-time]]) + [[20 :part-time] + [80 :full-time]]) :country (rand-nth (into [] constants/countries))}) diff --git a/shared/src/ethlance/shared/random.cljs b/shared/src/ethlance/shared/random.cljs index 69e18c4c..7cdb0cee 100644 --- a/shared/src/ethlance/shared/random.cljs +++ b/shared/src/ethlance/shared/random.cljs @@ -2,7 +2,9 @@ (def ^:dynamic *dist-resolution* 10000000000) -(defn get-distribution [norm-factor tupl] + +(defn get-distribution + [norm-factor tupl] (loop [cstart 0 tupl tupl distrib []] @@ -14,7 +16,9 @@ (conj distrib [[cstart cend] value]))) distrib))) -(defn -pick-rand-by-dist [ds] + +(defn -pick-rand-by-dist + [ds] (let [r (rand *dist-resolution*)] (->> ds (filter (fn [[[start end] _]] @@ -23,6 +27,7 @@ first second))) + (defn pick-rand-by-dist "Pick a value from the provided tuple pairs, where the first value represents a weight value on being picked, and the second value is @@ -53,6 +58,7 @@ distrib (get-distribution norm-factor tupl)] (-pick-rand-by-dist distrib))) + (defn pluck! "Plucks a random value from an atom containing a sequence, and updates the sequence with the plucked value removed. An empty sequence @@ -64,6 +70,7 @@ (swap! *coll (fn [v] (->> v (remove #(= val %)) (into (empty @*coll))))) val))) + (defn rand-nth-n "Retrieve `n` random distinct values from the collection `coll` and return it as a sequence. diff --git a/shared/src/ethlance/shared/routes.cljs b/shared/src/ethlance/shared/routes.cljs index 9092d9a1..e4c2c97f 100644 --- a/shared/src/ethlance/shared/routes.cljs +++ b/shared/src/ethlance/shared/routes.cljs @@ -1,32 +1,33 @@ (ns ethlance.shared.routes) -(def routes [["/" :route/home] - - ;; Users - ["/arbiters" :route.user/arbiters] - ["/candidates" :route.user/candidates] - ["/employers" :route.user/employers] - ["/user/" :route.user/profile] - ["/user/:address" :route.user/profile] - - ;; Jobs - ["/jobs" :route.job/jobs] - ["/jobs/new" :route.job/new] - ["/jobs/contract/:job-story-id" :route.job/contract] - ["/jobs/:id" :route.job/detail] - - ;; Invoices - ["/invoices/new" :route.invoice/new] - ["/invoices/:job-id/:invoice-id" :route.invoice/index] - - ;; Me - ["/me" :route.me/index] - ["/me/sign-up" :route.me/sign-up] - - ;; Misc. - ["/how-it-works" :route.misc/how-it-works] - ["/about" :route.misc/about]]) +(def routes + [["/" :route/home] + + ;; Users + ["/arbiters" :route.user/arbiters] + ["/candidates" :route.user/candidates] + ["/employers" :route.user/employers] + ["/user/" :route.user/profile] + ["/user/:address" :route.user/profile] + + ;; Jobs + ["/jobs" :route.job/jobs] + ["/jobs/new" :route.job/new] + ["/jobs/contract/:job-story-id" :route.job/contract] + ["/jobs/:id" :route.job/detail] + + ;; Invoices + ["/invoices/new" :route.invoice/new] + ["/invoices/:job-id/:invoice-id" :route.invoice/index] + + ;; Me + ["/me" :route.me/index] + ["/me/sign-up" :route.me/sign-up] + + ;; Misc. + ["/how-it-works" :route.misc/how-it-works] + ["/about" :route.misc/about]]) (def dev-routes diff --git a/shared/src/ethlance/shared/smart_contracts_dev.cljs b/shared/src/ethlance/shared/smart_contracts_dev.cljs index c250633f..06bc3c65 100644 --- a/shared/src/ethlance/shared/smart_contracts_dev.cljs +++ b/shared/src/ethlance/shared/smart_contracts_dev.cljs @@ -1,5 +1,6 @@ (ns ethlance.shared.smart-contracts-dev) + (def smart-contracts {:token {:name "TestToken" :address "0xaBcE6db8dB79c9651dFc0bf78496a5CaE63f5379"} :test-nft {:name "TestNft" :address "0x86B08490dc6A78Fc386eF3e017bE40B30C42Eb71"} diff --git a/shared/src/ethlance/shared/smart_contracts_prod.cljs b/shared/src/ethlance/shared/smart_contracts_prod.cljs index 1aadecb7..d275b7a5 100644 --- a/shared/src/ethlance/shared/smart_contracts_prod.cljs +++ b/shared/src/ethlance/shared/smart_contracts_prod.cljs @@ -1,5 +1,6 @@ (ns ethlance.shared.smart-contracts-prod) + (def smart-contracts {:token {:name "TestToken" :address "0x0000000000000000000000000000000000000000"} :test-nft {:name "TestNft" :address "0x0000000000000000000000000000000000000000"} diff --git a/shared/src/ethlance/shared/smart_contracts_qa.cljs b/shared/src/ethlance/shared/smart_contracts_qa.cljs index ef4797bf..bd8a1f90 100644 --- a/shared/src/ethlance/shared/smart_contracts_qa.cljs +++ b/shared/src/ethlance/shared/smart_contracts_qa.cljs @@ -1,5 +1,6 @@ (ns ethlance.shared.smart-contracts-qa) + (def smart-contracts {:token {:name "TestToken" :address "0xC7B869260BdB1516a638A3ae05FBb4EF2496849F"} :test-nft {:name "TestNft" :address "0x06E8636ee84fe530b06997e33C9Fa9ec7738e27C"} diff --git a/shared/src/ethlance/shared/spec.cljs b/shared/src/ethlance/shared/spec.cljs index 9f313bcc..a35d336b 100644 --- a/shared/src/ethlance/shared/spec.cljs +++ b/shared/src/ethlance/shared/spec.cljs @@ -1,6 +1,6 @@ (ns ethlance.shared.spec (:require - ; ["is-ipfs" :as is-ipfs] + ;; ["is-ipfs" :as is-ipfs] [cljs.spec.alpha :as s] [clojure.set :as set] [district.validation :refer [length? email? not-neg?]] @@ -17,25 +17,25 @@ (s/def :user/languages (fn [languages] (and (pos? (count languages)) (set/subset? languages (set constants/languages))))) -; (s/def :user/profile-image is-ipfs/multihash) ; TODO: figure out how to use is-ipfs + + +(s/def :user/languages + (fn [languages] + (and (pos? (count languages)) + (set/subset? languages (set constants/languages))))) + + +;; (s/def :user/profile-image is-ipfs/multihash) ; TODO: figure out how to use is-ipfs (s/def :user/profile-image string?) -(s/def :user/github-code string?) -(s/def :user/github-username (s/nilable string?)) -; Used on ethlance.ui.page.sign-up to validate form fields -; but fails when there's no logged in user -; (def ethereum-address-pattern #"^0x([A-Fa-f0-9]{40})$") -; (s/def :user/id #(re-matches ethereum-address-pattern %)) +;; Used on ethlance.ui.page.sign-up to validate form fields +;; but fails when there's no logged in user +;; (def ethereum-address-pattern #"^0x([A-Fa-f0-9]{40})$") +;; (s/def :user/id #(re-matches ethereum-address-pattern %)) (s/def :user/id #(or (nil? %) (string? %))) -(s/def :user/linkedin-code string?) -(s/def :user/linkedin-redirect-uri string?) -(s/def :user/is-registered-candidate boolean?) -(s/def :user/date-updated int?) -(s/def :candidate/date-updated int?) - (s/def :candidate/professional-title professional-title?) (s/def :candidate/rate not-neg?) (s/def :candidate/rate-currency-id keyword?) @@ -43,16 +43,24 @@ (and (pos? (count categories)) (set/subset? categories constants/categories)))) -(s/def :candidate/skills (fn [skills] - (and (pos? (count skills)) - (<= (count skills) 30) - (set/subset? skills constants/skills)))) + +(s/def :candidate/categories + (fn [categories] + (and (pos? (count categories)) + (set/subset? categories constants/categories)))) + + +(s/def :candidate/skills + (fn [skills] + (and (pos? (count skills)) + (<= (count skills) 30) + (set/subset? skills (set constants/skills))))) + (s/def :candidate/bio bio?) (s/def :employer/professional-title professional-title?) (s/def :employer/bio bio?) -(s/def :employer/date-updated int?) (s/def :arbiter/professional-title professional-title?) (s/def :arbiter/bio bio?) @@ -63,9 +71,12 @@ (s/def :job/title (s/and string? (comp not empty?))) (s/def :job/required-skills (comp not empty?)) + + (s/def :page.new-job/create (s/keys :req [:job/title :job/required-skills])) + (s/def :page.sign-up/update-candidate (s/keys :req [:user/name :user/email @@ -97,6 +108,6 @@ :arbiter/fee])) -(defn validate-keys [props] +(defn validate-keys + [props] (map-kv-vals #(s/valid? %1 %2) props)) - diff --git a/shared/src/ethlance/shared/spec_utils.cljs b/shared/src/ethlance/shared/spec_utils.cljs index b27d1f94..21589b84 100644 --- a/shared/src/ethlance/shared/spec_utils.cljs +++ b/shared/src/ethlance/shared/spec_utils.cljs @@ -2,7 +2,7 @@ "Includes functions for using clojure.spec to conform data at runtime." (:require - [clojure.spec.alpha :as s])) + [clojure.spec.alpha :as s])) (defn strict-conform @@ -25,6 +25,6 @@ (if (s/valid? spec value) value (throw (ex-info - (str "Failed Strict Spec Conform: " (s/explain-str spec value)) - {:type ::spec-strict-conform - :message (s/explain-str spec value)})))) + (str "Failed Strict Spec Conform: " (s/explain-str spec value)) + {:type ::spec-strict-conform + :message (s/explain-str spec value)})))) diff --git a/shared/src/ethlance/shared/token_utils.cljs b/shared/src/ethlance/shared/token_utils.cljs index d282d71a..f390b2f2 100644 --- a/shared/src/ethlance/shared/token_utils.cljs +++ b/shared/src/ethlance/shared/token_utils.cljs @@ -1,19 +1,27 @@ (ns ethlance.shared.token-utils - (:require [cljs-web3-next.eth :as w3-eth] - [cljs-web3-next.core :as w3-core] - [oops.core :refer [ocall ocall+ oget oget+ oset! oapply oapply+]] - ["xhr2" :as xhr2] - ["ethers" :refer [ethers]] - ["evm-proxy-detection" :rename {default detectProxyTarget}] - [cljs-http.client :as http] - [clojure.core.async :as async :refer [")) -(defn set-api-key [new-key] (reset! api-key new-key)) + + +(defn set-api-key + [new-key] + (reset! api-key new-key)) + (defn get-contract-abi ([contract-address] @@ -29,29 +37,40 @@ :action "getabi" :address contract-address :apikey api-key}] - (http/get etherscan-api-url {:query-params params})))) + (http/get etherscan-api-url {:query-params params})))) + -(defn parse-json [json-string] +(defn parse-json + [json-string] (.parse js/JSON json-string)) + (def env :dev) + + (def provider-url (if (= :dev env) "http://localhost:8549" "https://ethereum-mainnet-rpc.allthatnode.com")) -(defn promise->chan [promise] + +(defn promise->chan + [promise] (let [channel (async/chan)] (.then promise #(async/put! channel %)) channel)) -(defn get-proxy-address [contract-address] + +(defn get-proxy-address + [contract-address] (let [web3-instance (w3-core/create-web3 nil provider-url) provider (new (aget ethers "JsonRpcProvider") provider-url) request-fn (fn [params] (.send provider (oget params "method") (oget params "params")))] (promise->chan (detectProxyTarget contract-address request-fn)))) -(defn get-abi-for-token-info [contract-address] + +(defn get-abi-for-token-info + [contract-address] (go (let [response (hex "Useful for converting IPFS hash to a format suitable for storing in Solidity bytes memory _ipfsData @@ -21,6 +27,7 @@ hex/encode (str "0x" ,,,))) + (defn hex->base58 "Useful for converting Solidity bytes memory _ipfsData back to IPFS hash @@ -32,12 +39,22 @@ hex/decode base58/encode)) -(defn eth->wei [eth-amount] (.toWei (.-utils w3) (str eth-amount))) -(defn wei->eth [wei-amount] (.fromWei (.-utils w3) (str wei-amount))) -(defn millis->relative-time [millis] +(defn eth->wei + [eth-amount] + (.toWei (.-utils w3) (str eth-amount))) + + +(defn wei->eth + [wei-amount] + (.fromWei (.-utils w3) (str wei-amount))) + + +(defn millis->relative-time + [millis] (gdate/format (new js/Date (js/parseInt millis)))) + (defn ilike= "Makes case insensitive comparison of string representation of all arguments Note! @@ -48,8 +65,10 @@ [& args] (apply = (map #(clojure.string/lower-case (str %)) args))) + (def ilike!= (comp not ilike=)) + (defn js-obj->clj-map [obj] (-> (fn [result key] @@ -59,6 +78,7 @@ (assoc result key v)))) (reduce {} (.getKeys goog/object obj)))) + (defn deep-merge "Merges nested maps, left to right (overwriting existing values) diff --git a/ui/deps.edn b/ui/deps.edn index 3943e29f..ed57fbc2 100644 --- a/ui/deps.edn +++ b/ui/deps.edn @@ -49,6 +49,7 @@ cljsjs/apollo-fetch {:mvn/version "0.7.0-0"} cljsjs/graphql {:mvn/version "0.13.1-0"} + com.wsscode/fuzzy {:mvn/version "1.0.0"} expound/expound {:mvn/version "0.8.4"} flib/simplebar {:mvn/version "5.0.7-SNAPSHOT"} funcool/bide {:mvn/version "1.7.0"} ; FIXME: district.ui.router requires it but older version is included by some other library diff --git a/ui/resources/public/less/page/profile.less b/ui/resources/public/less/page/profile.less index 47bfe8c8..3f723783 100644 --- a/ui/resources/public/less/page/profile.less +++ b/ui/resources/public/less/page/profile.less @@ -7,7 +7,7 @@ #mixin.box-shadow.none(); background-color: transparent; border-radius: 0; - + > .candidate-profile , > .employer-profile , > .arbiter-profile { #mixin.box-shadow.main(); background-color: white; @@ -17,12 +17,12 @@ display: grid; grid-template-areas: - 'title title' + 'title title' 'biography detail-listing' 'rating detail-listing' 'location detail-listing' 'button-listing detail-listing'; - + grid-template-rows: repeat(5, auto); grid-template-columns: 5fr 2fr; @@ -37,7 +37,7 @@ grid-template-rows: repeat(6, auto); grid-template-columns: auto; } - + @media #media[mobile-query] { padding: 1.5em; } @@ -72,7 +72,7 @@ font-size: 0.9em; font-weight: 300; padding: 0.5em 1.4em; - + } } @@ -82,11 +82,12 @@ padding-right: 1em; font-size: 1.2em; line-height: 1.5em; + white-space: pre; } > .detail-listing { grid-area: detail-listing; - + // defaults for each of the detail listings. .listings() { display: flex; @@ -99,7 +100,7 @@ font-size: 1.1em; padding: 1em 0; } - + > .ethlance-tag { margin: 0.5em 0; } @@ -115,7 +116,7 @@ display: flex; align-items: center; margin: 1em 1em; - + > span { padding: 0 0.5em; } @@ -133,14 +134,14 @@ display: flex; align-items: center; margin: 1em 0; - + @media #media[tablet-query] { justify-content: space-around; } > .button { margin: 0 0.5em; - + @media #media[tablet-query] { width: 40%; } @@ -173,7 +174,7 @@ color: #color[secondary-accent]; padding: 0.5em 0; } - + > .scrollable { > .ethlance-table { width: 600px; diff --git a/ui/src/district/graphql_utils.cljs b/ui/src/district/graphql_utils.cljs index 2522cb7e..9a808765 100644 --- a/ui/src/district/graphql_utils.cljs +++ b/ui/src/district/graphql_utils.cljs @@ -1,18 +1,19 @@ (ns district.graphql-utils (:require - [bignumber.core :as bn] - [camel-snake-kebab.core :as camel-snake] - [camel-snake-kebab.extras :as camel-snake-extras] - [cljs-time.coerce :as tc] - [cljs-time.core :as t] - [clojure.string :as string] - [clojure.walk :as walk] - [district.cljs-utils :refer [js-obj->clj kw->str]] - ["graphql" :as GraphQL])) + ["graphql" :as GraphQL] + [bignumber.core :as bn] + [camel-snake-kebab.core :as camel-snake] + [camel-snake-kebab.extras :as camel-snake-extras] + [cljs-time.coerce :as tc] + [cljs-time.core :as t] + [clojure.string :as string] + [clojure.walk :as walk] + [district.cljs-utils :refer [js-obj->clj kw->str]])) -; (def GraphQL (if (exists? js/GraphQL) -; js/GraphQL -; (require "graphql"))) + +;; (def GraphQL (if (exists? js/GraphQL) +;; js/GraphQL +;; (require "graphql"))) (defn kw->gql-name @@ -23,25 +24,26 @@ (if (#{"ID" "ID!"} nm) nm (str - (when (string/starts-with? nm "__") - "__") - (when (and (keyword? kw) - (namespace kw)) - (str (string/replace (camel-snake/->camelCase (namespace kw)) "." "_") "_")) - (let [first-letter (first nm) - last-letter (last nm) - s (if (and (not= first-letter "_") - (= first-letter (string/upper-case first-letter))) - (camel-snake/->PascalCase nm) - (camel-snake/->camelCase nm))] - (if (= last-letter "?") - (.slice s 0 -1) - s)) - (when (string/ends-with? nm "?") - "_"))))) - - -(defn gql-name->kw [gql-name] + (when (string/starts-with? nm "__") + "__") + (when (and (keyword? kw) + (namespace kw)) + (str (string/replace (camel-snake/->camelCase (namespace kw)) "." "_") "_")) + (let [first-letter (first nm) + last-letter (last nm) + s (if (and (not= first-letter "_") + (= first-letter (string/upper-case first-letter))) + (camel-snake/->PascalCase nm) + (camel-snake/->camelCase nm))] + (if (= last-letter "?") + (.slice s 0 -1) + s)) + (when (string/ends-with? nm "?") + "_"))))) + + +(defn gql-name->kw + [gql-name] (when gql-name (let [k (name gql-name)] (if (string/starts-with? k "__") @@ -63,19 +65,23 @@ (clj->js))) -(defn gql->clj [m] +(defn gql->clj + [m] (->> m (js->clj) - (camel-snake-extras/transform-keys gql-name->kw ))) + (camel-snake-extras/transform-keys gql-name->kw))) -(defn gql-input->clj [input] +(defn gql-input->clj + [input] (reduce (fn [result field] (assoc result (gql-name->kw field) (aget input field))) {} (js-keys input))) -(defn clj->js-root-value [root-value & [opts]] + +(defn clj->js-root-value + [root-value & [opts]] (let [gql-name->kw (or (:gql-name->kw opts) gql-name->kw) kw->gql-name (or (:kw->gql-name opts) kw->gql-name)] @@ -103,7 +109,8 @@ :else root-value))) -(defn js->clj-objects [res] +(defn js->clj-objects + [res] (walk/prewalk (fn [x] (if (and (nil? (type x)) (seq (js-keys x))) @@ -112,13 +119,15 @@ (js->clj res :keywordize-keys true))) -(defn js->clj-response [res & [opts]] +(defn js->clj-response + [res & [opts]] (let [gql-name->kw (or (:gql-name->kw opts) gql-name->kw) resp (js->clj-objects res)] (update resp :data #(camel-snake-extras/transform-keys gql-name->kw %)))) -(defn add-fields-to-schema-types [schema-ast fields] +(defn add-fields-to-schema-types + [schema-ast fields] (let [query-type (js-invoke schema-ast "getQueryType") type-map (js-invoke schema-ast "getTypeMap")] (doseq [type-key (js-keys type-map)] @@ -157,6 +166,7 @@ :parseLiteral (fn [ast] (tc/from-long (aget ast "value")))}) + (def bignumber-scalar-type-config {:name "BigNumber" :description "bignumber.js" @@ -170,11 +180,12 @@ (bn/number (aget ast "value")))}) -(defn add-scalar-type [schema-ast {:keys [:name :description :serialize :parseValue :parseLiteral] - :or {serialize identity - parseValue identity - parseLiteral identity} - :as scalar-type-config}] +(defn add-scalar-type + [schema-ast {:keys [:name :serialize :parseValue :parseLiteral] + :or {serialize identity + parseValue identity + parseLiteral identity} + :as scalar-type-config}] (if (nil? (aget schema-ast "_typeMap" name)) (aset schema-ast "_typeMap" name (new (aget GraphQL "GraphQLScalarType") (clj->js scalar-type-config))) @@ -185,17 +196,20 @@ schema-ast) -(defn add-keyword-type [schema-ast & [{:keys [:disable-serialize?]}]] +(defn add-keyword-type + [schema-ast & [{:keys [:disable-serialize?]}]] (add-scalar-type schema-ast (cond-> keyword-scalar-type-config disable-serialize? (dissoc :serialize)))) -(defn add-date-type [schema-ast & [{:keys [:disable-serialize?]}]] +(defn add-date-type + [schema-ast & [{:keys [:disable-serialize?]}]] (add-scalar-type schema-ast (cond-> date-scalar-type-config disable-serialize? (dissoc :serialize)))) -(defn add-bignumber-type [schema-ast & [{:keys [:disable-serialize?]}]] +(defn add-bignumber-type + [schema-ast & [{:keys [:disable-serialize?]}]] (add-scalar-type schema-ast (cond-> bignumber-scalar-type-config disable-serialize? (dissoc :serialize)))) diff --git a/ui/src/district/ui/component/page.cljs b/ui/src/district/ui/component/page.cljs index 165ed16c..7897f964 100644 --- a/ui/src/district/ui/component/page.cljs +++ b/ui/src/district/ui/component/page.cljs @@ -1,3 +1,3 @@ (ns district.ui.component.page) -(defmulti page identity) \ No newline at end of file +(defmulti page identity) diff --git a/ui/src/district/ui/component/router.cljs b/ui/src/district/ui/component/router.cljs index 57f52411..a8b2787a 100644 --- a/ui/src/district/ui/component/router.cljs +++ b/ui/src/district/ui/component/router.cljs @@ -4,9 +4,11 @@ [district.ui.router.subs :as subs] [re-frame.core :refer [subscribe]])) -(defn router [] + +(defn router + [] (let [active-page (subscribe [::subs/active-page])] (fn [] (let [{:keys [:name :params :query]} @active-page] (when name - ^{:key (str name params query)} [page name]))))) \ No newline at end of file + ^{:key (str name params query)} [page name]))))) diff --git a/ui/src/ethlance/ui/component/button.cljs b/ui/src/ethlance/ui/component/button.cljs index 40f1d69d..ffc6d234 100644 --- a/ui/src/ethlance/ui/component/button.cljs +++ b/ui/src/ethlance/ui/component/button.cljs @@ -1,7 +1,7 @@ (ns ethlance.ui.component.button "An ethlance button component and additional child components." (:require - [ethlance.ui.component.icon :refer [c-icon]])) + [ethlance.ui.component.icon :refer [c-icon]])) (defn c-button @@ -38,18 +38,18 @@ (let [props (dissoc props :disabled? :active? :color :size)] (into [:a.button (merge - {:class [(case color - :primary "primary" - :secondary "secondary" - :warning "warning") - (when disabled? "disabled") - (when active? "active") - (condp = size - :small "small" - :normal "" - :large "large" - :auto "auto")]} - props)] + {:class [(case color + :primary "primary" + :secondary "secondary" + :warning "warning") + (when disabled? "disabled") + (when active? "active") + (condp = size + :small "small" + :normal "" + :large "large" + :auto "auto")]} + props)] children)))) diff --git a/ui/src/ethlance/ui/component/carousel.cljs b/ui/src/ethlance/ui/component/carousel.cljs index ce5da162..aaa16149 100644 --- a/ui/src/ethlance/ui/component/carousel.cljs +++ b/ui/src/ethlance/ui/component/carousel.cljs @@ -1,9 +1,11 @@ (ns ethlance.ui.component.carousel - (:require [ethlance.ui.component.circle-button :refer [c-circle-icon-button]] - [ethlance.ui.component.profile-image :refer [c-profile-image]] - [ethlance.ui.component.rating :refer [c-rating]] - [reagent.core :as r] - ["pure-react-carousel" :as react-carousel])) + (:require + ["pure-react-carousel" :as react-carousel] + [ethlance.ui.component.circle-button :refer [c-circle-icon-button]] + [ethlance.ui.component.profile-image :refer [c-profile-image]] + [ethlance.ui.component.rating :refer [c-rating]] + [reagent.core :as r])) + (defn c-carousel-old "Carousel Component for displaying multiple 'slides' of content @@ -31,30 +33,31 @@ :or {default-index 0}}] (let [*current-index (r/atom default-index)] (r/create-class - {:display-name "ethlance-carousel" - :reagent-render - (fn [_ & children] - (let [first-slide? (<= @*current-index 0) - last-slide? (>= @*current-index (dec (count children)))] - [:div.ethlance-carousel - [:div.slide-listing - (when-not first-slide? - [:div.left-slide]) - [:div.current-slide - (nth children @*current-index)] - (when-not last-slide? - [:div.right-slide])] - [:div.button-listing - [:div.back-button - [c-circle-icon-button - {:name :ic-arrow-left - :hide? first-slide? - :on-click #(swap! *current-index dec)}]] - [:div.forward-button - [c-circle-icon-button - {:name :ic-arrow-right - :hide? last-slide? - :on-click #(swap! *current-index inc)}]]]]))}))) + {:display-name "ethlance-carousel" + :reagent-render + (fn [_ & children] + (let [first-slide? (<= @*current-index 0) + last-slide? (>= @*current-index (dec (count children)))] + [:div.ethlance-carousel + [:div.slide-listing + (when-not first-slide? + [:div.left-slide]) + [:div.current-slide + (nth children @*current-index)] + (when-not last-slide? + [:div.right-slide])] + [:div.button-listing + [:div.back-button + [c-circle-icon-button + {:name :ic-arrow-left + :hide? first-slide? + :on-click #(swap! *current-index dec)}]] + [:div.forward-button + [c-circle-icon-button + {:name :ic-arrow-right + :hide? last-slide? + :on-click #(swap! *current-index inc)}]]]]))}))) + (defn c-feedback-slide [{:keys [id rating author text image-url class]}] @@ -66,7 +69,9 @@ [:div.message text] [:div.name author]]) -(defn c-carousel [{:keys []} & children] + +(defn c-carousel + [{:keys []} & children] [:div.ethlance-new-carousel [:> react-carousel/CarouselProvider {:natural-slide-width 388 :natural-slide-height 300 diff --git a/ui/src/ethlance/ui/component/chat.cljs b/ui/src/ethlance/ui/component/chat.cljs index 203ab0f2..931ad7e1 100644 --- a/ui/src/ethlance/ui/component/chat.cljs +++ b/ui/src/ethlance/ui/component/chat.cljs @@ -1,7 +1,8 @@ (ns ethlance.ui.component.chat (:require - [ethlance.ui.component.profile-image :refer [c-profile-image]] - [district.format :as format])) + [district.format :as format] + [ethlance.ui.component.profile-image :refer [c-profile-image]])) + ;; TODO: format 'text' into paragraphs

(defn c-chat-message @@ -28,10 +29,10 @@ [:span.full-name {:key (str "detail-full-name-" id)} full-name] [:div.info-listing (doall - (for [detail details] - ^{:key (str "detail-" detail)} - [:span.info detail]))] - ; TODO: remove new js/Date after switching to district.ui.graphql that converts Date GQL type automatically + (for [detail details] + ^{:key (str "detail-" detail)} + [:span.info detail]))] + ;; TODO: remove new js/Date after switching to district.ui.graphql that converts Date GQL type automatically [:span.date-updated (format/time-ago (new js/Date timestamp))]]] [:div.text text]]))) @@ -54,7 +55,6 @@ [chat-listing] [:div.ethlance-chat-log (doall - (for [message chat-listing] - (do - ^{:key (str "chat-message-" (:id message) "-" (hash (:text message)))} - [c-chat-message message])))]) + (for [message chat-listing] + ^{:key (str "chat-message-" (:id message) "-" (hash (:text message)))} + [c-chat-message message]))]) diff --git a/ui/src/ethlance/ui/component/checkbox.cljs b/ui/src/ethlance/ui/component/checkbox.cljs index b613d4cb..f0f17645 100644 --- a/ui/src/ethlance/ui/component/checkbox.cljs +++ b/ui/src/ethlance/ui/component/checkbox.cljs @@ -1,6 +1,8 @@ (ns ethlance.ui.component.checkbox - (:require [ethlance.ui.component.inline-svg :refer [c-inline-svg]] - [reagent.core :as r])) + (:require + [ethlance.ui.component.inline-svg :refer [c-inline-svg]] + [reagent.core :as r])) + (defn c-labeled-checkbox "Checkbox Input Component @@ -34,14 +36,14 @@ opts (dissoc opts :label :on-change :default-checked? :checked?)] [:div.ethlance-checkbox (merge - opts - {:on-click - (fn [] - (when on-change - (on-change (not checked?))) - (swap! *checked? not)) - - :class (when checked? "checked")}) + opts + {:on-click + (fn [] + (when on-change + (on-change (not checked?))) + (swap! *checked? not)) + + :class (when checked? "checked")}) [c-inline-svg {:src "/images/svg/checkbox.svg" :width 24 :height 24}] diff --git a/ui/src/ethlance/ui/component/circle_button.cljs b/ui/src/ethlance/ui/component/circle_button.cljs index 537df3ac..c2c63df5 100644 --- a/ui/src/ethlance/ui/component/circle_button.cljs +++ b/ui/src/ethlance/ui/component/circle_button.cljs @@ -1,7 +1,7 @@ (ns ethlance.ui.component.circle-button "Circle button, which usually displays an icon" (:require - [ethlance.ui.component.icon :refer [c-icon]])) + [ethlance.ui.component.icon :refer [c-icon]])) (defn c-circle-icon-button @@ -53,10 +53,10 @@ class-hide (when hide? "hide")] [:a.ethlance-circle-button.ethlance-circle-icon-button (merge - opts - {:class [class-color class-size class-disabled class-hide] - :on-click - (fn [e] - (when (and on-click (not disabled?)) - (on-click e)))}) + opts + {:class [class-color class-size class-disabled class-hide] + :on-click + (fn [e] + (when (and on-click (not disabled?)) + (on-click e)))}) [c-icon {:name name :color color :size size}]]))) diff --git a/ui/src/ethlance/ui/component/currency_input.cljs b/ui/src/ethlance/ui/component/currency_input.cljs index 5c503145..08897fb3 100644 --- a/ui/src/ethlance/ui/component/currency_input.cljs +++ b/ui/src/ethlance/ui/component/currency_input.cljs @@ -1,5 +1,7 @@ (ns ethlance.ui.component.currency-input - (:require [reagent.core :as r])) + (:require + [reagent.core :as r])) + (defn c-currency-input "Currency Component based on the react 'number' input component. diff --git a/ui/src/ethlance/ui/component/email_input.cljs b/ui/src/ethlance/ui/component/email_input.cljs index d3b953c6..97b36a16 100644 --- a/ui/src/ethlance/ui/component/email_input.cljs +++ b/ui/src/ethlance/ui/component/email_input.cljs @@ -1,6 +1,7 @@ (ns ethlance.ui.component.email-input (:require - [reagent.core :as r])) + [reagent.core :as r])) + (defn c-email-input [{:keys [default-value color]}] @@ -17,11 +18,11 @@ opts (dissoc opts :default-value :value :color :on-change :error?)] [:input.ethlance-email-input (merge - opts - {:class [class-color (when error? "error") (when @*dirty? "dirty")] - :value current-value - :on-change (fn [e] - (reset! *dirty? true) - (let [target-value (-> e .-target .-value)] - (reset! *current-value target-value) - (when on-change (on-change target-value))))})])))) + opts + {:class [class-color (when error? "error") (when @*dirty? "dirty")] + :value current-value + :on-change (fn [e] + (reset! *dirty? true) + (let [target-value (-> e .-target .-value)] + (reset! *current-value target-value) + (when on-change (on-change target-value))))})])))) diff --git a/ui/src/ethlance/ui/component/error_message.cljs b/ui/src/ethlance/ui/component/error_message.cljs index 603e1786..3e9322cc 100644 --- a/ui/src/ethlance/ui/component/error_message.cljs +++ b/ui/src/ethlance/ui/component/error_message.cljs @@ -1,7 +1,7 @@ (ns ethlance.ui.component.error-message (:require - [reagent.core :as r] - [taoensso.timbre :as log])) + [reagent.core :as r] + [taoensso.timbre :as log])) (defn c-error-message @@ -13,7 +13,7 @@ (fn [] [:div.error-message [:div.logo - [:img {:src "/images/svg/ethlance_spinner.svg"}]] ;; FIXME + [:img {:src "/images/svg/ethlance_spinner.svg"}]] ; FIXME [:div.message message] (when details [:div.show-button diff --git a/ui/src/ethlance/ui/component/ethlance_logo.cljs b/ui/src/ethlance/ui/component/ethlance_logo.cljs index 4ec39c0a..442cc149 100644 --- a/ui/src/ethlance/ui/component/ethlance_logo.cljs +++ b/ui/src/ethlance/ui/component/ethlance_logo.cljs @@ -1,7 +1,7 @@ (ns ethlance.ui.component.ethlance-logo "The ethlance logo as an SVG image" (:require - [ethlance.ui.component.inline-svg :refer [c-inline-svg]])) + [ethlance.ui.component.inline-svg :refer [c-inline-svg]])) (def default-logo-url "/images/ethlance_logo.svg") @@ -11,7 +11,7 @@ (def black-logo-url "/images/ethlance_logo_bw.svg") -(defn c-ethlance-logo +(defn c-ethlance-logo "Ethlance Logo Component which displays the ethlance logo. # Keyword Arguments diff --git a/ui/src/ethlance/ui/component/file_drag_input.cljs b/ui/src/ethlance/ui/component/file_drag_input.cljs index d9cdcd8a..5ddd9ef1 100644 --- a/ui/src/ethlance/ui/component/file_drag_input.cljs +++ b/ui/src/ethlance/ui/component/file_drag_input.cljs @@ -1,41 +1,47 @@ (ns ethlance.ui.component.file-drag-input - (:require [clojure.string :as str] - [ethlance.ui.component.icon :refer [c-icon]])) + (:require + [clojure.string :as str] + [ethlance.ui.component.icon :refer [c-icon]])) + (def empty-img-src "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=") -(defn id-for-path [path] + +(defn id-for-path + [path] (if (sequential? path) (str/join "-" (map name path)) (name path))) -(defn c-file-drag-input [{:keys [form-data id file-accept-pred on-file-accepted on-file-rejected] - :or {file-accept-pred (constantly true)}}] + +(defn c-file-drag-input + [{:keys [form-data id file-accept-pred on-file-accepted on-file-rejected] + :or {file-accept-pred (constantly true)}}] (let [allow-drop #(.preventDefault %) handle-files-select (fn [files] - (if-let [f (aget files 0)] + (when-let [f (aget files 0)] (let [fprops {:name (.-name f) - :type (.-type f) - :size (.-size f) - :file f}] - (if (file-accept-pred fprops) - (let [url-reader (js/FileReader.) - ab-reader (js/FileReader.)] - (set! (.-onload url-reader) (fn [e] - (let [img-data (-> e .-target .-result) - fmap (assoc fprops :url-data img-data)] - (swap! form-data assoc-in [id :selected-file] fmap)))) - (.readAsDataURL url-reader f) - (set! (.-onload ab-reader) (fn [e] - (let [img-data (-> e .-target .-result) - fmap (assoc fprops :array-buffer img-data)] - (swap! form-data update id merge fmap) - (when on-file-accepted (on-file-accepted fmap))))) - (.readAsArrayBuffer ab-reader f)) - (when on-file-rejected - (on-file-rejected fprops))))))] + :type (.-type f) + :size (.-size f) + :file f}] + (if (file-accept-pred fprops) + (let [url-reader (js/FileReader.) + ab-reader (js/FileReader.)] + (set! (.-onload url-reader) (fn [e] + (let [img-data (-> e .-target .-result) + fmap (assoc fprops :url-data img-data)] + (swap! form-data assoc-in [id :selected-file] fmap)))) + (.readAsDataURL url-reader f) + (set! (.-onload ab-reader) (fn [e] + (let [img-data (-> e .-target .-result) + fmap (assoc fprops :array-buffer img-data)] + (swap! form-data update id merge fmap) + (when on-file-accepted (on-file-accepted fmap))))) + (.readAsArrayBuffer ab-reader f)) + (when on-file-rejected + (on-file-rejected fprops))))))] (fn [{:keys [form-data id] - :as opts}] + :as opts}] (let [{:keys [url-data]} (get-in @form-data [id :selected-file])] [:div.dropzone {:on-drag-over allow-drop @@ -47,10 +53,10 @@ [:img {:src (or url-data empty-img-src)}] (when (not url-data) - [:label.file-input-label - {:for (id-for-path id)} - [c-icon {:name :ic-upload :color :dark-blue :inline? false}] - [:div (get opts :label "File...")]]) + [:label.file-input-label + {:for (id-for-path id)} + [c-icon {:name :ic-upload :color :dark-blue :inline? false}] + [:div (get opts :label "File...")]]) [:input {:type :file :id (id-for-path id) diff --git a/ui/src/ethlance/ui/component/icon.cljs b/ui/src/ethlance/ui/component/icon.cljs index bcb8180a..578480ec 100644 --- a/ui/src/ethlance/ui/component/icon.cljs +++ b/ui/src/ethlance/ui/component/icon.cljs @@ -1,6 +1,6 @@ (ns ethlance.ui.component.icon (:require - [ethlance.ui.component.inline-svg :refer [c-inline-svg]])) + [ethlance.ui.component.inline-svg :refer [c-inline-svg]])) (def icon-listing diff --git a/ui/src/ethlance/ui/component/info_message.cljs b/ui/src/ethlance/ui/component/info_message.cljs index fbcf7647..4f5dcbd3 100644 --- a/ui/src/ethlance/ui/component/info_message.cljs +++ b/ui/src/ethlance/ui/component/info_message.cljs @@ -1,7 +1,7 @@ (ns ethlance.ui.component.info-message (:require - [reagent.core :as r] - [taoensso.timbre :as log])) + [reagent.core :as r] + [taoensso.timbre :as log])) (defn c-info-message diff --git a/ui/src/ethlance/ui/component/inline_svg.cljs b/ui/src/ethlance/ui/component/inline_svg.cljs index 00ab1fb2..7f6ef75a 100644 --- a/ui/src/ethlance/ui/component/inline_svg.cljs +++ b/ui/src/ethlance/ui/component/inline_svg.cljs @@ -8,16 +8,19 @@ - https://stackoverflow.com/questions/24933430/img-src-svg-changing-the-fill-color " (:require - [reagent.core :as r] - ["react" :as react])) + ["react" :as react] + [reagent.core :as r])) + (def *cached-svg-listing (atom {})) + (defn fetch-url "Returns js/Promise" [url] (.fetch js/window url)) + (defn parse-xml-from-string "Parses the given string of XML into a DOM structure. Used to parse the SVG to inline within the page." @@ -25,6 +28,7 @@ (let [parser (js/DOMParser.)] (.parseFromString parser s "text/xml"))) + (defn xml->svg "Removes namespacing from the given XML element to appear as an SVG element." @@ -33,16 +37,21 @@ (doto svg (.removeAttribute "xmlns:a")))) -(defn- clone-element [elnode] + +(defn- clone-element + [elnode] (.cloneNode elnode true)) -(defn- remove-element-children [elnode] + +(defn- remove-element-children + [elnode] (loop [child (aget elnode "lastElementChild")] (when child (.removeChild elnode child) (recur (aget elnode "lastElementChild")))) elnode) + (defn prepare-svg "Prepares the given SVG image residing at the given `url`. @@ -68,54 +77,55 @@ (swap! *cached-svg-listing assoc url svg) (clone-element svg))))))) + (defn c-inline-svg [{:keys [src]}] (let [*inline-svg (r/atom nil) react-ref (react/createRef)] (r/create-class - {:display-name "c-inline-svg" - - :component-did-mount - (fn [] - ;; Preemptive Caching - (if-let [cached-svg (get @*cached-svg-listing src)] - (reset! *inline-svg (clone-element cached-svg)) - (-> (prepare-svg src) - (.then (fn [svg] (reset! *inline-svg svg)))))) - - :component-did-update - (fn [this old-argv] - (let [{:keys [id class width height on-ready src]} - (-> this r/argv second) - old-src (-> old-argv second :src)] - - ;; To support changing the src of an inline-svg, need to - ;; kickstart retrieving the new src and clear out the old - ;; one - (when-not (= src old-src) - (-> (prepare-svg src) - (.then (fn [svg] (reset! *inline-svg svg))))) - - (when @*inline-svg - (let [inline-svg @*inline-svg - elnode (.-current react-ref)] - (when id (.setAttribute inline-svg "id" id)) - (when class (.setAttribute inline-svg "class" class)) - (when width (.setAttribute inline-svg "width" width)) - (when height (.setAttribute inline-svg "height" height)) - (when (not= inline-svg (-> elnode .-firstChild)) - (doto elnode - (remove-element-children) - (.appendChild inline-svg))) - (when on-ready - (on-ready elnode inline-svg)))))) - - :reagent-render - (fn [{:keys [key class width height root-class]}] - (let [style (cond-> {} - width (assoc :width (str width "px")) - height (assoc :height (str height "px")))] - [:div.ethlance-inline-svg - {:class root-class :key key :ref react-ref} - (when-not @*inline-svg [:img.svg {:style (merge style {:opacity 0}) - :class class}])]))}))) + {:display-name "c-inline-svg" + + :component-did-mount + (fn [] + ;; Preemptive Caching + (if-let [cached-svg (get @*cached-svg-listing src)] + (reset! *inline-svg (clone-element cached-svg)) + (-> (prepare-svg src) + (.then (fn [svg] (reset! *inline-svg svg)))))) + + :component-did-update + (fn [this old-argv] + (let [{:keys [id class width height on-ready src]} + (-> this r/argv second) + old-src (-> old-argv second :src)] + + ;; To support changing the src of an inline-svg, need to + ;; kickstart retrieving the new src and clear out the old + ;; one + (when-not (= src old-src) + (-> (prepare-svg src) + (.then (fn [svg] (reset! *inline-svg svg))))) + + (when @*inline-svg + (let [inline-svg @*inline-svg + elnode (.-current react-ref)] + (when id (.setAttribute inline-svg "id" id)) + (when class (.setAttribute inline-svg "class" class)) + (when width (.setAttribute inline-svg "width" width)) + (when height (.setAttribute inline-svg "height" height)) + (when (not= inline-svg (-> elnode .-firstChild)) + (doto elnode + (remove-element-children) + (.appendChild inline-svg))) + (when on-ready + (on-ready elnode inline-svg)))))) + + :reagent-render + (fn [{:keys [key class width height root-class]}] + (let [style (cond-> {} + width (assoc :width (str width "px")) + height (assoc :height (str height "px")))] + [:div.ethlance-inline-svg + {:class root-class :key key :ref react-ref} + (when-not @*inline-svg [:img.svg {:style (merge style {:opacity 0}) + :class class}])]))}))) diff --git a/ui/src/ethlance/ui/component/loading_spinner.cljs b/ui/src/ethlance/ui/component/loading_spinner.cljs index a0401c32..87dedea4 100644 --- a/ui/src/ethlance/ui/component/loading_spinner.cljs +++ b/ui/src/ethlance/ui/component/loading_spinner.cljs @@ -1,5 +1,7 @@ (ns ethlance.ui.component.loading-spinner) -(defn c-loading-spinner [] + +(defn c-loading-spinner + [] [:div.loading-spinner [:img {:src "/images/svg/ethlance_spinner.svg"}]]) diff --git a/ui/src/ethlance/ui/component/main_layout.cljs b/ui/src/ethlance/ui/component/main_layout.cljs index 7285c37f..358345ea 100644 --- a/ui/src/ethlance/ui/component/main_layout.cljs +++ b/ui/src/ethlance/ui/component/main_layout.cljs @@ -1,31 +1,32 @@ (ns ethlance.ui.component.main-layout - (:require [ethlance.ui.component.circle-button :refer [c-circle-icon-button]] - [district.ui.component.notification :as component.notification] - [ethlance.ui.component.main-navigation-bar - :refer - [c-main-navigation-bar]] - [ethlance.ui.component.main-navigation-menu - :refer - [c-main-navigation-menu]] - [ethlance.ui.component.mobile-navigation-bar - :refer - [c-mobile-navigation-bar]] - [district.ui.router.subs :as router.subs] - [re-frame.core :as re] - [akiroz.re-frame.storage] - [ethlance.ui.component.sign-in-dialog :refer [c-sign-in-dialog] :as sidi])) + (:require + [akiroz.re-frame.storage] + [clojure.string] + [district.ui.component.notification :as component.notification] + [district.ui.router.subs :as router.subs] + [ethlance.ui.component.circle-button :refer [c-circle-icon-button]] + [ethlance.ui.component.main-navigation-bar + :refer + [c-main-navigation-bar]] + [ethlance.ui.component.main-navigation-menu + :refer + [c-main-navigation-menu]] + [ethlance.ui.component.mobile-navigation-bar + :refer + [c-mobile-navigation-bar]] + [ethlance.ui.component.sign-in-dialog :refer [c-sign-in-dialog] :as sidi] + [re-frame.core :as re])) + (defn page-title-from-route-name "Example: :route.job/detail => Ethlance: Job Detail" [route-name] (let [app-name "Ethlance" name-parts-from-route-ns (rest (clojure.string/split (namespace route-name) ".")) - name-part-from-route-name(name route-name) + name-part-from-route-name (name route-name) name-parts (flatten [app-name ":" name-parts-from-route-ns name-part-from-route-name])] (clojure.string/join " " (map clojure.string/capitalize name-parts)))) -(defn has-active-session? [] - (not (nil? (akiroz.re-frame.storage/<-store :ethlance)))) (defn c-main-layout "The main layout of each page in the ethlance ui. diff --git a/ui/src/ethlance/ui/component/main_navigation_bar.cljs b/ui/src/ethlance/ui/component/main_navigation_bar.cljs index 5237b832..806c46aa 100644 --- a/ui/src/ethlance/ui/component/main_navigation_bar.cljs +++ b/ui/src/ethlance/ui/component/main_navigation_bar.cljs @@ -2,27 +2,27 @@ (:require [district.format :as format] [district.ui.conversion-rates.subs :as conversion-subs] + [district.ui.graphql.subs :as gql] [district.ui.web3-account-balances.subs :as balances-subs] [district.ui.web3-accounts.subs :as accounts-subs] [district.web3-utils :as web3-utils] [ethlance.ui.component.ethlance-logo :refer [c-ethlance-logo]] [ethlance.ui.component.profile-image :refer [c-profile-image]] [ethlance.ui.event.sign-in] - [ethlance.ui.subscriptions :as ethlance-subs] [ethlance.ui.util.navigation :as util.navigation] - [district.ui.graphql.subs :as gql] [print.foo :include-macros true] [re-frame.core :as re])) -(defn c-signed-in-user-info [] + +(defn c-signed-in-user-info + [] (let [active-account @(re/subscribe [::accounts-subs/active-account]) query [:user {:user/id active-account} [:user/id :user/name :user/email :user/profile-image]] - result (re/subscribe [::gql/query {:queries [query]} {:refetch-on #{:ethlance.user-profile-updated}}]) - profile-image (get-in @result [:user :user/profile-image])] + result (re/subscribe [::gql/query {:queries [query]} {:refetch-on #{:ethlance.user-profile-updated}}])] [:a.profile (util.navigation/link-params {:route :route.user/profile :params {:address active-account}}) (when (not (:graphql/loading? @result)) [c-profile-image {:size :small :src (get-in @result [:user :user/profile-image])}]) @@ -32,15 +32,14 @@ active-account (format/truncate active-account 12) :else "Wallet not connected")]])) + (defn c-main-navigation-bar "Main Navigation bar seen while the site is in desktop-mode." [] (let [active-account (re/subscribe [::accounts-subs/active-account]) - active-session (re/subscribe [::ethlance-subs/active-session]) balance-eth (re/subscribe [::balances-subs/active-account-balance])] (fn [] - (let [active-user-id (or (:user/id @active-session) @active-account) - eth-balance (web3-utils/wei->eth-number (or @balance-eth 0))] + (let [eth-balance (web3-utils/wei->eth-number (or @balance-eth 0))] [:div.main-navigation-bar [c-ethlance-logo {:color :white @@ -50,7 +49,7 @@ :href (util.navigation/resolve-route {:route :route/home}) :inline? false}] (when @active-account [c-signed-in-user-info]) - [:div.account-balances - [:div.token-value (format/format-eth eth-balance)] - [:div.usd-value (-> @(re/subscribe [::conversion-subs/convert :ETH :USD eth-balance]) + [:div.account-balances + [:div.token-value (format/format-eth eth-balance)] + [:div.usd-value (-> @(re/subscribe [::conversion-subs/convert :ETH :USD eth-balance]) (format/format-currency {:currency "USD"}))]]])))) diff --git a/ui/src/ethlance/ui/component/main_navigation_menu.cljs b/ui/src/ethlance/ui/component/main_navigation_menu.cljs index bb7192cf..e7f525fa 100644 --- a/ui/src/ethlance/ui/component/main_navigation_menu.cljs +++ b/ui/src/ethlance/ui/component/main_navigation_menu.cljs @@ -1,13 +1,13 @@ (ns ethlance.ui.component.main-navigation-menu (:require - [district.ui.router.subs :as ui.router.subs] - [district.ui.web3-accounts.subs :as accounts-subs] - [ethlance.ui.component.icon :refer [c-icon]] - [ethlance.ui.subscriptions :as ethlance-subs] - [ethlance.ui.util.navigation :as util.navigation] [district.ui.graphql.subs :as gql] - [re-frame.core :as re :refer [subscribe]] - )) + [district.ui.router.subs :as ui.router.subs] + [district.ui.web3-accounts.subs :as accounts-subs] + [ethlance.ui.component.icon :refer [c-icon]] + [ethlance.ui.subscriptions :as ethlance-subs] + [ethlance.ui.util.navigation :as util.navigation] + [re-frame.core :as re])) + (defn- c-menu-item "Menu Item used within the navigation menu." @@ -28,8 +28,7 @@ "Main Navigation Menu seen while the ethlance website is in desktop-mode." [] (fn [] - (let [ - active-account @(re/subscribe [::accounts-subs/active-account]) + (let [active-account @(re/subscribe [::accounts-subs/active-account]) active-session @(re/subscribe [::ethlance-subs/active-session]) active-user-id (or (:user/id active-session) active-account) query [:user {:user/id active-user-id} @@ -37,7 +36,7 @@ :user/name :user/profile-image]] result (re/subscribe [::gql/query {:queries [query]}]) - active-user (get-in @result [:user])] + active-user (get @result :user)] [:div.main-navigation-menu [c-menu-item {:name :new-job :label "New Job" :route :route.job/new}] [c-menu-item {:name :jobs :label "Jobs" :route :route.job/jobs}] @@ -45,6 +44,6 @@ [c-menu-item {:name :arbiters :label "Arbiters" :route :route.user/arbiters}] [c-menu-item {:name :about :label "About" :route :route.misc/about}] (when (and active-account (not active-user)) - [c-menu-item {:name :sign-up :label "Sign Up" :route :route.me/sign-up}]) + [c-menu-item {:name :sign-up :label "Sign Up" :route :route.me/sign-up}]) (when active-user - [c-menu-item {:name :my-activity :label "My Activity" :route :route.me/index}])]))) + [c-menu-item {:name :my-activity :label "My Activity" :route :route.me/index}])]))) diff --git a/ui/src/ethlance/ui/component/mobile_navigation_bar.cljs b/ui/src/ethlance/ui/component/mobile_navigation_bar.cljs index f809bd39..c828a2a6 100644 --- a/ui/src/ethlance/ui/component/mobile_navigation_bar.cljs +++ b/ui/src/ethlance/ui/component/mobile_navigation_bar.cljs @@ -1,16 +1,14 @@ (ns ethlance.ui.component.mobile-navigation-bar (:require - [reagent.core :as r] - [re-frame.core :as re] - [district.ui.router.subs :as ui.router.subs] - - ;; Ethlance Components - [ethlance.ui.component.ethlance-logo :refer [c-ethlance-logo]] - [ethlance.ui.component.profile-image :refer [c-profile-image]] - [ethlance.ui.component.icon :refer [c-icon]] - - ;; Ethlance Utils - [ethlance.ui.util.navigation :as util.navigation])) + [district.ui.router.subs :as ui.router.subs] + ;; Ethlance Components + [ethlance.ui.component.ethlance-logo :refer [c-ethlance-logo]] + [ethlance.ui.component.icon :refer [c-icon]] + [ethlance.ui.component.profile-image :refer [c-profile-image]] + ;; Ethlance Utils + [ethlance.ui.util.navigation :as util.navigation] + [re-frame.core :as re] + [reagent.core :as r])) (defn- c-menu-item @@ -27,7 +25,8 @@ [:span.label label]]))) -(defn c-mobile-navigation-menu [] +(defn c-mobile-navigation-menu + [] [:div.mobile-navigation-menu [c-menu-item {:name :new-job :label "New Job" :route :route.job/new}] [c-menu-item {:name :jobs :label "Jobs" :route :route.job/jobs}] @@ -38,7 +37,8 @@ [c-menu-item {:name :my-activity :label "My Activity" :route :route.me/index}]]) -(defn c-mobile-account-page [] +(defn c-mobile-account-page + [] [:div.mobile-account-page [:div.account-profile [c-profile-image {}] @@ -48,7 +48,8 @@ [:span.usd-value "$1,337.00"]]]) -(defn c-mobile-navigation-bar [] +(defn c-mobile-navigation-bar + [] (let [*open? (r/atom false)] (fn [] [:div.mobile-navigation-bar diff --git a/ui/src/ethlance/ui/component/mobile_search_filter.cljs b/ui/src/ethlance/ui/component/mobile_search_filter.cljs index dfa54ff7..2065d121 100644 --- a/ui/src/ethlance/ui/component/mobile_search_filter.cljs +++ b/ui/src/ethlance/ui/component/mobile_search_filter.cljs @@ -1,15 +1,14 @@ (ns ethlance.ui.component.mobile-search-filter (:require - [reagent.core :as r] - - ;; Ethlance Components - [ethlance.ui.component.icon :refer [c-icon]])) + ;; Ethlance Components + [ethlance.ui.component.icon :refer [c-icon]] + [reagent.core :as r])) (defn c-mobile-search-filter [& children] (let [*open? (r/atom false)] - (fn [] + (fn [] [:div.mobile-search-filter {:class (when @*open? "open")} [:div.filter-button diff --git a/ui/src/ethlance/ui/component/mobile_sidebar.cljs b/ui/src/ethlance/ui/component/mobile_sidebar.cljs index bfe865f8..ec009145 100644 --- a/ui/src/ethlance/ui/component/mobile_sidebar.cljs +++ b/ui/src/ethlance/ui/component/mobile_sidebar.cljs @@ -1,16 +1,14 @@ (ns ethlance.ui.component.mobile-sidebar - "" (:require - [reagent.core :as r] - - ;; Ethlance Components - [ethlance.ui.component.icon :refer [c-icon]])) + ;; Ethlance Components + [ethlance.ui.component.icon :refer [c-icon]] + [reagent.core :as r])) (defn c-mobile-sidebar [& children] (let [*open? (r/atom false)] - (fn [] + (fn [] [:div.mobile-sidebar {:class (when @*open? "open")} [:div.nav-button diff --git a/ui/src/ethlance/ui/component/modal.cljs b/ui/src/ethlance/ui/component/modal.cljs index 5c74b4ba..8932e967 100644 --- a/ui/src/ethlance/ui/component/modal.cljs +++ b/ui/src/ethlance/ui/component/modal.cljs @@ -1,5 +1,6 @@ (ns ethlance.ui.component.modal) + (defn c-modal [{:keys [] :as opts} & children] (into [:div.ethlance-modal opts] children)) diff --git a/ui/src/ethlance/ui/component/modal/events.cljs b/ui/src/ethlance/ui/component/modal/events.cljs index 0a695319..e4adc92f 100644 --- a/ui/src/ethlance/ui/component/modal/events.cljs +++ b/ui/src/ethlance/ui/component/modal/events.cljs @@ -1,6 +1,6 @@ (ns ethlance.ui.component.modal.events (:require - [re-frame.core :as re])) + [re-frame.core :as re])) (defn open-modal! diff --git a/ui/src/ethlance/ui/component/modal/subscriptions.cljs b/ui/src/ethlance/ui/component/modal/subscriptions.cljs index 70d7263c..ba982601 100644 --- a/ui/src/ethlance/ui/component/modal/subscriptions.cljs +++ b/ui/src/ethlance/ui/component/modal/subscriptions.cljs @@ -1,11 +1,10 @@ (ns ethlance.ui.component.modal.subscriptions (:require - [re-frame.core :as re])) + [re-frame.core :as re])) (re/reg-sub - :modal/open? - (fn [db [_ modal-id]] - (let [active-modal-id (:ethlance.ui.component.modal/active-modal-id db)] - (= active-modal-id (or modal-id :default))))) - + :modal/open? + (fn [db [_ modal-id]] + (let [active-modal-id (:ethlance.ui.component.modal/active-modal-id db)] + (= active-modal-id (or modal-id :default))))) diff --git a/ui/src/ethlance/ui/component/pagination.cljs b/ui/src/ethlance/ui/component/pagination.cljs index 230876ea..01c24475 100644 --- a/ui/src/ethlance/ui/component/pagination.cljs +++ b/ui/src/ethlance/ui/component/pagination.cljs @@ -1,11 +1,14 @@ (ns ethlance.ui.component.pagination - (:require [ethlance.ui.component.icon :refer [c-icon]] - [ethlance.ui.component.circle-button :refer [c-circle-icon-button]] - [re-frame.core :as re])) + (:require + [ethlance.ui.component.circle-button :refer [c-circle-icon-button]] + [ethlance.ui.component.icon :refer [c-icon]] + [re-frame.core :as re])) + ;; Math Functions (def ceil (aget js/Math "ceil")) + (defn c-pagination "Component for handling pagination wrt a given listing. @@ -51,6 +54,7 @@ :title "Go To Next Page" :on-click #(re/dispatch [set-offset-event next-offset])}]]))) + (defn c-pagination-ends "Component for handling pagination with @@ -70,7 +74,6 @@ offset set-offset-event]}] (let [total-count (or total-count 0) - current-page (-> offset (/ limit) ceil inc) num-pages (-> total-count (/ limit) ceil) prev-offset (- offset limit) prev-offset (if (< prev-offset 0) 0 prev-offset) diff --git a/ui/src/ethlance/ui/component/profile_image.cljs b/ui/src/ethlance/ui/component/profile_image.cljs index 1f61d82c..c82f7164 100644 --- a/ui/src/ethlance/ui/component/profile_image.cljs +++ b/ui/src/ethlance/ui/component/profile_image.cljs @@ -2,8 +2,10 @@ (:require [ethlance.ui.util.urls :as util.urls])) + (def placeholder-image-url "/images/avatar-placeholder.png") + (defn c-profile-image "Profile Image component for displaying a given user's profile image. @@ -20,10 +22,10 @@ " [{:keys [src size]}] (let [size-class (case size - :small " small " - :normal "" - :large " large " - "") + :small " small " + :normal "" + :large " large " + "") src-url (util.urls/ipfs-hash->gateway-url src)] [:div.ethlance-profile-image {:class size-class} diff --git a/ui/src/ethlance/ui/component/radio_select.cljs b/ui/src/ethlance/ui/component/radio_select.cljs index 179fd97d..1b9fa15d 100644 --- a/ui/src/ethlance/ui/component/radio_select.cljs +++ b/ui/src/ethlance/ui/component/radio_select.cljs @@ -1,14 +1,16 @@ (ns ethlance.ui.component.radio-select - (:require [clojure.core.async - :as - async - :refer - [ [c-inline-svg {:src "/images/svg/radio-button.svg" @@ -96,7 +101,8 @@ [:span.label label]]) -(defn c-radio-secondary-element [label] +(defn c-radio-secondary-element + [label] [:div.radio-secondary-element [c-inline-svg {:src "/images/svg/radio-button.svg" diff --git a/ui/src/ethlance/ui/component/rating.cljs b/ui/src/ethlance/ui/component/rating.cljs index dd9731ae..294b7b35 100644 --- a/ui/src/ethlance/ui/component/rating.cljs +++ b/ui/src/ethlance/ui/component/rating.cljs @@ -1,11 +1,15 @@ (ns ethlance.ui.component.rating - (:require [reagent.core :as r])) + (:require + [reagent.core :as r])) + (def rating-star-src-primary "/images/icons/ethlance-star-icon-primary.svg") (def rating-star-src-black "/images/icons/ethlance-star-icon-black.svg") (def rating-star-src-white "/images/icons/ethlance-star-icon-white.svg") -(defn c-star [] + +(defn c-star + [] (fn [{:keys [active? color index on-change size] :or {color :primary size :default}}] (let [color-src (case color @@ -31,6 +35,7 @@ :height (str size-value "px")} :class [active-class size-class]}]))) + (defn c-rating "Rating Component, for displaying feedback within ethlance. diff --git a/ui/src/ethlance/ui/component/scrollable.cljs b/ui/src/ethlance/ui/component/scrollable.cljs index 9005fc59..54af06bb 100644 --- a/ui/src/ethlance/ui/component/scrollable.cljs +++ b/ui/src/ethlance/ui/component/scrollable.cljs @@ -1,11 +1,15 @@ (ns ethlance.ui.component.scrollable (:require - [reagent.core :as r] - ["react" :as react] - ["simplebar" :as simplebar])) + ["react" :as react] + ["simplebar" :as simplebar] + [reagent.core :as r])) + + +(defn- c-scrollable-noop + [_opts body] + body) -(defn- c-scrollable-noop [_opts body] body) (defn- c-scrollable-real "Scrollable container. Uses simplebar-react @@ -21,21 +25,22 @@ (let [*instance (r/atom nil) react-ref (react/createRef)] (r/create-class - {:display-name "c-scrollable" + {:display-name "c-scrollable" + + :component-did-mount + (fn [_this] + (let [elnode (.-current react-ref) + simplebar (simplebar elnode (clj->js opts))] + (reset! *instance simplebar))) - :component-did-mount - (fn [this] - (let [elnode (.-current react-ref) - simplebar (simplebar elnode (clj->js opts))] - (reset! *instance simplebar))) + :component-will-unmount + (fn [] + (.unMount @*instance)) - :component-will-unmount - (fn [] - (.unMount @*instance)) + :reagent-render + (fn [_ child] + [:div.scrollable {:ref react-ref} child])}))) - :reagent-render - (fn [_ child] - [:div.scrollable {:ref react-ref} child])}))) (defn c-scrollable [opts val] diff --git a/ui/src/ethlance/ui/component/search_input.cljs b/ui/src/ethlance/ui/component/search_input.cljs index 4352024a..0a154e88 100644 --- a/ui/src/ethlance/ui/component/search_input.cljs +++ b/ui/src/ethlance/ui/component/search_input.cljs @@ -1,18 +1,20 @@ (ns ethlance.ui.component.search-input - (:require [cuerdas.core :as string] - [ethlance.ui.component.icon :refer [c-icon]] - [reagent.core :as r] - ["react" :as react])) + (:require + ["react" :as react] + [com.wsscode.fuzzy :as fz] + [cuerdas.core :as string] + [ethlance.ui.component.icon :refer [c-icon]] + [reagent.core :as r])) + (def blur-delay-ms 200) + (defn filter-selections [search-text selections label-fn] - (if (and (seq search-text) (seq selections)) - (->> selections - (filter #(string/includes? (string/lower (label-fn %)) (string/lower search-text))) - vec) - nil)) + (let [fuzzy-options (map (fn [sel] {::fz/string (label-fn sel)}) selections)] + (when (and (seq search-text) (seq selections)) + (map ::fz/string (fz/fuzzy-match {::fz/search-input search-text ::fz/options fuzzy-options}))))) (defn next-element "Get the next element in `xs` after element `v`." @@ -23,6 +25,7 @@ (>= (inc index) (count xs)) (first xs) :else (get xs (inc index))))) + (defn previous-element "Get the previous element in `xs` before element `v`." [xs v] @@ -32,7 +35,9 @@ (= index 0) (last xs) :else (get xs (dec index))))) -(defn c-chip [{:keys [on-close]} label] + +(defn c-chip + [{:keys [on-close]} label] [:div.ethlance-chip {:title label} [:span.label label] @@ -41,6 +46,7 @@ :title (str "Remove '" label "'")} [c-icon {:name :close :size :x-small :color :black :inline? false}]]]) + (defn c-chip-search-input "A standalone component for handling chip search inputs. @@ -83,7 +89,7 @@ (r/create-class {:display-name "ethlance-chip-search-input" :component-did-mount - (fn [this] + (fn [_this] (let [root-dom (.-current react-ref) search-input (.querySelector root-dom ".search-input")] (.addEventListener @@ -142,7 +148,7 @@ (for [chip current-chip-listing] ^{:key (str "chip-" (value-fn chip))} [c-chip - {:on-close #(-update-chip-listing (disj current-chip-listing chip))} + {:on-close #(-update-chip-listing (disj (into #{} current-chip-listing) chip))} (label-fn chip)])) [:input.search-input {:type "text" diff --git a/ui/src/ethlance/ui/component/select_input.cljs b/ui/src/ethlance/ui/component/select_input.cljs index 72898134..2b8ee939 100644 --- a/ui/src/ethlance/ui/component/select_input.cljs +++ b/ui/src/ethlance/ui/component/select_input.cljs @@ -1,7 +1,9 @@ (ns ethlance.ui.component.select-input - (:require [cuerdas.core :as string] - [ethlance.ui.component.icon :refer [c-icon]] - [reagent.core :as r])) + (:require + [cuerdas.core :as string] + [ethlance.ui.component.icon :refer [c-icon]] + [reagent.core :as r])) + (defn filter-selections [search-text selections label-fn] @@ -10,6 +12,7 @@ (filter #(string/includes? (string/lower (label-fn %)) (string/lower search-text)))) selections)) + (defn c-select-input "Select Input Component for a dropdown listing of selections. Can also include a search box for easier navigation. @@ -105,13 +108,13 @@ :inline? false}]]) [:div.selection-listing (doall - (for [selection (filter-selections @*search-text selections label-fn)] - ^{:key (str "selection-" (value-fn selection))} - [:div.selection - {:on-click - (fn [] - (reset! *current-default-selection selection) - (reset! *search-text "") - (reset! *open? false) - (when on-select (on-select selection)))} - (label-fn selection)]))]])])))) + (for [selection (filter-selections @*search-text selections label-fn)] + ^{:key (str "selection-" (value-fn selection))} + [:div.selection + {:on-click + (fn [] + (reset! *current-default-selection selection) + (reset! *search-text "") + (reset! *open? false) + (when on-select (on-select selection)))} + (label-fn selection)]))]])])))) diff --git a/ui/src/ethlance/ui/component/sign_in_dialog.cljs b/ui/src/ethlance/ui/component/sign_in_dialog.cljs index f778b5ca..53400d33 100644 --- a/ui/src/ethlance/ui/component/sign_in_dialog.cljs +++ b/ui/src/ethlance/ui/component/sign_in_dialog.cljs @@ -1,27 +1,28 @@ (ns ethlance.ui.component.sign-in-dialog "Sign In Dialog, which makes use of modal component." (:require - [re-frame.core :as re] + [ethlance.ui.component.button :refer [c-button c-button-label]] + ;; Ethlance Components + [ethlance.ui.component.icon :refer [c-icon]] + [ethlance.ui.component.modal :refer [c-modal]] + ;; re-frame Prerequisites + [ethlance.ui.component.modal.subscriptions] + [ethlance.ui.events] + [re-frame.core :as re])) - ;; re-frame Prerequisites - [ethlance.ui.component.modal.subscriptions] - [ethlance.ui.events] - ;; Ethlance Components - [ethlance.ui.component.icon :refer [c-icon]] - [ethlance.ui.component.button :refer [c-button c-button-label]] - [ethlance.ui.component.modal :refer [c-modal]])) - - -(defn open! [] +(defn open! + [] (re/dispatch [:modal/open ::sign-in])) -(defn close! [] +(defn close! + [] (re/dispatch [:modal/close])) -(defn sign-in! [] +(defn sign-in! + [] (re/dispatch [:user/sign-in]) (close!)) @@ -40,8 +41,8 @@ :class "close-button" :color :secondary :title "Close Dialog"}] - ; FIXME: Get actual image. The current one is placeholder and displaces the design - ; [:img.sign-in-dialog {:src "/images/svg/sign_in_dialog.svg"}] + ;; FIXME: Get actual image. The current one is placeholder and displaces the design + ;; [:img.sign-in-dialog {:src "/images/svg/sign_in_dialog.svg"}] [:h1 "Sign In and Verify Address"] [:p "After clicking \"Continue\", a wallet dialogue will prompt you to verify your unique address."] [:p "Once you verify, you will be signed in to the network."] @@ -50,4 +51,3 @@ {:color :primary :on-click sign-in!} [c-button-label [:span "Continue"]]]]]])))) - diff --git a/ui/src/ethlance/ui/component/splash_layout.cljs b/ui/src/ethlance/ui/component/splash_layout.cljs index b0268742..14b32601 100644 --- a/ui/src/ethlance/ui/component/splash_layout.cljs +++ b/ui/src/ethlance/ui/component/splash_layout.cljs @@ -1,22 +1,25 @@ (ns ethlance.ui.component.splash-layout (:require + ["react-transition-group" :refer [CSSTransition TransitionGroup]] [ethlance.ui.component.button :refer [c-button c-button-label]] [ethlance.ui.component.circle-button :refer [c-circle-icon-button]] [ethlance.ui.component.ethlance-logo :refer [c-ethlance-logo]] [ethlance.ui.component.icon :refer [c-icon]] + [ethlance.ui.component.sign-in-dialog :refer [c-sign-in-dialog]] [ethlance.ui.component.splash-mobile-navigation-bar :refer [c-splash-mobile-navigation-bar]] [ethlance.ui.component.splash-navigation-bar :refer [c-splash-navigation-bar]] [ethlance.ui.util.navigation :as util.navigation] - [reagent.core :as r] - ["react-transition-group" :refer [CSSTransition TransitionGroup]] - [ethlance.ui.component.sign-in-dialog :refer [c-sign-in-dialog]])) + [reagent.core :as r])) -(defn c-how-to-card [label src] + +(defn c-how-to-card + [label src] [:div.how-to-card [:div.image [:img {:src src}]] [:span.label label]]) + (defn c-how-to-candidate [] [:div.how-to-candidate @@ -27,6 +30,7 @@ [c-how-to-card "Receive Ether" "/images/icon-free-ether.png"] [c-how-to-card "Leave Feedback" "/images/icon-feedback.png"]]) + (defn c-how-to-employer [] [:div.how-to-employer @@ -37,6 +41,7 @@ [c-how-to-card "Pay Invoices In Ether" "/images/icon-ether.png"] [c-how-to-card "Leave Feedback" "/images/icon-feedback.png"]]) + (defn c-how-to-arbiter [] [:div.how-to-arbiter @@ -47,6 +52,7 @@ [c-how-to-card "Receive Ether" "/images/icon-free-ether.png"] [c-how-to-card "Leave Feedback" "/images/icon-feedback.png"]]) + (defn c-how-it-works-layout [] (let [*current-selection (r/atom :candidate)] @@ -92,6 +98,7 @@ :timeout 200} (r/as-element [c-how-to-arbiter])])]]))) + (defn c-splash-layout [] [:div.splash-layout.animation-fade-in @@ -308,5 +315,4 @@ [:div.footer-section [:span "Copyright © 2020 Ethlance.com. All rights reserved."]] [:div.modals - [c-sign-in-dialog]] - ]]]) + [c-sign-in-dialog]]]]]) diff --git a/ui/src/ethlance/ui/component/splash_mobile_navigation_bar.cljs b/ui/src/ethlance/ui/component/splash_mobile_navigation_bar.cljs index 665b0a8e..d8c34df1 100644 --- a/ui/src/ethlance/ui/component/splash_mobile_navigation_bar.cljs +++ b/ui/src/ethlance/ui/component/splash_mobile_navigation_bar.cljs @@ -1,17 +1,22 @@ (ns ethlance.ui.component.splash-mobile-navigation-bar - (:require [ethlance.ui.component.ethlance-logo :refer [c-ethlance-logo]] - [ethlance.ui.component.icon :refer [c-icon]] - [ethlance.ui.util.navigation :as util.navigation] - [reagent.core :as r])) + (:require + [ethlance.ui.component.ethlance-logo :refer [c-ethlance-logo]] + [ethlance.ui.component.icon :refer [c-icon]] + [ethlance.ui.util.navigation :as util.navigation] + [reagent.core :as r])) -(defn c-nav-link [{:keys [name route]}] + +(defn c-nav-link + [{:keys [name route]}] [:a.nav-link {:title name :on-click (util.navigation/create-handler {:route route}) :href (util.navigation/resolve-route {:route route})} name]) -(defn c-splash-mobile-navigation-bar [] + +(defn c-splash-mobile-navigation-bar + [] (let [*open? (r/atom false)] (fn [] [:div.splash-mobile-navigation-bar diff --git a/ui/src/ethlance/ui/component/splash_navigation_bar.cljs b/ui/src/ethlance/ui/component/splash_navigation_bar.cljs index e5084a41..d77f5a81 100644 --- a/ui/src/ethlance/ui/component/splash_navigation_bar.cljs +++ b/ui/src/ethlance/ui/component/splash_navigation_bar.cljs @@ -1,7 +1,9 @@ (ns ethlance.ui.component.splash-navigation-bar - (:require [ethlance.ui.component.ethlance-logo :refer [c-ethlance-logo]] - [ethlance.ui.util.navigation :as util.navigation] - [reagent.core :as r])) + (:require + [ethlance.ui.component.ethlance-logo :refer [c-ethlance-logo]] + [ethlance.ui.util.navigation :as util.navigation] + [reagent.core :as r])) + (defn c-splash-navigation-link [{:keys [name route href *hover]}] @@ -18,7 +20,9 @@ navigation) name]])) -(defn c-splash-navigation-bar [] + +(defn c-splash-navigation-bar + [] (let [*hover (r/atom nil)] (fn [] [:div.splash-navigation-bar diff --git a/ui/src/ethlance/ui/component/table.cljs b/ui/src/ethlance/ui/component/table.cljs index e6df43b8..7e170f81 100644 --- a/ui/src/ethlance/ui/component/table.cljs +++ b/ui/src/ethlance/ui/component/table.cljs @@ -1,5 +1,6 @@ (ns ethlance.ui.component.table) + (defn c-table "Ethlance Table Component. @@ -46,23 +47,21 @@ [:tbody [:tr (doall - (for [[i header] (map-indexed vector headers)] - ^{:key (str "header-" i)} - [:th header]))] + (for [[i header] (map-indexed vector headers)] + ^{:key (str "header-" i)} + [:th header]))] (doall - (for [[i row] (map-indexed vector rows)] - (if (map? row) + (for [[i row] (map-indexed vector rows)] + (if (map? row) - ^{:key (str "row-" i)} - [:tr.clickable (:row-link row) - (for [[i elem] (map-indexed vector (:row-cells row))] - ^{:key (str "elem-" i)} - [:td elem])] + ^{:key (str "row-" i)} + [:tr.clickable (:row-link row) + (for [[i elem] (map-indexed vector (:row-cells row))] + ^{:key (str "elem-" i)} + [:td elem])] - ^{:key (str "row-" i)} - [:tr - (for [[i elem] (map-indexed vector row)] - ^{:key (str "elem-" i)} - [:td elem])] - ) - ))]]]) + ^{:key (str "row-" i)} + [:tr + (for [[i elem] (map-indexed vector row)] + ^{:key (str "elem-" i)} + [:td elem])])))]]]) diff --git a/ui/src/ethlance/ui/component/tabular_layout.cljs b/ui/src/ethlance/ui/component/tabular_layout.cljs index 78b1ad54..bc22fadf 100644 --- a/ui/src/ethlance/ui/component/tabular_layout.cljs +++ b/ui/src/ethlance/ui/component/tabular_layout.cljs @@ -1,6 +1,8 @@ (ns ethlance.ui.component.tabular-layout - (:require [ethlance.ui.component.select-input :refer [c-select-input]] - [reagent.core :as r])) + (:require + [ethlance.ui.component.select-input :refer [c-select-input]] + [reagent.core :as r])) + (defn c-tabular-layout "Tabular Layout used within several pages on Ethlance. @@ -58,15 +60,15 @@ [:div.tab-listing {:class tab-count-class} (doall - (for [{:keys [index label on-click]} tab-options] - ^{:key (str "tab-" index)} - [:div.tab - {:class (when (= @*active-tab-index index) "active") - :on-click (fn [event] - (reset! *active-tab-index index) - (when on-click - (on-click event)))} - [:span.label label]]))] + (for [{:keys [index label on-click]} tab-options] + ^{:key (str "tab-" index)} + [:div.tab + {:class (when (= @*active-tab-index index) "active") + :on-click (fn [event] + (reset! *active-tab-index index) + (when on-click + (on-click event)))} + [:span.label label]]))] [:div.mobile-tab-listing [c-select-input diff --git a/ui/src/ethlance/ui/component/text_input.cljs b/ui/src/ethlance/ui/component/text_input.cljs index cc1f8c93..5819f5c5 100644 --- a/ui/src/ethlance/ui/component/text_input.cljs +++ b/ui/src/ethlance/ui/component/text_input.cljs @@ -1,6 +1,7 @@ (ns ethlance.ui.component.text-input (:require - [reagent.core :as r])) + [reagent.core :as r])) + (defn c-text-input "Default Text Input Component diff --git a/ui/src/ethlance/ui/component/textarea_input.cljs b/ui/src/ethlance/ui/component/textarea_input.cljs index 05c455c8..a62362b5 100644 --- a/ui/src/ethlance/ui/component/textarea_input.cljs +++ b/ui/src/ethlance/ui/component/textarea_input.cljs @@ -1,6 +1,7 @@ (ns ethlance.ui.component.textarea-input (:require - [reagent.core :as r])) + [reagent.core :as r])) + (defn c-textarea-input "Default TextArea Input Component @@ -22,10 +23,10 @@ opts (dissoc opts :default-value :value :color :on-change)] [:textarea.ethlance-textarea-input (merge - opts - {:class [class-color] - :value current-value - :on-change (fn [e] - (let [target-value (-> e .-target .-value)] - (reset! *current-value target-value) - (when on-change (on-change target-value))))})])))) + opts + {:class [class-color] + :value current-value + :on-change (fn [e] + (let [target-value (-> e .-target .-value)] + (reset! *current-value target-value) + (when on-change (on-change target-value))))})])))) diff --git a/ui/src/ethlance/ui/component/token_amount_input.cljs b/ui/src/ethlance/ui/component/token_amount_input.cljs index 68954d76..449ac229 100644 --- a/ui/src/ethlance/ui/component/token_amount_input.cljs +++ b/ui/src/ethlance/ui/component/token_amount_input.cljs @@ -1,17 +1,18 @@ (ns ethlance.ui.component.token-amount-input (:require - [reagent.core :as r] - [ethlance.ui.component.text-input :as text-input] - [ethlance.ui.util.tokens :as util-tokens])) + [ethlance.ui.component.text-input :as text-input] + [ethlance.ui.util.tokens :as util-tokens] + [cljs.math])) + (defn c-token-amount-input [{:keys [decimals on-change] :as opts}] (let [text-input-opts (dissoc opts :decimals) - ; Even though tokens (including ETH) can have 18 decimals, using so many in the UI isn't practical + ;; Even though tokens (including ETH) can have 18 decimals, using so many in the UI isn't practical max-ui-decimals 3 decimals-for-ui (min decimals max-ui-decimals) step (/ 1 (cljs.math/pow 10 decimals-for-ui)) - ; Should use method similar to https://stackoverflow.com/a/10880710/1025412 + ;; Should use method similar to https://stackoverflow.com/a/10880710/1025412 human->token-amount (fn [human-amount] (.round js/Math (* (cljs.math/pow 10 decimals) human-amount))) token-on-change (fn [human-amount] (on-change {:token-amount (human->token-amount human-amount) diff --git a/ui/src/ethlance/ui/component/token_info.cljs b/ui/src/ethlance/ui/component/token_info.cljs index 03fddaf9..a449a04c 100644 --- a/ui/src/ethlance/ui/component/token_info.cljs +++ b/ui/src/ethlance/ui/component/token_info.cljs @@ -1,11 +1,14 @@ (ns ethlance.ui.component.token-info "Component to show token info (ETH, ERC20/721/1155) with USD value and link to contract" (:require - [ethlance.ui.util.tokens :as util.tokens] [district.ui.conversion-rates.subs :as rates-subs] - [re-frame.core :as re])) + [ethlance.ui.util.tokens :as util.tokens] + [re-frame.core :as re] + [clojure.string])) -(defn token-info-str [token-amount token-detail] + +(defn token-info-str + [token-amount token-detail] (let [token-type (:token-detail/type token-detail) token-name (:token-detail/name token-detail) decimals (:token-detail/decimals token-detail)] @@ -15,17 +18,19 @@ token-name (clojure.string/upper-case (name (or token-type :?))))))) -; Examples: -; 0.02 ETH ($30, 0x000...) -; 10 TEST (ERC20, 0x76BE3...) -; 1 (ERC721, 0x3xZ...) -; 2 (ERC1155, 0x76BE3...) linking to https://etherscan.io/token/0x76be3b62873462d2142405439777e971754e8e77 -(defn c-token-info [token-amount token-detail & {:keys [show-address?] :or {show-address? true}}] + +;; Examples: +;; 0.02 ETH ($30, 0x000...) +;; 10 TEST (ERC20, 0x76BE3...) +;; 1 (ERC721, 0x3xZ...) +;; 2 (ERC1155, 0x76BE3...) linking to https://etherscan.io/token/0x76be3b62873462d2142405439777e971754e8e77 +(defn c-token-info + [token-amount token-detail & {:keys [show-address?] :or {show-address? true}}] (let [token-type (:token-detail/type token-detail) token-decimals (:token-detail/decimals token-detail) display-amount (util.tokens/human-amount token-amount token-type token-decimals) token-symbol (:token-detail/symbol token-detail) - dollar-amount (if (= token-type :eth) + dollar-amount (when (= token-type :eth) (util.tokens/round 2 (* display-amount @(re/subscribe [::rates-subs/conversion-rate :ETH :USD])))) display-type (if (= token-type :eth) (str "$" dollar-amount) diff --git a/ui/src/ethlance/ui/config.cljs b/ui/src/ethlance/ui/config.cljs index 9aeecac1..74abf2e8 100644 --- a/ui/src/ethlance/ui/config.cljs +++ b/ui/src/ethlance/ui/config.cljs @@ -1,38 +1,42 @@ (ns ethlance.ui.config - (:require [district.ui.component.router :as router] - [ethlance.shared.smart-contracts-dev :as smart-contracts-dev] - [ethlance.shared.smart-contracts-prod :as smart-contracts-prod] - [ethlance.shared.smart-contracts-qa :as smart-contracts-qa] - [ethlance.shared.graphql.schema :refer [schema]] - [ethlance.shared.config :as shared-config] - [district.graphql-utils] - [taoensso.timbre :refer [merge-config!] :as log] - [ethlance.shared.utils :include-macros true :refer [slurp] :as shared-utils] - [ethlance.shared.routes :as routes])) - -; gql-name->kw transforms "erc20" to :erc-20 -; This is a small wrapper that maintains these, e.g. -; "erc1155" -> :erc1155 -; Source: -; - https://github.com/district0x/district-graphql-utils/blob/e814beb9222c9d029a78a39b1c78f6644f0aa4c6/src/district/graphql_utils.cljs#L43 -; - https://github.com/district0x/district-graphql-utils/blob/e814beb9222c9d029a78a39b1c78f6644f0aa4c6/src/district/graphql_utils.cljs#L77 -; - https://github.com/clj-commons/camel-snake-kebab/blob/ac08444c94aca4cba25d86f3b3faf36596809380/src/camel_snake_kebab/internals/string_separator.cljc#L42 -(defn token-type-fixed-gql-name->kw [s] + (:require + [district.graphql-utils] + [district.ui.component.router :as router] + [ethlance.shared.graphql.schema :refer [schema]] + [ethlance.shared.routes :as routes] + [ethlance.shared.smart-contracts-dev :as smart-contracts-dev] + [ethlance.shared.smart-contracts-prod :as smart-contracts-prod] + [ethlance.shared.smart-contracts-qa :as smart-contracts-qa] + [ethlance.shared.utils :include-macros true :as shared-utils])) + + +;; gql-name->kw transforms "erc20" to :erc-20 +;; This is a small wrapper that maintains these, e.g. +;; "erc1155" -> :erc1155 +;; Source: +;; - https://github.com/district0x/district-graphql-utils/blob/e814beb9222c9d029a78a39b1c78f6644f0aa4c6/src/district/graphql_utils.cljs#L43 +;; - https://github.com/district0x/district-graphql-utils/blob/e814beb9222c9d029a78a39b1c78f6644f0aa4c6/src/district/graphql_utils.cljs#L77 +;; - https://github.com/clj-commons/camel-snake-kebab/blob/ac08444c94aca4cba25d86f3b3faf36596809380/src/camel_snake_kebab/internals/string_separator.cljc#L42 +(defn token-type-fixed-gql-name->kw + [s] (let [fixed-names #{"erc20" "erc721" "erc1155"}] (if (contains? fixed-names s) (keyword s) (district.graphql-utils/gql-name->kw s)))) + (def environment (shared-utils/get-environment)) + (def contracts-var (condp = environment "prod" smart-contracts-prod/smart-contracts "qa" smart-contracts-qa/smart-contracts "dev" smart-contracts-dev/smart-contracts)) + (def default-config - ; config of https://github.com/district0x/district-ui-smart-contracts + ;; config of https://github.com/district0x/district-ui-smart-contracts {:logging {:level :info :console? true} @@ -68,27 +72,17 @@ (def config-dev {:logging {:level :debug} :web3 {:url "http://d0x-vm:8549"} ; "https://mainnet.infura.io/" - :server-config {:url "http://d0x-vm:6300/config" :format :json} - - ; :ipfs - ; {:endpoint "/api/v0" - ; :host "http://host-machine:5001" - ; :gateway "http://ipfs.localhost:8080/ipfs"} - ; :ipfs - ; {:host "https://ipfs.infura.io:5001" - ; :endpoint "/api/v0" - ; :gateway "https://ethlance-qa.infura-ipfs.io/ipfs" - ; :auth {:username "2DWc3aSeqUSU5fMM64UuGvpMuPS" - ; :password "c0fed9d891d419e72f41ee451b1055ec"}} - } - ) + :server-config {:url "http://d0x-vm:6300/config" :format :json}}) + (def config-qa {:server-config {:url "https://ethlance-api.qa.district0x.io/config"}}) + (def config-prod {:server-config {:url "http://api.ethlance.com"}}) + (defn get-config ([] (get-config environment)) ([env] diff --git a/ui/src/ethlance/ui/core.cljs b/ui/src/ethlance/ui/core.cljs index 03da57d0..f2ef9946 100644 --- a/ui/src/ethlance/ui/core.cljs +++ b/ui/src/ethlance/ui/core.cljs @@ -1,20 +1,25 @@ (ns ethlance.ui.core (:require [akiroz.re-frame.storage :refer [reg-co-fx!]] - [district.ui.notification] + [cljsjs.apollo-fetch] + [cljsjs.dataloader] [district.ui.component.router] [district.ui.conversion-rates] + [district.ui.graphql] [district.ui.ipfs] [district.ui.logging] + [district.ui.notification] [district.ui.reagent-render] [district.ui.router] - [district.ui.web3-account-balances] - [district.ui.web3-accounts] [district.ui.server-config] + [district.ui.smart-contracts] [district.ui.web3] + [district.ui.web3-account-balances] + [district.ui.web3-accounts] + [district.ui.web3-tx] [district0x.re-frame.web3-fx] - [district.ui.smart-contracts] - [district.ui.web3-tx] ; to register effect :web3-tx-localstorage + [ethlance.shared.utils :as shared-utils] + ; to register effect :web3-tx-localstorage [ethlance.ui.config :as ui.config] [ethlance.ui.effects] [ethlance.ui.events] @@ -22,26 +27,24 @@ [ethlance.ui.subscriptions] [ethlance.ui.util.injection :as util.injection] [mount.core :as mount] - [cljsjs.apollo-fetch] - [cljsjs.dataloader] - [district.ui.graphql] [print.foo :include-macros true] - [ethlance.shared.utils :as shared-utils] [re-frame.core :as re])) + (enable-console-print!) (def environment (shared-utils/get-environment)) -(defn fetch-config-from-server [url callback] - (let [] - (-> (js/fetch url) - (.then ,,, (fn [response] - (.json response))) - (.then ,,, (fn [config] - (callback (js->clj config {:keywordize-keys true}))))))) -(defn ^:export init [] +(defn fetch-config-from-server + [url callback] + (-> (js/fetch url) + (.then ,,, (fn [response] (.json response))) + (.then ,,, (fn [config] (callback (js->clj config {:keywordize-keys true})))))) + + +(defn ^:export init + [] (let [main-config (ui.config/get-config environment)] (util.injection/inject-data-scroll! {:injection-selector "#app"}) diff --git a/ui/src/ethlance/ui/effects.cljs b/ui/src/ethlance/ui/effects.cljs index 4b3ede0a..d73e8c08 100644 --- a/ui/src/ethlance/ui/effects.cljs +++ b/ui/src/ethlance/ui/effects.cljs @@ -1,7 +1,8 @@ (ns ethlance.ui.effects (:require - [re-frame.core :as re] - [cljs-web3.core :as web3])) + [cljs-web3.core :as web3] + [re-frame.core :as re])) + ;; TODO : move this to maybe re-frame-web3-fx (re/reg-fx diff --git a/ui/src/ethlance/ui/event/sign_in.cljs b/ui/src/ethlance/ui/event/sign_in.cljs index 428b3004..67a9500a 100644 --- a/ui/src/ethlance/ui/event/sign_in.cljs +++ b/ui/src/ethlance/ui/event/sign_in.cljs @@ -1,11 +1,11 @@ (ns ethlance.ui.event.sign-in + (:refer-clojure :exclude [resolve]) (:require + [district.ui.graphql.events :as gql-events] [district.ui.logging.events :as logging.events] [district.ui.web3-accounts.queries :as account-queries] [district.ui.web3.queries :as web3-queries] - [district.ui.graphql.events :as gql-events] - [re-frame.core :as re]) - (:refer-clojure :exclude [resolve])) + [re-frame.core :as re])) (re/reg-event-fx @@ -17,9 +17,9 @@ ;; - This will attempt to 'sign' the `data-str` using the given active ;; account. If the signed message is valid, the active ethereum account ;; will be signed in by providing the session with a JWT Token. - (fn [{:keys [db]} _] - (let [active-account (account-queries/active-account db) - data-str " Sign in to Ethlance! "] + (fn [{:keys [db]} _] + (let [active-account (account-queries/active-account db) + data-str " Sign in to Ethlance! "] {:web3/personal-sign {:web3 (web3-queries/web3 db) :data-str data-str @@ -42,8 +42,9 @@ (fn [cofx [_ event-data]] (-> cofx (assoc-in ,,, [:db :active-session] (select-keys (:sign-in event-data) [:jwt :user/id])) - (assoc-in ,,, [:store] (select-keys (:sign-in event-data) [:jwt :user/id])) - (assoc-in ,,, [:fx] [[:dispatch [:district.ui.graphql.events/set-authorization-token (get-in event-data [:sign-in :jwt])]]])))) + (assoc ,,, :store (select-keys (:sign-in event-data) [:jwt :user/id])) + (assoc ,,, :fx [[:dispatch [:district.ui.graphql.events/set-authorization-token (get-in event-data [:sign-in :jwt])]]])))) + ;; Intermediates (re/reg-event-fx @@ -51,10 +52,11 @@ :user/-authenticate (fn [_ [_ {:keys [data-str]} data-signature]] {:dispatch [::gql-events/mutation - {:queries [[:sign-in {:data-signature data-signature - :data data-str} - [:jwt :user/id]]] - :on-success [::store-active-session]}]})) + {:queries [[:sign-in {:data-signature data-signature + :data data-str} + [:jwt :user/id]]] + :on-success [::store-active-session]}]})) + (comment (re/dispatch [:user/sign-in])) diff --git a/ui/src/ethlance/ui/event/templates.cljs b/ui/src/ethlance/ui/event/templates.cljs index 7a68a617..7ddb4651 100644 --- a/ui/src/ethlance/ui/event/templates.cljs +++ b/ui/src/ethlance/ui/event/templates.cljs @@ -1,5 +1,7 @@ (ns ethlance.ui.event.templates - (:require district.parsers)) + (:require + [district.parsers])) + (defn create-set-feedback-min-rating "Event FX Handler. Set the current feedback min rating. diff --git a/ui/src/ethlance/ui/events.cljs b/ui/src/ethlance/ui/events.cljs index 8aa80a17..68a03064 100644 --- a/ui/src/ethlance/ui/events.cljs +++ b/ui/src/ethlance/ui/events.cljs @@ -1,10 +1,12 @@ (ns ethlance.ui.events (:require + [akiroz.re-frame.storage] [day8.re-frame.forward-events-fx] - [ethlance.ui.page.home.events] + [district.ui.web3-accounts.events] [ethlance.ui.page.arbiters.events] [ethlance.ui.page.candidates.events] [ethlance.ui.page.employers.events] + [ethlance.ui.page.home.events] [ethlance.ui.page.invoices.events] [ethlance.ui.page.job-contract.events] [ethlance.ui.page.job-detail.events] @@ -14,26 +16,20 @@ [ethlance.ui.page.new-job.events] [ethlance.ui.page.profile.events] [ethlance.ui.page.sign-up.events] - [district.ui.web3-accounts.events] [print.foo] - [re-frame.core :as re] - [akiroz.re-frame.storage])) + [re-frame.core :as re])) -(defn has-active-session? [] - (not (nil? (akiroz.re-frame.storage/<-store :ethlance)))) (re/reg-event-fx :ethlance/initialize [(re/inject-cofx :store)] - (fn [{:keys [db store]} [_ config]] + (fn [{:keys [db]} [_ config]] (let [updated-db (-> db (assoc :ethlance/config config) - (assoc :active-session (select-keys (akiroz.re-frame.storage/<-store :ethlance) [:jwt :user/id])) - )] + (assoc :active-session (select-keys (akiroz.re-frame.storage/<-store :ethlance) [:jwt :user/id])))] {:db updated-db :dispatch-n - [ - [:district.ui.graphql.events/set-authorization-token (get-in updated-db [:active-session :jwt])] + [[:district.ui.graphql.events/set-authorization-token (get-in updated-db [:active-session :jwt])] [:page.home/initialize-page] [:page.jobs/initialize-page] [:page.me/initialize-page] diff --git a/ui/src/ethlance/ui/page/about.cljs b/ui/src/ethlance/ui/page/about.cljs index 4ec54dfe..631afec0 100644 --- a/ui/src/ethlance/ui/page/about.cljs +++ b/ui/src/ethlance/ui/page/about.cljs @@ -1,6 +1,8 @@ (ns ethlance.ui.page.about - (:require [district.ui.component.page :refer [page]] - [ethlance.ui.component.main-layout :refer [c-main-layout]])) + (:require + [district.ui.component.page :refer [page]] + [ethlance.ui.component.main-layout :refer [c-main-layout]])) + (defmethod page :route.misc/about [] (fn [] diff --git a/ui/src/ethlance/ui/page/arbiters.cljs b/ui/src/ethlance/ui/page/arbiters.cljs index ef953434..1f00972b 100644 --- a/ui/src/ethlance/ui/page/arbiters.cljs +++ b/ui/src/ethlance/ui/page/arbiters.cljs @@ -1,28 +1,31 @@ (ns ethlance.ui.page.arbiters - (:require [district.ui.component.page :refer [page]] - [district.ui.router.events :as router-events] - [ethlance.shared.constants :as constants] - [ethlance.ui.util.tokens :as tokens] - [ethlance.shared.enumeration.currency-type :as enum.currency] - [ethlance.ui.component.currency-input :refer [c-currency-input]] - [ethlance.ui.component.error-message :refer [c-error-message]] - [ethlance.ui.component.info-message :refer [c-info-message]] - [ethlance.ui.component.loading-spinner :refer [c-loading-spinner]] - [ethlance.ui.component.main-layout :refer [c-main-layout]] - [ethlance.ui.component.mobile-search-filter - :refer - [c-mobile-search-filter]] - [ethlance.ui.component.pagination :refer [c-pagination]] - [ethlance.ui.component.profile-image :refer [c-profile-image]] - [ethlance.ui.component.rating :refer [c-rating]] - [ethlance.ui.component.search-input :refer [c-chip-search-input]] - [ethlance.ui.component.select-input :refer [c-select-input]] - [ethlance.ui.component.tag :refer [c-tag c-tag-label]] - [ethlance.ui.component.text-input :refer [c-text-input]] - [district.ui.graphql.subs :as gql] - [re-frame.core :as re])) - -(defn cf-arbiter-search-filter [] + (:require + [district.ui.component.page :refer [page]] + [district.ui.graphql.subs :as gql] + [district.ui.router.events :as router-events] + [ethlance.shared.constants :as constants] + [ethlance.shared.enumeration.currency-type :as enum.currency] + [ethlance.ui.component.currency-input :refer [c-currency-input]] + [ethlance.ui.component.error-message :refer [c-error-message]] + [ethlance.ui.component.info-message :refer [c-info-message]] + [ethlance.ui.component.loading-spinner :refer [c-loading-spinner]] + [ethlance.ui.component.main-layout :refer [c-main-layout]] + [ethlance.ui.component.mobile-search-filter + :refer + [c-mobile-search-filter]] + [ethlance.ui.component.pagination :refer [c-pagination]] + [ethlance.ui.component.profile-image :refer [c-profile-image]] + [ethlance.ui.component.rating :refer [c-rating]] + [ethlance.ui.component.search-input :refer [c-chip-search-input]] + [ethlance.ui.component.select-input :refer [c-select-input]] + [ethlance.ui.component.tag :refer [c-tag c-tag-label]] + [ethlance.ui.component.text-input :refer [c-text-input]] + [ethlance.ui.util.tokens :as tokens] + [re-frame.core :as re])) + + +(defn cf-arbiter-search-filter + [] (let [*category (re/subscribe [:page.arbiters/category]) *feedback-max-rating (re/subscribe [:page.arbiters/feedback-max-rating]) *feedback-min-rating (re/subscribe [:page.arbiters/feedback-min-rating]) @@ -81,15 +84,19 @@ :color :secondary :default-search-text "Search Countries"}]]))) -(defn c-arbiter-search-filter [] + +(defn c-arbiter-search-filter + [] [:div.search-filter [cf-arbiter-search-filter]]) + (defn c-arbiter-mobile-search-filter [] [c-mobile-search-filter [cf-arbiter-search-filter]]) + (defn c-arbiter-element [{:keys [:user/id] :as arbiter}] [:div.arbiter-element {:on-click #(re/dispatch [::router-events/navigate :route.user/profile {:address id} {:tab "arbiter"}])} @@ -97,35 +104,36 @@ [:div.profile-image [c-profile-image {:src (-> arbiter :user :user/profile-image)}]] [:div.name (get-in arbiter [:user :user/name])]] [:div.price (tokens/fiat-amount-with-symbol (-> arbiter :arbiter/fee-currency-id) - (-> arbiter :arbiter/fee))] + (-> arbiter :arbiter/fee))] [:div.tags (doall - (for [tag-label (get-in arbiter [:skills])] - ^{:key (str "tag-" tag-label)} - [c-tag {:on-click #(re/dispatch [:page.arbiters/add-skill tag-label]) - :title (str "Add '" tag-label "' to Search")} - [c-tag-label tag-label]]))] + (for [tag-label (get arbiter :skills)] + ^{:key (str "tag-" tag-label)} + [c-tag {:on-click #(re/dispatch [:page.arbiters/add-skill tag-label]) + :title (str "Add '" tag-label "' to Search")} + [c-tag-label tag-label]]))] [:div.rating [c-rating {:rating (-> arbiter :arbiter/rating)}] [:div.label (str "(" (-> arbiter :arbiter/feedback :total-count) ")")]] [:div.location (get-in arbiter [:user :user/country])]]) -(defn c-arbiter-listing [] +(defn c-arbiter-listing + [] (let [query-params (re/subscribe [:page.arbiters/search-params]) query [:arbiter-search @query-params - [:total-count - [:items [:user/id - [:user [:user/id - :user/name - :user/country - :user/profile-image]] - [:arbiter/feedback [:total-count]] - :arbiter/categories - :arbiter/skills - :arbiter/rating - :arbiter/fee - :arbiter/fee-currency-id]]]] + [:total-count + [:items [:user/id + [:user [:user/id + :user/name + :user/country + :user/profile-image]] + [:arbiter/feedback [:total-count]] + :arbiter/categories + :arbiter/skills + :arbiter/rating + :arbiter/fee + :arbiter/fee-currency-id]]]] results (re/subscribe [::gql/query {:queries [query]} {:id @query-params}]) *limit (re/subscribe [:page.arbiters/limit]) *offset (re/subscribe [:page.arbiters/offset]) @@ -163,6 +171,7 @@ :offset @*offset :set-offset-event :page.arbiters/set-offset}])])) + (defmethod page :route.user/arbiters [] (let [*skills (re/subscribe [:page.arbiters/skills])] (fn [] diff --git a/ui/src/ethlance/ui/page/arbiters/events.cljs b/ui/src/ethlance/ui/page/arbiters/events.cljs index a9103c37..05d8250b 100644 --- a/ui/src/ethlance/ui/page/arbiters/events.cljs +++ b/ui/src/ethlance/ui/page/arbiters/events.cljs @@ -1,12 +1,14 @@ (ns ethlance.ui.page.arbiters.events - (:require [district.parsers :refer [parse-int]] - [district.ui.router.effects :as router.effects] - [ethlance.shared.constants :as constants] - [ethlance.ui.event.templates :as event.templates] - [ethlance.ui.event.utils :as event.utils] - [re-frame.core :as re])) + (:require + [district.parsers :refer [parse-int]] + [ethlance.ui.event.templates :as event.templates] + [ethlance.ui.event.utils :as event.utils] + [re-frame.core :as re])) + (def state-key :page.arbiters) + + (def state-default {:offset 0 :limit 10 @@ -23,13 +25,15 @@ (defn initialize-page "Event FX Handler. Setup listener to dispatch an event when the page is active/visited." [{:keys [db]} _] - {:db (assoc-in db [state-key] state-default)}) + {:db (assoc db state-key state-default)}) + (defn add-skill "Event FX Handler. Append skill to skill listing." [{:keys [db]} [_ new-skill]] {:db (update-in db [state-key :skills] conj new-skill)}) + ;; ;; Registered Events ;; diff --git a/ui/src/ethlance/ui/page/arbiters/subscriptions.cljs b/ui/src/ethlance/ui/page/arbiters/subscriptions.cljs index 88cd2f4c..5f567c31 100644 --- a/ui/src/ethlance/ui/page/arbiters/subscriptions.cljs +++ b/ui/src/ethlance/ui/page/arbiters/subscriptions.cljs @@ -1,10 +1,10 @@ (ns ethlance.ui.page.arbiters.subscriptions (:require - [ethlance.ui.util.graphql :as graphql-util] - [re-frame.core :as re] + [ethlance.ui.page.arbiters.events :as arbiters.events] + [ethlance.ui.subscription.utils :as subscription.utils] + [ethlance.ui.util.graphql :as graphql-util] + [re-frame.core :as re])) - [ethlance.ui.page.arbiters.events :as arbiters.events] - [ethlance.ui.subscription.utils :as subscription.utils])) (def create-get-handler #(subscription.utils/create-get-handler arbiters.events/state-key %)) @@ -24,13 +24,14 @@ (re/reg-sub :page.arbiters/payment-type (create-get-handler :payment-type)) (re/reg-sub :page.arbiters/country (create-get-handler :country)) + (re/reg-sub :page.arbiters/search-params (fn [db _] {:offset (get-in db [arbiters.events/state-key :offset]) :limit (get-in db [arbiters.events/state-key :limit]) :search-params (graphql-util/prepare-search-params - (get-in db [arbiters.events/state-key] {}) + (get db arbiters.events/state-key {}) [[:skills #(into [] %)] [:category second] [:feedback-min-rating] diff --git a/ui/src/ethlance/ui/page/candidates.cljs b/ui/src/ethlance/ui/page/candidates.cljs index c3735357..89c46762 100644 --- a/ui/src/ethlance/ui/page/candidates.cljs +++ b/ui/src/ethlance/ui/page/candidates.cljs @@ -1,31 +1,30 @@ (ns ethlance.ui.page.candidates "General Candidate Listings on ethlance" - (:require [cuerdas.core :as str] - [district.ui.component.page :refer [page]] - [district.ui.router.events :as router-events] - [ethlance.shared.constants :as constants] - [district.ui.graphql.subs :as gql] - [ethlance.ui.util.tokens :as tokens] - [ethlance.shared.enumeration.currency-type :as enum.currency] - [ethlance.ui.component.currency-input :refer [c-currency-input]] - [ethlance.ui.component.error-message :refer [c-error-message]] - [ethlance.ui.component.info-message :refer [c-info-message]] - [ethlance.ui.component.loading-spinner :refer [c-loading-spinner]] - [ethlance.ui.component.main-layout :refer [c-main-layout]] - [ethlance.ui.component.mobile-search-filter - :refer - [c-mobile-search-filter]] - [ethlance.ui.component.pagination :refer [c-pagination]] - [ethlance.ui.component.profile-image :refer [c-profile-image]] - [ethlance.ui.component.radio-select - :refer - [c-radio-search-filter-element c-radio-select]] - [ethlance.ui.component.rating :refer [c-rating]] - [ethlance.ui.component.search-input :refer [c-chip-search-input]] - [ethlance.ui.component.select-input :refer [c-select-input]] - [ethlance.ui.component.tag :refer [c-tag c-tag-label]] - [ethlance.ui.component.text-input :refer [c-text-input]] - [re-frame.core :as re])) + (:require + [cuerdas.core :as str] + [district.ui.component.page :refer [page]] + [district.ui.graphql.subs :as gql] + [district.ui.router.events :as router-events] + [ethlance.shared.constants :as constants] + [ethlance.shared.enumeration.currency-type :as enum.currency] + [ethlance.ui.component.currency-input :refer [c-currency-input]] + [ethlance.ui.component.error-message :refer [c-error-message]] + [ethlance.ui.component.info-message :refer [c-info-message]] + [ethlance.ui.component.loading-spinner :refer [c-loading-spinner]] + [ethlance.ui.component.main-layout :refer [c-main-layout]] + [ethlance.ui.component.mobile-search-filter + :refer + [c-mobile-search-filter]] + [ethlance.ui.component.pagination :refer [c-pagination]] + [ethlance.ui.component.profile-image :refer [c-profile-image]] + [ethlance.ui.component.rating :refer [c-rating]] + [ethlance.ui.component.search-input :refer [c-chip-search-input]] + [ethlance.ui.component.select-input :refer [c-select-input]] + [ethlance.ui.component.tag :refer [c-tag c-tag-label]] + [ethlance.ui.component.text-input :refer [c-text-input]] + [ethlance.ui.util.tokens :as tokens] + [re-frame.core :as re])) + (defn cf-candidate-search-filter "Component Fragment for the candidate search filter." @@ -90,17 +89,21 @@ :color :secondary :default-search-text "Search Countries"}]]]))) + (defn c-candidate-search-filter [] [:div.search-filter [cf-candidate-search-filter]]) + (defn c-candidate-mobile-search-filter [] [c-mobile-search-filter [cf-candidate-search-filter]]) -(defn c-candidate-element [candidate] + +(defn c-candidate-element + [candidate] [:div.candidate-element {:on-click #(re/dispatch [::router-events/navigate :route.user/profile {:address (-> candidate :user/id)} {:tab "candidate"}])} [:div.profile [:div.profile-image [c-profile-image {:src (-> candidate :user :user/profile-image)}]] @@ -109,17 +112,18 @@ [:div.price (tokens/fiat-amount-with-symbol (-> candidate :candidate/rate-currency-id) (-> candidate :candidate/rate))] [:div.tags (doall - (for [tag-label (-> candidate :candidate/skills)] - ^{:key (str "tag-" tag-label)} - [c-tag {:on-click #(re/dispatch [:page.candidates/add-skill tag-label]) - :title (str "Add '" tag-label "' to Search")} - [c-tag-label tag-label]]))] + (for [tag-label (-> candidate :candidate/skills)] + ^{:key (str "tag-" tag-label)} + [c-tag {:on-click #(re/dispatch [:page.candidates/add-skill tag-label]) + :title (str "Add '" tag-label "' to Search")} + [c-tag-label tag-label]]))] [:div.rating [c-rating {:rating (-> candidate :candidate/rating)}] [:div.label (str "(" (-> candidate :candidate/feedback :total-count) ")")]]]) -(defn c-candidate-listing [] +(defn c-candidate-listing + [] (let [*limit (re/subscribe [:page.candidates/limit]) *offset (re/subscribe [:page.candidates/offset]) query-params (re/subscribe [:page.candidates/search-params])] @@ -161,9 +165,9 @@ :else (doall - (for [candidate candidate-listing] - ^{:key (str "candidate-" (hash candidate))} - [c-candidate-element candidate]))) + (for [candidate candidate-listing] + ^{:key (str "candidate-" (hash candidate))} + [c-candidate-element candidate]))) ;; Pagination (when (seq candidate-listing) @@ -173,6 +177,7 @@ :offset (or @*offset 0) :set-offset-event :page.candidates/set-offset}])])))) + (defmethod page :route.user/candidates [] (let [*skills (re/subscribe [:page.candidates/skills])] (fn [] diff --git a/ui/src/ethlance/ui/page/candidates/events.cljs b/ui/src/ethlance/ui/page/candidates/events.cljs index 897fb1fc..6f5de050 100644 --- a/ui/src/ethlance/ui/page/candidates/events.cljs +++ b/ui/src/ethlance/ui/page/candidates/events.cljs @@ -1,12 +1,14 @@ (ns ethlance.ui.page.candidates.events - (:require [district.ui.router.effects :as router.effects] - [district.parsers :refer [parse-int]] - [ethlance.shared.constants :as constants] - [ethlance.ui.event.templates :as event.templates] - [ethlance.ui.event.utils :as event.utils] - [re-frame.core :as re])) + (:require + [district.parsers :refer [parse-int]] + [ethlance.ui.event.templates :as event.templates] + [ethlance.ui.event.utils :as event.utils] + [re-frame.core :as re])) + (def state-key :page.candidates) + + (def state-default {:offset 0 :limit 10 @@ -16,16 +18,19 @@ :feedback-max-rating 5 :country nil}) + (defn initialize-page "Event FX Handler. Setup listener to dispatch an event when the page is active/visited." [{:keys [db]} _] - {:db (assoc-in db [state-key] state-default)}) + {:db (assoc db state-key state-default)}) + (defn add-skill "Event FX Handler. Append skill to skill listing." [{:keys [db]} [_ new-skill]] {:db (update-in db [state-key :skills] conj new-skill)}) + ;; ;; Registered Events ;; diff --git a/ui/src/ethlance/ui/page/candidates/subscriptions.cljs b/ui/src/ethlance/ui/page/candidates/subscriptions.cljs index 9e5817ae..cfe10a4c 100644 --- a/ui/src/ethlance/ui/page/candidates/subscriptions.cljs +++ b/ui/src/ethlance/ui/page/candidates/subscriptions.cljs @@ -1,9 +1,10 @@ (ns ethlance.ui.page.candidates.subscriptions (:require - [re-frame.core :as re] - [ethlance.ui.util.graphql :as graphql-util] - [ethlance.ui.page.candidates.events :as candidates.events] - [ethlance.ui.subscription.utils :as subscription.utils])) + [ethlance.ui.page.candidates.events :as candidates.events] + [ethlance.ui.subscription.utils :as subscription.utils] + [ethlance.ui.util.graphql :as graphql-util] + [re-frame.core :as re])) + (def create-get-handler #(subscription.utils/create-get-handler candidates.events/state-key %)) @@ -23,6 +24,7 @@ (re/reg-sub :page.candidates/min-num-feedbacks (create-get-handler :min-num-feedbacks)) (re/reg-sub :page.candidates/country (create-get-handler :country)) + (re/reg-sub :page.candidates/search-params (fn [db _] @@ -30,7 +32,7 @@ :limit (get-in db [candidates.events/state-key :limit]) :search-params (graphql-util/prepare-search-params - (get-in db [candidates.events/state-key] {}) + (get db candidates.events/state-key {}) [[:skills #(into [] %)] [:category second] [:feedback-max-rating] diff --git a/ui/src/ethlance/ui/page/dev/contract_ops.cljs b/ui/src/ethlance/ui/page/dev/contract_ops.cljs index af7acc45..404eb65a 100644 --- a/ui/src/ethlance/ui/page/dev/contract_ops.cljs +++ b/ui/src/ethlance/ui/page/dev/contract_ops.cljs @@ -1,29 +1,34 @@ (ns ethlance.ui.page.dev.contract-ops (:require + [clojure.edn :as edn] + [cljs.pprint] [district.ui.component.page :refer [page]] - [ethlance.ui.component.main-layout :refer [c-main-layout]] - [district.ui.router.subs :as router.subs] - [re-frame.core :as re] - [ethlance.ui.component.select-input :refer [c-select-input]] - [district.ui.smart-contracts.queries :as contract-queries] [district.ui.web3-tx.events :as web3-events] - [clojure.edn :as edn])) + [district.ui.web3-accounts.queries] + [ethlance.ui.config] + [re-frame.core :as re])) + -; ------------ EVENTS ---------------- +;; ------------ EVENTS ---------------- (def state-key :page.dev.contract-ops) + (re/reg-event-db :page.dev.contract-ops/set-token-amount (fn [db [_ amount]] (assoc-in db [state-key :token-amount] amount))) + (re/reg-event-db :page.dev.contract-ops/set-token-info (fn [db [_ id]] (assoc-in db [state-key :token-info] id))) + (def ze-tx-receipt (atom nil)) + + (re/reg-event-db ::mint-token-tx-success (fn [db [_ token-type tx-receipt]] @@ -35,11 +40,13 @@ :erc1155 (get-in tx-receipt [:events :Transfer-single :return-values :id]) nil)))) + (re/reg-event-db ::mint-token-tx-error - (fn [db [_ tx-receipt]] + (fn [_ [_ tx-receipt]] (println ">>> ::mint-token-tx-error" tx-receipt))) + (re/reg-event-fx :page.dev.contract-ops/send-mint-token-tx (fn [{:keys [db]} _] @@ -67,42 +74,50 @@ :on-tx-success [::mint-token-tx-success token-type] :on-tx-error [::mint-token-tx-error]}]]]}))) + (re/reg-event-db :page.dev.contract-ops/update-app-db-query (fn [db [_ query]] (assoc-in db [state-key :app-db-query] query))) + (re/reg-event-db :page.dev.contract-ops/make-app-db-query (fn [db [_ query]] (assoc-in db [state-key :app-db-query-vector] (clojure.edn/read-string query)))) -; ------------ SUBSCRIPTIONS ---------------- + +;; ------------ SUBSCRIPTIONS ---------------- (re/reg-sub :page.dev.contract-ops/token-amount (fn [db _] (get-in db [state-key :token-amount]))) + (re/reg-sub :page.dev.contract-ops/token-info (fn [db _] (get-in db [state-key :token-info]))) + (re/reg-sub :page.dev.contract-ops/minted-token-id (fn [db _] (get-in db [state-key :minted-token-id]))) + (re/reg-sub :page.dev.contract-ops/app-db-query-input (fn [db _] (get-in db [state-key :app-db-query]))) + (re/reg-sub :page.dev.contract-ops/app-db-query-results (fn [db _] (get-in db (get-in db [state-key :app-db-query-vector])))) + (re/reg-sub :page.dev.contract-ops/app-db-result-keys :<- [:page.dev.contract-ops/app-db-query-results] @@ -111,13 +126,14 @@ (keys query-result) "Not a map"))) -; ----------------- VIEWS ------------------- -(defn c-app-db [] +;; ----------------- VIEWS ------------------- + +(defn c-app-db + [] (let [query-input (re/subscribe [:page.dev.contract-ops/app-db-query-input]) query-results (re/subscribe [:page.dev.contract-ops/app-db-query-results]) - result-keys (re/subscribe [:page.dev.contract-ops/app-db-result-keys]) - ] + result-keys (re/subscribe [:page.dev.contract-ops/app-db-result-keys])] [:div {:style {:margin "1em" :border "solid 1px"}} [:label "Enter query for app-db (ENTER to query)"] [:input {:value @query-input @@ -125,8 +141,8 @@ (set! (.. js/window -zeOnChangeEvent) event) (re/dispatch [:page.dev.contract-ops/update-app-db-query (-> event .-target .-value)])) :on-key-up (fn [event] - (if (= "Enter" (.-key event)) - (re/dispatch [:page.dev.contract-ops/make-app-db-query @query-input])))}] + (when (= "Enter" (.-key event)) + (re/dispatch [:page.dev.contract-ops/make-app-db-query @query-input])))}] [:h3 {:style {:font-size "2em"}} "Keys"] [:pre {:style {:font-family "Monospace" :font-weight "bold"}} (with-out-str (cljs.pprint/pprint @result-keys))] @@ -134,9 +150,10 @@ [:pre {:style {:font-family "Monospace"}} (with-out-str (cljs.pprint/pprint @query-results))]])) -(defn c-mint-tokens [] + +(defn c-mint-tokens + [] (let [token-amount (re/subscribe [:page.dev.contract-ops/token-amount]) - selected-token (re/subscribe [:page.dev.contract-ops/token-info]) minted-token-id (re/subscribe [:page.dev.contract-ops/minted-token-id]) tokens {:erc20 :token :erc721 :test-nft @@ -155,8 +172,7 @@ :value token-info :name "token-selection" :on-change #(re/dispatch [:page.dev.contract-ops/set-token-info token-info])}] - [:label {:for (:name token-info)} (str (name (:type token-info)) " " (:name token-info) " (" (:address token-info)")")] - ]) + [:label {:for (:name token-info)} (str (name (:type token-info)) " " (:name token-info) " (" (:address token-info) ")")]]) available-tokens)) [:label "Token Amount"] [:input {:type :text :value @token-amount :on-change #(re/dispatch [:page.dev.contract-ops/set-token-amount (-> % .-target .-value)])}] @@ -164,10 +180,11 @@ [:p "Minted token with ID: "] [:code @minted-token-id]])) + (defmethod page :route.dev/contract-ops [] (fn [] [:div [:h1 "Contract operations"] - [:h2 {:style {:font-size "3em"}}"Mint tokens"] + [:h2 {:style {:font-size "3em"}} "Mint tokens"] [c-mint-tokens] [c-app-db]])) diff --git a/ui/src/ethlance/ui/page/devcard.cljs b/ui/src/ethlance/ui/page/devcard.cljs index 8f874523..cd30cef1 100644 --- a/ui/src/ethlance/ui/page/devcard.cljs +++ b/ui/src/ethlance/ui/page/devcard.cljs @@ -1,17 +1,19 @@ (ns ethlance.ui.page.devcard "Development Page for showing off different reagent components" - (:require [district.ui.component.page :refer [page]] - [ethlance.shared.constants :as constants] - [ethlance.ui.component.button :refer [c-button c-button-label]] - [ethlance.ui.component.checkbox :refer [c-labeled-checkbox]] - [ethlance.ui.component.circle-button :refer [c-circle-icon-button]] - [ethlance.ui.component.ethlance-logo :refer [c-ethlance-logo]] - [ethlance.ui.component.icon :refer [c-icon]] - [ethlance.ui.component.inline-svg :refer [c-inline-svg]] - [ethlance.ui.component.rating :refer [c-rating]] - [ethlance.ui.component.scrollable :refer [c-scrollable]] - [ethlance.ui.component.search-input :refer [c-chip-search-input]] - [ethlance.ui.component.select-input :refer [c-select-input]])) + (:require + [district.ui.component.page :refer [page]] + [ethlance.shared.constants :as constants] + [ethlance.ui.component.button :refer [c-button c-button-label]] + [ethlance.ui.component.checkbox :refer [c-labeled-checkbox]] + [ethlance.ui.component.circle-button :refer [c-circle-icon-button]] + [ethlance.ui.component.ethlance-logo :refer [c-ethlance-logo]] + [ethlance.ui.component.icon :refer [c-icon]] + [ethlance.ui.component.inline-svg :refer [c-inline-svg]] + [ethlance.ui.component.rating :refer [c-rating]] + [ethlance.ui.component.scrollable :refer [c-scrollable]] + [ethlance.ui.component.search-input :refer [c-chip-search-input]] + [ethlance.ui.component.select-input :refer [c-select-input]])) + (defmethod page :route.devcard/index [] (fn [] @@ -228,17 +230,17 @@ [c-scrollable {:forceVisible true :autoHide false} [:ul (doall - (for [i (range 30)] - ^{:key (str "el-" i)} - [:li (str "Element " (inc i))]))]]] + (for [i (range 30)] + ^{:key (str "el-" i)} + [:li (str "Element " (inc i))]))]]] [:div.scrollable-fixed-horizontal [c-scrollable {:forceVisible true :autoHide false} [:div (doall - (for [i (range 30)] - ^{:key (str "el-" i)} - [:span (str "Element " (inc i))]))]]]]] + (for [i (range 30)] + ^{:key (str "el-" i)} + [:span (str "Element " (inc i))]))]]]]] [:div.dark-grouping [:div.title "Ethlance Scrollable (dark)"] @@ -247,12 +249,12 @@ [c-scrollable {:maxHeight 400 :forceVisible true :autoHide false} [:ul (doall - (for [i (range 30)] - [:li (str "Element " (inc i))]))]]] + (for [i (range 30)] + [:li (str "Element " (inc i))]))]]] [:div.scrollable-fixed-horizontal [c-scrollable {} [:div (doall - (for [i (range 30)] - [:span (str "Element " (inc i))]))]]]]]]])) + (for [i (range 30)] + [:span (str "Element " (inc i))]))]]]]]]])) diff --git a/ui/src/ethlance/ui/page/employers.cljs b/ui/src/ethlance/ui/page/employers.cljs index 7f4ec7b1..70cb2284 100644 --- a/ui/src/ethlance/ui/page/employers.cljs +++ b/ui/src/ethlance/ui/page/employers.cljs @@ -1,24 +1,27 @@ (ns ethlance.ui.page.employers "General Employer Listings on ethlance" - (:require [district.ui.component.page :refer [page]] - [ethlance.shared.constants :as constants] - [ethlance.ui.component.error-message :refer [c-error-message]] - [ethlance.ui.component.info-message :refer [c-info-message]] - [ethlance.ui.component.loading-spinner :refer [c-loading-spinner]] - [ethlance.ui.component.main-layout :refer [c-main-layout]] - [ethlance.ui.component.mobile-search-filter - :refer - [c-mobile-search-filter]] - [ethlance.ui.component.pagination :refer [c-pagination]] - [ethlance.ui.component.profile-image :refer [c-profile-image]] - [ethlance.ui.component.rating :refer [c-rating]] - [ethlance.ui.component.search-input :refer [c-chip-search-input]] - [ethlance.ui.component.select-input :refer [c-select-input]] - [ethlance.ui.component.tag :refer [c-tag c-tag-label]] - [ethlance.ui.component.text-input :refer [c-text-input]] - [re-frame.core :as re])) - -(defn cf-employer-search-filter [] + (:require + [district.ui.component.page :refer [page]] + [ethlance.shared.constants :as constants] + [ethlance.ui.component.error-message :refer [c-error-message]] + [ethlance.ui.component.info-message :refer [c-info-message]] + [ethlance.ui.component.loading-spinner :refer [c-loading-spinner]] + [ethlance.ui.component.main-layout :refer [c-main-layout]] + [ethlance.ui.component.mobile-search-filter + :refer + [c-mobile-search-filter]] + [ethlance.ui.component.pagination :refer [c-pagination]] + [ethlance.ui.component.profile-image :refer [c-profile-image]] + [ethlance.ui.component.rating :refer [c-rating]] + [ethlance.ui.component.search-input :refer [c-chip-search-input]] + [ethlance.ui.component.select-input :refer [c-select-input]] + [ethlance.ui.component.tag :refer [c-tag c-tag-label]] + [ethlance.ui.component.text-input :refer [c-text-input]] + [re-frame.core :as re])) + + +(defn cf-employer-search-filter + [] (let [*category (re/subscribe [:page.employers/category]) *feedback-max-rating (re/subscribe [:page.employers/feedback-max-rating]) *feedback-min-rating (re/subscribe [:page.employers/feedback-min-rating]) @@ -59,15 +62,19 @@ :color :secondary :default-search-text "Search Countries"}]]]))) -(defn c-employer-search-filter [] + +(defn c-employer-search-filter + [] [:div.search-filter [cf-employer-search-filter]]) + (defn c-employer-mobile-search-filter [] [c-mobile-search-filter [cf-employer-search-filter]]) + (defn c-employer-element [{:employer/keys [professional-title]}] [:div.employer-element @@ -77,31 +84,33 @@ [:div.title professional-title]] [:div.tags (doall - (for [tag-label #{"System Administration" "Game Design" "C++" "HopScotch Master"}] - ^{:key (str "tag-" tag-label)} - [c-tag {:on-click #(re/dispatch [:page.employers/add-skill tag-label]) - :title (str "Add '" tag-label "' to Search")} - [c-tag-label tag-label]]))] + (for [tag-label #{"System Administration" "Game Design" "C++" "HopScotch Master"}] + ^{:key (str "tag-" tag-label)} + [c-tag {:on-click #(re/dispatch [:page.employers/add-skill tag-label]) + :title (str "Add '" tag-label "' to Search")} + [c-tag-label tag-label]]))] [:div.rating [c-rating {:default-rating 3}] [:div.label "(4)"]] [:div.location "New York, United States"]]) -(defn c-employer-listing [] + +(defn c-employer-listing + [] (let [*limit (re/subscribe [:page.employers/limit]) *offset (re/subscribe [:page.employers/offset]) *employer-listing-query (re/subscribe - [:gql/query - {:queries - [[:employer-search - {:limit @*limit - :offset @*offset} - [[:items [:user/id - :employer/bio - :employer/professional-title]] - :total-count - :end-cursor]]]}])] + [:gql/query + {:queries + [[:employer-search + {:limit @*limit + :offset @*offset} + [[:items [:user/id + :employer/bio + :employer/professional-title]] + :total-count + :end-cursor]]]}])] (fn [] (let [{employer-search :employer-search preprocessing? :graphql/preprocessing? @@ -125,9 +134,9 @@ :else (doall - (for [employer employer-listing] - ^{:key (str "employer-" (hash employer))} - [c-employer-element employer]))) + (for [employer employer-listing] + ^{:key (str "employer-" (hash employer))} + [c-employer-element employer]))) ;; Pagination (when (seq employer-listing) @@ -137,6 +146,7 @@ :offset @*offset :set-offset-event :page.employers/set-offset}])])))) + (defmethod page :route.user/employers [] (let [*skills (re/subscribe [:page.employers/skills])] (fn [] diff --git a/ui/src/ethlance/ui/page/employers/events.cljs b/ui/src/ethlance/ui/page/employers/events.cljs index 9eae8fca..86dbf652 100644 --- a/ui/src/ethlance/ui/page/employers/events.cljs +++ b/ui/src/ethlance/ui/page/employers/events.cljs @@ -1,13 +1,16 @@ (ns ethlance.ui.page.employers.events - (:require [district.parsers :refer [parse-int]] - [district.ui.router.effects :as router.effects] - [ethlance.shared.constants :as constants] - [ethlance.ui.event.templates :as event.templates] - [ethlance.ui.event.utils :as event.utils] - [re-frame.core :as re])) + (:require + [district.parsers :refer [parse-int]] + [district.ui.router.effects :as router.effects] + [ethlance.shared.constants :as constants] + [ethlance.ui.event.templates :as event.templates] + [ethlance.ui.event.utils :as event.utils] + [re-frame.core :as re])) + (def state-key :page.employers) + (def state-default {:offset 0 :limit 10 @@ -18,8 +21,10 @@ :min-num-feedbacks nil :country nil}) + (def create-assoc-handler (partial event.utils/create-assoc-handler state-key)) + (defn initialize-page "Event FX Handler. Setup listener to dispatch an event when the page is active/visited." [{:keys []} _] @@ -28,11 +33,13 @@ :name :route.user/employers :dispatch []}]}) + (defn add-skill "Event FX Handler. Append skill to skill listing." [{:keys [db]} [_ new-skill]] {:db (update-in db [state-key :skills] conj new-skill)}) + (re/reg-event-fx :page.employers/initialize-page initialize-page) (re/reg-event-fx :page.employers/set-offset (create-assoc-handler :offset)) (re/reg-event-fx :page.employers/set-limit (create-assoc-handler :limit)) diff --git a/ui/src/ethlance/ui/page/employers/subscriptions.cljs b/ui/src/ethlance/ui/page/employers/subscriptions.cljs index 78c90ce0..26f3f5f0 100644 --- a/ui/src/ethlance/ui/page/employers/subscriptions.cljs +++ b/ui/src/ethlance/ui/page/employers/subscriptions.cljs @@ -1,9 +1,8 @@ (ns ethlance.ui.page.employers.subscriptions (:require - [re-frame.core :as re] - - [ethlance.ui.page.employers.events :as employers.events] - [ethlance.ui.subscription.utils :as subscription.utils])) + [ethlance.ui.page.employers.events :as employers.events] + [ethlance.ui.subscription.utils :as subscription.utils] + [re-frame.core :as re])) (def create-get-handler #(subscription.utils/create-get-handler employers.events/state-key %)) diff --git a/ui/src/ethlance/ui/page/home.cljs b/ui/src/ethlance/ui/page/home.cljs index e2e1d1bc..64c02b0d 100644 --- a/ui/src/ethlance/ui/page/home.cljs +++ b/ui/src/ethlance/ui/page/home.cljs @@ -1,7 +1,9 @@ (ns ethlance.ui.page.home "Main landing page for ethlance website" - (:require [district.ui.component.page :refer [page]] - [ethlance.ui.component.splash-layout :refer [c-splash-layout]])) + (:require + [district.ui.component.page :refer [page]] + [ethlance.ui.component.splash-layout :refer [c-splash-layout]])) + (defmethod page :route/home [] (fn [] diff --git a/ui/src/ethlance/ui/page/home/events.cljs b/ui/src/ethlance/ui/page/home/events.cljs index e26152e4..fabff0cc 100644 --- a/ui/src/ethlance/ui/page/home/events.cljs +++ b/ui/src/ethlance/ui/page/home/events.cljs @@ -5,17 +5,19 @@ [re-frame.core :as re])) -(defn logged-in? [active-account active-session-user-id] +(defn logged-in? + [active-account active-session-user-id] (and (not-any? nil? [active-account active-session-user-id]) (ethlance.shared.utils/ilike= active-account active-session-user-id))) + (defn initialize-page "Event FX Handler. Setup listener to dispatch an event when the page is active/visited." [{:keys [db]} _] - (let [] - (when-not (logged-in? (accounts-queries/active-account db) - (get-in db [:active-session :user/id])) - {:fx [[:dispatch [:modal/open ::sign-in]]]}))) + (when-not (logged-in? (accounts-queries/active-account db) + (get-in db [:active-session :user/id])) + {:fx [[:dispatch [:modal/open ::sign-in]]]})) + (re/reg-event-fx :page.home/initialize-page initialize-page) diff --git a/ui/src/ethlance/ui/page/how_it_works.cljs b/ui/src/ethlance/ui/page/how_it_works.cljs index 55ed0abb..69b40262 100644 --- a/ui/src/ethlance/ui/page/how_it_works.cljs +++ b/ui/src/ethlance/ui/page/how_it_works.cljs @@ -1,6 +1,8 @@ (ns ethlance.ui.page.how-it-works - (:require [district.ui.component.page :refer [page]] - [ethlance.ui.component.main-layout :refer [c-main-layout]])) + (:require + [district.ui.component.page :refer [page]] + [ethlance.ui.component.main-layout :refer [c-main-layout]])) + (defmethod page :route.misc/how-it-works [] (fn [] diff --git a/ui/src/ethlance/ui/page/invoices.cljs b/ui/src/ethlance/ui/page/invoices.cljs index 91e03d03..82ad84c2 100644 --- a/ui/src/ethlance/ui/page/invoices.cljs +++ b/ui/src/ethlance/ui/page/invoices.cljs @@ -1,28 +1,30 @@ (ns ethlance.ui.page.invoices - (:require [district.ui.component.page :refer [page]] - [reagent.ratom] - [ethlance.shared.utils :as shared-utils] - [ethlance.ui.util.tokens :as tokens] - [ethlance.ui.util.dates :refer [relative-ago formatted-date]] - [ethlance.ui.component.icon :refer [c-icon]] - [re-frame.core :as re] - [district.ui.router.subs :as router.subs] - [district.ui.graphql.subs :as gql] - [ethlance.ui.component.main-layout :refer [c-main-layout]] - [ethlance.ui.component.profile-image :refer [c-profile-image]] - [ethlance.ui.component.token-info :refer [c-token-info]] - [ethlance.ui.util.navigation :as util.navigation] - [ethlance.ui.component.rating :refer [c-rating]])) + (:require + [clojure.string] + [district.ui.component.page :refer [page]] + [district.ui.graphql.subs :as gql] + [ethlance.ui.component.icon :refer [c-icon]] + [ethlance.ui.component.main-layout :refer [c-main-layout]] + [ethlance.ui.component.profile-image :refer [c-profile-image]] + [ethlance.ui.component.rating :refer [c-rating]] + [ethlance.ui.component.token-info :refer [c-token-info]] + [ethlance.ui.util.dates :refer [formatted-date]] + [ethlance.ui.util.navigation :as util.navigation] + [re-frame.core :as re] + [reagent.ratom])) -(defn c-participant-user-info [data-prefix data] + +(defn c-participant-user-info + [data-prefix data] [:div.profile.employer [:div.label (clojure.string/capitalize (name data-prefix))] [c-profile-image {:src (get-in data [:user :user/profile-image])}] [:div.name (get-in data [:user :user/name])] - [:div.rating - [c-rating {:rating (get-in data [(keyword data-prefix "rating")])}] - [:span.num-feedback (str "(" (get-in data [(keyword data-prefix "feedback") :total-count]) ")")]] - [:div.location (get-in data [:user :user/country])]]) + [:div.rating + [c-rating {:rating (get-in data [(keyword data-prefix "rating")])}] + [:span.num-feedback (str "(" (get data (keyword data-prefix "feedback") :total-count) ")")]] + [:div.location (get-in data [:user :user/country])]]) + (defmethod page :route.invoice/index [] (fn [] @@ -74,12 +76,10 @@ result @(re/subscribe [::gql/query {:queries [query]} {:refetch-on #{:page.invoices/refetch-invoice}}]) - job-token-symbol (get-in result [:job :token-details :token-detail/symbol]) job (:job result) - job-title (:title job) - invoice (get-in job [:invoice]) - employer (get-in job [:job/employer]) - arbiter (get-in job [:job/arbiter]) + invoice (get job :invoice) + employer (get job :job/employer) + arbiter (get job :job/arbiter) candidate (get-in invoice [:job-story :candidate]) invoice-to-pay {:job/id contract-address @@ -89,12 +89,12 @@ :payer (:user/id employer) :receiver (:user/id candidate)} - invoice-payable? (not= "paid" (get-in invoice [:invoice/status])) + invoice-payable? (not= "paid" (get invoice :invoice/status)) info-panel [["Invoiced Amount" (when-not (:graphql/loading? result) [c-token-info (:invoice/amount-requested invoice) (:token-details job)])] - ["Hours Worked" (get-in invoice [:invoice/hours-worked])] - ["Hourly Rate" (get-in invoice [:invoice/hourly-rate])] + ["Hours Worked" (get invoice :invoice/hours-worked)] + ["Hourly Rate" (get invoice :invoice/hourly-rate)] ["Invoiced On" (formatted-date #(get-in % [:creation-message :message/date-created]) invoice)]]] [c-main-layout {:container-opts {:class :invoice-detail-main-container}} [:div.title "Invoice"] @@ -113,7 +113,7 @@ (if invoice-payable? [:div.button {:on-click #(re/dispatch [:page.invoices/pay invoice-to-pay])} - [:span "Pay Invoice"] - [c-icon {:name :ic-arrow-right :size :small :color :white}]] + [:span "Pay Invoice"] + [c-icon {:name :ic-arrow-right :size :small :color :white}]] [:div.button {:style {:background-color :gray}} - [:span "Invoice Paid"]])]))) + [:span "Invoice Paid"]])]))) diff --git a/ui/src/ethlance/ui/page/invoices/events.cljs b/ui/src/ethlance/ui/page/invoices/events.cljs index 0e1479c2..312d7c5f 100644 --- a/ui/src/ethlance/ui/page/invoices/events.cljs +++ b/ui/src/ethlance/ui/page/invoices/events.cljs @@ -1,18 +1,16 @@ (ns ethlance.ui.page.invoices.events - (:require [district.ui.router.effects :as router.effects] - [ethlance.ui.event.utils :as event.utils] - [district.ui.notification.events :as notification.events] - [ethlance.ui.util.tokens :as util.tokens] - [district.ui.smart-contracts.queries :as contract-queries] - [ethlance.shared.utils :refer [eth->wei base58->hex]] - [district.ui.web3.queries :as web3-queries] - [district.ui.web3-tx.events :as web3-events] - [re-frame.core :as re])) + (:require + [district.ui.notification.events :as notification.events] + [district.ui.router.effects :as router.effects] + [district.ui.smart-contracts.queries :as contract-queries] + [district.ui.web3-tx.events :as web3-events] + [ethlance.shared.utils :refer [base58->hex]] + [re-frame.core :as re])) + ;; Page State (def state-key :page.invoices) -(def state-default - {}) + (defn initialize-page "Event FX Handler. Setup listener to dispatch an event when the page is active/visited." @@ -22,30 +20,28 @@ :name :route.invoice/index :dispatch []}]}) + ;; ;; Registered Events ;; -(def create-assoc-handler (partial event.utils/create-assoc-handler state-key)) - ;; TODO: switch based on dev environment (re/reg-event-fx :page.invoices/initialize-page initialize-page) + (re/reg-event-fx :page.invoices/pay - (fn [{:keys [db]} [_ invoice]] - (println ">>> page.invoices/pay" invoice) - (let [] - {:ipfs/call {:func "add" - :args [(js/Blob. [invoice])] - :on-success [::invoice-to-ipfs-success invoice] - :on-error [::invoice-to-ipfs-failure invoice]}}))) + (fn [_ [_ invoice]] + {:ipfs/call {:func "add" + :args [(js/Blob. [invoice])] + :on-success [::invoice-to-ipfs-success invoice] + :on-error [::invoice-to-ipfs-failure invoice]}})) + (re/reg-event-fx ::invoice-to-ipfs-success (fn [cofx [_event invoice ipfs-event]] - (let [invoice-fields (get-in cofx [:db state-key]) - contract-address (:job/id invoice) + (let [contract-address (:job/id invoice) tx-opts {:from (:payer invoice) :gas 10000000} invoice-id (:invoice-id invoice) ipfs-hash (-> ipfs-event :Hash base58->hex)] @@ -59,18 +55,17 @@ :on-tx-success [::send-invoice-tx-success invoice] :on-tx-error [::send-invoice-tx-error invoice]}]}))) + (re/reg-event-fx ::invoice-to-ipfs-failure (fn [_ _] {:dispatch [::notification.events/show "Error uploading invoice data to IPFS"]})) + (re/reg-event-fx ::tx-hash - (fn [db event] (println ">>> ethlance.ui.page.invoices.events :tx-hash" event))) + (fn [_ event] (println ">>> ethlance.ui.page.invoices.events :tx-hash" event))) -(re/reg-event-fx - ::web3-tx-localstorage - (fn [db event] (println ">>> ethlance.ui.page.invoices.events :web3-tx-localstorage" event))) (re/reg-event-fx ::send-invoice-tx-success @@ -78,7 +73,8 @@ {:fx [[:dispatch [:page.invoices/refetch-invoice]] [:dispatch [::notification.events/show "Invoice transaction processed sucessfully"]]]})) + (re/reg-event-fx ::send-invoice-tx-error - (fn [db event] + (fn [_ _] {:dispatch [::notification.events/show "Error with send invoice transaction"]})) diff --git a/ui/src/ethlance/ui/page/invoices/subscriptions.cljs b/ui/src/ethlance/ui/page/invoices/subscriptions.cljs index 44f7b739..7e838aaf 100644 --- a/ui/src/ethlance/ui/page/invoices/subscriptions.cljs +++ b/ui/src/ethlance/ui/page/invoices/subscriptions.cljs @@ -1,5 +1,7 @@ (ns ethlance.ui.page.invoices.subscriptions - (:require [ethlance.ui.page.invoices.events :as invoices.events] - [ethlance.ui.subscription.utils :as subscription.utils])) + (:require + [ethlance.ui.page.invoices.events :as invoices.events] + [ethlance.ui.subscription.utils :as subscription.utils])) + (def create-get-handler #(subscription.utils/create-get-handler invoices.events/state-key %)) diff --git a/ui/src/ethlance/ui/page/job_contract.cljs b/ui/src/ethlance/ui/page/job_contract.cljs index c3aeeb03..ebfbf330 100644 --- a/ui/src/ethlance/ui/page/job_contract.cljs +++ b/ui/src/ethlance/ui/page/job_contract.cljs @@ -1,24 +1,28 @@ (ns ethlance.ui.page.job-contract - (:require [district.parsers :refer [parse-int]] - [district.ui.component.page :refer [page]] - [district.ui.router.subs :as router.subs] - [ethlance.ui.util.tokens :as tokens] - [district.format :as format] - [ethlance.shared.utils :refer [ilike=]] - [district.ui.graphql.subs :as gql] - [ethlance.ui.component.button :refer [c-button c-button-label]] - [ethlance.ui.component.chat :refer [c-chat-log]] - [ethlance.ui.component.main-layout :refer [c-main-layout]] - [ethlance.ui.component.radio-select :refer [c-radio-secondary-element c-radio-select]] - [ethlance.ui.component.rating :refer [c-rating]] - [ethlance.ui.component.tabular-layout :refer [c-tabular-layout]] - [ethlance.ui.component.textarea-input :refer [c-textarea-input]] - [ethlance.ui.component.text-input :refer [c-text-input]] - [ethlance.ui.component.token-info :refer [c-token-info] :as token-info] - [ethlance.ui.util.navigation :as util.navigation] - [re-frame.core :as re])) - -(defn profile-link-handlers [user-type address] + (:require + [clojure.string] + [district.format :as format] + [district.parsers :refer [parse-int]] + [district.ui.component.page :refer [page]] + [district.ui.graphql.subs :as gql] + [district.ui.router.subs :as router.subs] + [ethlance.shared.utils :refer [ilike=]] + [ethlance.ui.component.button :refer [c-button c-button-label]] + [ethlance.ui.component.chat :refer [c-chat-log]] + [ethlance.ui.component.main-layout :refer [c-main-layout]] + [ethlance.ui.component.radio-select :refer [c-radio-secondary-element c-radio-select]] + [ethlance.ui.component.rating :refer [c-rating]] + [ethlance.ui.component.tabular-layout :refer [c-tabular-layout]] + [ethlance.ui.component.text-input :refer [c-text-input]] + [ethlance.ui.component.textarea-input :refer [c-textarea-input]] + [ethlance.ui.component.token-info :refer [c-token-info] :as token-info] + [ethlance.ui.util.navigation :as util.navigation] + [ethlance.ui.util.tokens :as tokens] + [re-frame.core :as re])) + + +(defn profile-link-handlers + [user-type address] {:on-click (util.navigation/create-handler {:route :route.user/profile :params {:address address} @@ -28,6 +32,7 @@ :params {:address address} :query {:tab user-type}})}) + (defn c-job-detail-table [{:keys [status funds employer candidate arbiter]}] [:div.job-detail-table @@ -47,22 +52,27 @@ [:div.name "Arbiter"] [:a.value (profile-link-handlers :arbiter (:address arbiter)) (:name arbiter)]]) + (defn c-header-profile [{:keys [title] :as details}] [:div.header-profile [:div.title "Job Contract"] [:a.job-name - (util.navigation/link-params {:route :route.job/detail :params {:id (:job/id details)}}) + (util.navigation/link-params {:route :route.job/detail :params {:id (:job/id details)}}) title] [:div.job-details [c-job-detail-table details]]]) -(defn c-information [text] + +(defn c-information + [text] [:div.feedback-input-container {:style {:opacity "50%"}} [:div {:style {:height "10em" :display "flex" :align-items "center" :justify-content "center"}} text]]) -(defn common-chat-fields [current-user entity field-fn details] + +(defn common-chat-fields + [current-user entity field-fn details] (let [direction (fn [viewer creator] (if (ilike= viewer creator) :sent :received)) @@ -80,17 +90,15 @@ detail-or-fn)) details)}))) -(defn invoice-detail [job-story amount-field invoice] - (let [amount (tokens/human-amount - (-> invoice :invoice/amount-requested) - (-> job-story :job :job/token-type)) - token-name (-> job-story :job :token-details :token-detail/name) - token-symbol (-> job-story :job :token-details :token-detail/symbol)] - [c-token-info (:invoice/amount-requested invoice) (get-in job-story [:job :token-details])])) - -(defn extract-chat-messages [job-story current-user] - (let [job-story-id (-> job-story :job-story :job-story/id) - add-to-details (fn [message additional-detail] + +(defn invoice-detail + [job-story amount-field invoice] + [c-token-info (amount-field invoice) (get-in job-story [:job :token-details])]) + + +(defn extract-chat-messages + [job-story current-user] + (let [add-to-details (fn [message additional-detail] (when message (assoc message :details (conj (:details message) additional-detail)))) format-proposal-amount (fn [job-story] @@ -100,16 +108,16 @@ invitation (common-fields :invitation-message ["Sent job invitation"]) invitation-accepted (common-fields :invitation-accepted-message ["Accepted invitation"]) proposal (-> (common-fields :proposal-message ["Sent job proposal"]) - (add-to-details ,,, (format-proposal-amount job-story))) + (add-to-details ,,, (format-proposal-amount job-story))) proposal-accepted (common-fields :proposal-accepted-message ["Accepted proposal"]) arbiter-feedback (map #(common-chat-fields current-user % :message ["Feedback for arbiter"]) (:job-story/arbiter-feedback job-story)) employer-feedback (map #(common-chat-fields current-user % :message ["Feedback for employer"]) - (:job-story/employer-feedback job-story)) + (:job-story/employer-feedback job-story)) candidate-feedback (map #(common-chat-fields current-user % :message ["Feedback for candidate"]) - (:job-story/candidate-feedback job-story)) + (:job-story/candidate-feedback job-story)) direct-messages (map #(common-chat-fields current-user % identity ["Direct message"]) - (:direct-messages job-story)) + (:direct-messages job-story)) invoice-link (fn [invoice] [:a (util.navigation/link-params {:route :route.invoice/index @@ -139,10 +147,12 @@ (flatten ,,,) (remove nil?) (sort-by :timestamp) - ; (reverse ,,,) ; Uncomment to see more recent messages at the top + ;; (reverse ,,,) ; Uncomment to see more recent messages at the top ))) -(defn c-chat [job-story-id] + +(defn c-chat + [job-story-id] (let [active-user (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) message-fields [:message/id :message/text @@ -165,9 +175,9 @@ [:job-story/arbiter-feedback [:message/id [:message message-fields]]] [:job-story/employer-feedback [:message/id - [:message message-fields]]] - [:job-story/candidate-feedback [:message/id [:message message-fields]]] + [:job-story/candidate-feedback [:message/id + [:message message-fields]]] [:direct-messages (into message-fields [:message/creator :direct-message/recipient])] [:job-story/invoices [[:items [:id :invoice/id @@ -182,16 +192,14 @@ messages-result (re/subscribe [::gql/query {:queries [messages-query]} {:refetch-on #{:page.job-contract/refetch-messages}}])] - (fn [job-story-id] - (let [chat-messages (extract-chat-messages (:job-story @messages-result) active-user)] - [c-chat-log chat-messages])))) + [c-chat-log (extract-chat-messages (:job-story @messages-result) active-user)])) -(defn c-feedback-panel [feedbacker feedback-recipients] + +(defn c-feedback-panel + [feedbacker] (let [job-story-id (re/subscribe [:page.job-contract/job-story-id]) feedback-text (re/subscribe [:page.job-contract/feedback-text]) feedback-rating (re/subscribe [:page.job-contract/feedback-rating]) - feedback-recipient (re/subscribe [:page.job-contract/feedback-recipient]) - user-fields [:user [:user/id :user/name]] query [:job-story {:job-story/id @job-story-id} [:job-story/id @@ -218,12 +226,13 @@ :candidate (get-in results [:job-story :candidate :user]) :arbiter (get-in results [:job-story :job :job/arbiter :user])} - normalized-feedback-users (map (fn [fb] [(:feedback/from-user-type fb) - (:feedback/to-user-type fb)]) + normalized-feedback-users (map (fn [fb] + [(:feedback/from-user-type fb) + (:feedback/to-user-type fb)]) feedbacks) - feedback-between? (fn [participants feedbacks from to] + feedback-between? (fn [feedbacks from to] (some #(= % [from to]) feedbacks)) - given-feedback? (partial feedback-between? participants normalized-feedback-users) + given-feedback? (partial feedback-between? normalized-feedback-users) feedback-receiver-role (case feedbacker :employer (cond @@ -268,7 +277,6 @@ all-feedbacks-done? (and (empty? open-invoices) (nil? feedback-receiver-role)) - new-feedback-recipients (dissoc participants feedbacker) next-feedback-receiver (get participants feedback-receiver-role)] [:div.feedback-input-container (when open-invoices? @@ -302,7 +310,9 @@ :to (:user/id next-feedback-receiver)}])} [c-button-label "Send Feedback"]]])])) -(defn c-direct-message [recipients] + +(defn c-direct-message + [recipients] (let [text (re/subscribe [:page.job-contract/message-text]) recipient (re/subscribe [:page.job-contract/message-recipient]) job-story-id (re/subscribe [:page.job-contract/job-story-id])] @@ -325,17 +335,12 @@ :job-story/id @job-story-id}])} [c-button-label "Send Message"]]])) -(defn c-accept-proposal-message [message-params] + +(defn c-accept-proposal-message + [message-params] (let [text (re/subscribe [:page.job-contract/accept-proposal-message-text]) proposal-data (assoc (select-keys message-params [:job/id :job-story/id :candidate :employer]) :text @text) - query [:job-story {:job-story/id (:job-story/id message-params)} - [:job-story/id - [:proposal-message [:message/id]] - :job-story/status]] - result (re/subscribe [::gql/query {:queries [query]} - {:refetch-on #{:page.job-contract/refetch-messages}}]) - can-accept? (= :proposal (:job-story/status message-params))] (if can-accept? [:div.message-input-container @@ -348,9 +353,11 @@ [c-button-label "Accept Proposal"]]] [:div.message-input-container - [c-information "No proposals to accept"]]))) + [c-information "No proposals to accept"]]))) + -(defn c-accept-invitation [message-params] +(defn c-accept-invitation + [message-params] (let [text (re/subscribe [:page.job-contract/accept-invitation-message-text]) job-story-id (re/subscribe [:page.job-contract/job-story-id])] [:div.message-input-container @@ -365,7 +372,9 @@ :job-story/id @job-story-id}])} [c-button-label "Accept Invitation"]]])) -(defn c-employer-options [message-params] + +(defn c-employer-options + [message-params] [c-tabular-layout {:key "employer-tabular-layout" :default-tab 1} @@ -377,17 +386,14 @@ [c-accept-proposal-message message-params] {:label "Leave Feedback"} - [c-feedback-panel :employer (select-keys message-params [:candidate :arbiter])]]) - -(defn c-candidate-options [{job-story-status :job-story/status ; TODO: take into account for limiting actions (feedback, disputes) - job-id :job/id - employer :employer - arbiter :arbiter - candidate :candidate - current-user-role :current-user-role - :as message-params}] - (let [active-user (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) - *active-page-params (re/subscribe [::router.subs/active-page-params]) + [c-feedback-panel :employer]]) + + +(defn c-candidate-options + [{job-story-status :job-story/status ; TODO: take into account for limiting actions (feedback, disputes) + job-id :job/id + :as message-params}] + (let [*active-page-params (re/subscribe [::router.subs/active-page-params]) job-story-id (-> @*active-page-params :job-story-id parse-int) invoice-query [:job-story {:job-story/id job-story-id} @@ -442,8 +448,7 @@ has-arbiter? (not (nil? (get-in @invoice-result [:job-story :job :job/arbiter]))) can-dispute? (and has-invoice? has-arbiter? - (nil? (get-in latest-unpaid-invoice [:dispute-raised-message]))) - dispute-available? (and has-invoice? can-dispute?) + (nil? (get latest-unpaid-invoice :dispute-raised-message))) dispute-unavailable-message (cond (not has-arbiter?) "This job doesn't yet have an arbiter so disputes can't be created." has-invoice? "You have already raised a dispute on your latest invoice. One invoice can only be disputed once." @@ -461,7 +466,7 @@ [:div.info-message "Click here to create new invoice for this job"] [c-button {:color :primary :on-click (util.navigation/create-handler {:route :route.invoice/new})} - [c-button-label "Go to create invoice"]]] + [c-button-label "Go to create invoice"]]] {:label "Send Message"} @@ -486,13 +491,15 @@ :invoice/id (:invoice/id latest-unpaid-invoice)}])} [c-button-label "Raise Dispute"]]] - ; else: can't dispute + ;; else: can't dispute [c-information dispute-unavailable-message]) {:label "Leave Feedback"} - [c-feedback-panel :candidate (select-keys message-params [:employer :arbiter])]])) + [c-feedback-panel :candidate]])) + -(defn c-arbiter-options [message-params] +(defn c-arbiter-options + [message-params] (let [*active-page-params (re/subscribe [::router.subs/active-page-params]) job-story-id (-> @*active-page-params :job-story-id parse-int) @@ -533,9 +540,10 @@ token-address (get-in @invoice-result [:job-story :job :job/token-address]) token-id (get-in @invoice-result [:job-story :job :job/token-id]) invoices (get-in @invoice-result [:job-story :job-story/invoices :items]) - dispute-open? (fn [invoice] (and - (not (nil? (:dispute-raised-message invoice))) - (nil? (:dispute-resolved-message invoice)))) + dispute-open? (fn [invoice] + (and + (not (nil? (:dispute-raised-message invoice))) + (nil? (:dispute-resolved-message invoice)))) latest-disputed-invoice (->> invoices (filter dispute-open? ,,,) (sort-by #(get-in % [:creation-message :message/date-created]) > ,,,) @@ -560,7 +568,7 @@ {:label "Send Message"} [c-direct-message (select-keys message-params [:candidate :employer])] - {:label "Resolve Dispute" :active? true} ;; TODO: conditionally show + {:label "Resolve Dispute" :active? true} ; TODO: conditionally show (if dispute-to-resolve? [:div.dispute-input-container [:div {:style {:gap "2em" :display "flex"}} @@ -601,15 +609,18 @@ :token-id token-id}])} [c-button-label "Resolve Dispute"]]] - ; Else + ;; Else [c-information "There are no invoices with unresolved disputes for this job story"]) {:label "Leave Feedback"} (if feedback-available-for-arbiter? - [c-feedback-panel :arbiter (select-keys message-params [:candidate :employer])] + [c-feedback-panel :arbiter] [c-information "Leaving feedback becomes available after employer or candidate have given their feedback (and thus terminated the job contract)"])])) -(defn c-guest-options []) + +(defn c-guest-options + []) + (defmethod page :route.job/contract [] (let [*active-page-params (re/subscribe [::router.subs/active-page-params])] @@ -659,10 +670,6 @@ (assoc ,,, :job-story/status (:job-story/status job-story)) (assoc ,,, :job-story/id job-story-id) (assoc ,,, :current-user-role current-user-role)) - - token-type (keyword (get-in job-story [:job :job/token-type])) - raw-amount (get-in job-story [:job :job/token-amount]) - human-amount (tokens/human-amount raw-amount token-type) profile {:title (get-in job-story [:job :job/title]) :job/id (:job/id job-story) :status (get-in job-story [:job-story/status]) diff --git a/ui/src/ethlance/ui/page/job_contract/events.cljs b/ui/src/ethlance/ui/page/job_contract/events.cljs index 5f161476..e8c99347 100644 --- a/ui/src/ethlance/ui/page/job_contract/events.cljs +++ b/ui/src/ethlance/ui/page/job_contract/events.cljs @@ -1,22 +1,22 @@ (ns ethlance.ui.page.job-contract.events - (:require [district.ui.router.effects :as router.effects] - [ethlance.ui.event.utils :as event.utils] - [ethlance.shared.utils :refer [eth->wei base58->hex]] - [district.ui.notification.events :as notification.events] - [district.ui.graphql.events :as gql-events] - [district.ui.web3-tx.events :as web3-events] - ["web3" :as w3] - [ethlance.shared.contract-constants :as contract-constants] - [district.ui.smart-contracts.queries :as contract-queries] - [district.ui.web3-accounts.queries :as accounts-queries] - [district.ui.web3.queries] - [district0x.re-frame.web3-fx] - [re-frame.core :as re])) + (:require + [district.ui.graphql.events :as gql-events] + [district.ui.notification.events :as notification.events] + [district.ui.router.effects :as router.effects] + [district.ui.smart-contracts.queries :as contract-queries] + [district.ui.web3-accounts.queries :as accounts-queries] + [district.ui.web3-tx.events :as web3-events] + [district.ui.web3.queries] + [district0x.re-frame.web3-fx] + [ethlance.shared.contract-constants :as contract-constants] + [ethlance.shared.utils :refer [base58->hex]] + [ethlance.ui.event.utils :as event.utils] + [re-frame.core :as re])) + ;; Page State (def state-key :page.job-contract) -(def state-default - {}) + (defn initialize-page "Event FX Handler. Setup listener to dispatch an event when the page is active/visited." @@ -26,23 +26,31 @@ :name :route.job/contract :dispatch []}]}) + ;; ;; Registered Events ;; (def create-assoc-handler (partial event.utils/create-assoc-handler state-key)) + + (defn create-logging-handler ([] (create-logging-handler "")) - ([text] (fn [db args] (println ">>> Received event in" state-key " " text " with args:" args)))) + ([text] (fn [_db args] (println ">>> Received event in" state-key " " text " with args:" args)))) + (re/reg-event-fx :page.job-contract/initialize-page initialize-page) (re/reg-event-fx :page.job-contract/set-message-text (create-assoc-handler :message-text)) (re/reg-event-fx :page.job-contract/set-message-recipient (create-assoc-handler :message-recipient)) + (re/reg-event-fx :page.job-contract/set-accept-proposal-message-text (create-assoc-handler :accept-proposal-message-text)) + + (re/reg-event-fx :page.job-contract/set-accept-invitation-message-text (create-assoc-handler :accept-invitation-message-text)) + (re/reg-event-fx :page.job-contract/set-dispute-text (create-assoc-handler :dispute-text)) (re/reg-event-fx :page.job-contract/set-dispute-candidate-percentage (create-assoc-handler :dispute-candidate-percentage)) @@ -54,7 +62,9 @@ (re/reg-event-fx :page.job-contract/tx-hash (create-logging-handler)) (re/reg-event-db ::dispute-tx-error (create-logging-handler)) -(defn clear-forms [db] + +(defn clear-forms + [db] (let [field-names [:message-text :message-recipient :dispute-text @@ -66,35 +76,38 @@ :accept-proposal-message-text]] (reduce (fn [acc field] (assoc-in acc [state-key field] nil)) db field-names))) + (defn send-feedback - [{:keys [db]} [_event-name params]] + [_cofx [_event-name params]] (let [job-story-id (:job-story/id params) text (:text params) rating (:rating params) to (:to params) mutation-params {:job-story/id job-story-id - :text text - :rating rating - :to to}] + :text text + :rating rating + :to to}] {:fx [[:dispatch [::gql-events/mutation {:queries [[:leave-feedback mutation-params]] :id :SendEmployerFeedbackMutation}]] [:dispatch [:page.job-contract/refetch-messages]] [:dispatch [:page.job-contract/clear-message-forms]]]})) + (defn send-message [{:keys [db]} [_event-name params]] (let [job-story-id (:job-story/id params) text (:text params) to (:to params) mutation-params {:job-story/id job-story-id - :text text - :to to}] + :text text + :to to}] {:db (clear-forms db) :fx [[:dispatch [::gql-events/mutation {:queries [[:send-message mutation-params]] :id :SendDirectMessageMutation}]] [:dispatch [:page.job-contract/refetch-messages]]]})) + (defn accept-invitation [{:keys [db]} [_event-name params]] (let [job-story-id (:job-story/id params) @@ -111,8 +124,10 @@ :id :SendDirectMessageMutation}]] [:dispatch [:page.job-contract/refetch-messages]]]})) -(defn raise-dispute [{:keys [db]} - [_ {invoice-id :invoice/id job-id :job/id job-story-id :job-story/id :as event}]] + +(defn raise-dispute + [{:keys [db]} + [_ {invoice-id :invoice/id job-id :job/id job-story-id :job-story/id}]] (let [ipfs-dispute {:message/text (get-in db [state-key :dispute-text]) :message/creator (accounts-queries/active-account db) :job/id job-id @@ -132,7 +147,7 @@ invoice-id (:invoice/id dispute-details) job-contract-address (:job/id dispute-details) tx-opts {:from creator :gas 10000000}] - {:dispatch [::web3-events/send-tx + {:dispatch [::web3-events/send-tx {:instance (contract-queries/instance db :job job-contract-address) :fn :raiseDispute :args [invoice-id ipfs-hash] @@ -142,9 +157,10 @@ :on-tx-success [::dispute-tx-success "Transaction to raise dispute processed successfully"] :on-tx-error [::dispute-tx-error]}]}))) + (re/reg-event-fx :page.job-contract/accept-proposal - (fn [{:keys [db]} [_ proposal-data]] + (fn [_cofx [_ proposal-data]] (let [to-ipfs {:candidate (:candidate proposal-data) :employer (:employer proposal-data) :job-story-message/type :accept-proposal @@ -155,7 +171,8 @@ {:ipfs/call {:func "add" :args [(js/Blob. [to-ipfs])] :on-success [:accept-proposal-to-ipfs-success to-ipfs] - :on-error [:accept-proposal-to-ipfs-failure to-ipfs]}}))) + :on-error [::accept-proposal-to-ipfs-failure to-ipfs]}}))) + (re/reg-event-fx :accept-proposal-to-ipfs-success @@ -165,7 +182,7 @@ job-contract-address (:job/id ipfs-accept) candidate (:candidate ipfs-accept) tx-opts {:from creator :gas 10000000}] - {:dispatch [::web3-events/send-tx + {:dispatch [::web3-events/send-tx {:instance (contract-queries/instance db :job job-contract-address) :fn :add-candidate :args [candidate ipfs-hash] @@ -175,11 +192,13 @@ :on-tx-success [::accept-proposal-tx-success] :on-tx-error [::accept-proposal-tx-failure]}]}))) + (re/reg-event-fx ::accept-proposal-to-ipfs-failure - (fn [{:keys [db]} event] + (fn [_cofx event] (println ">>> :invitation-to-ipfs-failure" event))) + (re/reg-event-fx ::accept-proposal-tx-success (fn [{:keys [db]} event] @@ -188,23 +207,27 @@ :fx [[:dispatch [:page.job-contract/refetch-messages]] [:dispatch [::notification.events/show "Transaction to accept proposal processed successfully"]]]})) + (re/reg-event-fx ::accept-proposal-tx-failure - (fn [{:keys [db]} event] + (fn [_cofx _event] {:dispatch [::notification.events/show "Error processing accept proposal transaction"]})) + (re/reg-event-fx :page.job-contract/clear-message-forms (fn [{:keys [db]}] {:db (clear-forms db)})) + (re/reg-event-fx ::dispute-tx-success - (fn [{:keys [db]} [event-name message]] + (fn [{:keys [db]} [_event-name message]] {:db (clear-forms db) :fx [[:dispatch [:page.job-contract/refetch-messages]] [:dispatch [::notification.events/show message]]]})) + (defn send-resolve-dispute-ipfs [{:keys [db]} [_ {invoice-id :invoice/id job-id :job/id @@ -220,7 +243,9 @@ :on-success [:page.job-contract/resolve-dispute-to-ipfs-success event] :on-error [:page.job-contract/dispute-to-ipfs-failure event]}})) -(defn send-resolve-dispute-tx [cofx [_event-name forwarded-event-data ipfs-data]] + +(defn send-resolve-dispute-tx + [cofx [_event-name forwarded-event-data ipfs-data]] (let [creator (accounts-queries/active-account (:db cofx)) job-address (:job/id forwarded-event-data) invoice-id (:invoice/id forwarded-event-data) @@ -257,6 +282,7 @@ :on-tx-success [::dispute-tx-success "Transaction to resolve dispute processed successfully"] :on-tx-error [::dispute-tx-error]}]})) + (re/reg-event-fx :page.job-contract/resolve-dispute send-resolve-dispute-ipfs) (re/reg-event-fx :page.job-contract/resolve-dispute-to-ipfs-success send-resolve-dispute-tx) (re/reg-event-fx :page.job-contract/send-message send-message) diff --git a/ui/src/ethlance/ui/page/job_contract/subscriptions.cljs b/ui/src/ethlance/ui/page/job_contract/subscriptions.cljs index 2cfadf72..271ec603 100644 --- a/ui/src/ethlance/ui/page/job_contract/subscriptions.cljs +++ b/ui/src/ethlance/ui/page/job_contract/subscriptions.cljs @@ -1,19 +1,21 @@ (ns ethlance.ui.page.job-contract.subscriptions - (:require [ethlance.ui.page.job-contract.events :as job-contract.events] - [ethlance.ui.subscription.utils :as subscription.utils] - [ethlance.shared.utils :refer [ilike=]] - [district.ui.graphql.queries :as ui-gql-queries] - [district.ui.router.queries :as ui-router-queries] - [district.parsers :refer [parse-int]] - [re-frame.core :as re])) + (:require + [district.parsers :refer [parse-int]] + [district.ui.router.queries :as ui-router-queries] + [ethlance.ui.page.job-contract.events :as job-contract.events] + [ethlance.ui.subscription.utils :as subscription.utils] + [re-frame.core :as re])) + (def create-get-handler #(subscription.utils/create-get-handler job-contract.events/state-key %)) + (re/reg-sub :page.job-contract/job-story-id (fn [db _] (-> (ui-router-queries/active-page-params db) :job-story-id parse-int))) + (re/reg-sub :page.job-contract/message-text (create-get-handler :message-text)) (re/reg-sub :page.job-contract/message-recipient (create-get-handler :message-recipient)) @@ -25,4 +27,3 @@ (re/reg-sub :page.job-contract/feedback-rating (create-get-handler :feedback-rating)) (re/reg-sub :page.job-contract/feedback-text (create-get-handler :feedback-text)) -(re/reg-sub :page.job-contract/feedback-recipient (create-get-handler :feedback-recipient)) diff --git a/ui/src/ethlance/ui/page/job_detail.cljs b/ui/src/ethlance/ui/page/job_detail.cljs index 71250428..ebe88ce2 100644 --- a/ui/src/ethlance/ui/page/job_detail.cljs +++ b/ui/src/ethlance/ui/page/job_detail.cljs @@ -1,43 +1,42 @@ (ns ethlance.ui.page.job-detail - (:require [district.ui.component.page :refer [page]] - [district.format :as format] - [ethlance.ui.component.search-input :refer [c-chip-search-input]] - [ethlance.ui.component.button :refer [c-button c-button-label]] - [ethlance.ui.component.info-message :refer [c-info-message]] - [ethlance.ui.component.token-info :refer [c-token-info]] - [ethlance.ui.component.carousel - :refer - [c-carousel c-carousel-old c-feedback-slide]] - [ethlance.ui.component.scrollable :refer [c-scrollable]] - [ethlance.ui.component.circle-button :refer [c-circle-icon-button]] - [ethlance.ui.component.main-layout :refer [c-main-layout]] - [ethlance.ui.component.profile-image :refer [c-profile-image]] - [ethlance.ui.component.rating :refer [c-rating]] - [ethlance.ui.component.select-input :refer [c-select-input]] - [ethlance.ui.component.table :refer [c-table]] - [ethlance.ui.component.tag :refer [c-tag c-tag-label]] - [ethlance.ui.component.text-input :refer [c-text-input]] - [ethlance.ui.component.textarea-input :refer [c-textarea-input]] - [ethlance.ui.component.pagination :as pagination] - [ethlance.ui.component.token-amount-input :refer [c-token-amount-input]] - [district.ui.graphql.subs :as gql] - [ethlance.ui.util.component :refer [evt]] - [ethlance.ui.util.navigation :refer [link-params] :as util.navigation] - [ethlance.ui.util.tokens :as token-utils] - [ethlance.ui.util.job :as util.job] - [ethlance.shared.utils :refer [millis->relative-time ilike!= ilike=]] - [ethlance.shared.utils :as shared-utils] - [ethlance.ui.util.tokens :as util.tokens] - [re-frame.core :as re])) - -(defn c-token-values [{:keys [token-type token-amount token-address token-id disabled? token-symbol token-name] :as opts}] + (:require + [clojure.set] + [clojure.string] + [district.format :as format] + [district.ui.component.page :refer [page]] + [district.ui.graphql.subs :as gql] + [ethlance.shared.utils :refer [ilike!= ilike=]] + [ethlance.ui.component.button :refer [c-button c-button-label]] + [ethlance.ui.component.carousel :refer [c-carousel-old c-feedback-slide]] + [ethlance.ui.component.info-message :refer [c-info-message]] + [ethlance.ui.component.main-layout :refer [c-main-layout]] + [ethlance.ui.component.pagination :as pagination] + [ethlance.ui.component.profile-image :refer [c-profile-image]] + [ethlance.ui.component.rating :refer [c-rating]] + [ethlance.ui.component.scrollable :refer [c-scrollable]] + [ethlance.ui.component.search-input :refer [c-chip-search-input]] + [ethlance.ui.component.table :refer [c-table]] + [ethlance.ui.component.tag :refer [c-tag c-tag-label]] + [ethlance.ui.component.text-input :refer [c-text-input]] + [ethlance.ui.component.textarea-input :refer [c-textarea-input]] + [ethlance.ui.component.token-amount-input :refer [c-token-amount-input]] + [ethlance.ui.component.token-info :refer [c-token-info]] + [ethlance.ui.util.component :refer [>evt]] + [ethlance.ui.util.job :as util.job] + [ethlance.ui.util.navigation :refer [link-params] :as util.navigation] + [ethlance.ui.util.tokens :as token-utils] + [re-frame.core :as re])) + + +(defn c-token-values + [{:keys [token-type token-amount token-address token-id disabled? token-symbol token-name]}] (let [token-type (keyword token-type) step (if (= token-type :eth) 0.001 1)] (cond (= :erc721 token-type) [:div "The payment will be NFT (ERC721)"] - (#{:eth :erc1155 :erc20} token-type) + (#{:eth :erc1155 :erc20} token-type) [:div.amount-input [c-text-input {:placeholder "Token amount" @@ -48,10 +47,12 @@ :value token-amount :on-change #(re/dispatch [:page.job-detail/set-proposal-token-amount (js/parseFloat %)])}] [:a {:href (token-utils/address->token-info-url token-address) :target "_blank"} - [:label token-symbol] - [:label (str "(" (or token-name (name token-type)) ")")]]]))) + [:label token-symbol] + [:label (str "(" (or token-name (name token-type)) ")")]]]))) -(defn c-invoice-listing [contract-address] + +(defn c-invoice-listing + [contract-address] (let [invoices-query [:job {:job/id contract-address} [[:token-details [:token-detail/id @@ -79,21 +80,23 @@ job-token-symbol (get-in result [:job :token-details :token-detail/symbol]) invoices (map (fn [invoice] {:name (get-in invoice [:creation-message :creator :user/name]) - :amount (str (util.tokens/human-amount (get-in invoice [:invoice/amount-requested]) job-token-symbol) " " job-token-symbol) + :amount (str (token-utils/human-amount (get invoice :invoice/amount-requested) job-token-symbol) " " job-token-symbol) :timestamp (format/time-ago (new js/Date (get-in invoice [:creation-message :message/date-created]))) - :status (get-in invoice [:invoice/status])}) + :status (get invoice :invoice/status)}) (-> result :job :invoices :items))] [:div.invoice-listing - [:div.label "Invoices"] - (into [c-table {:headers ["Candidate" "Amount" "Created" "Status"]}] - (map (fn [invoice] - [[:span (:name invoice)] - [:span (:amount invoice)] - [:span (:timestamp invoice)] - [:span (:status invoice)]]) - invoices))])) - -(defn c-employer-feedback [contract-address] + [:div.label "Invoices"] + (into [c-table {:headers ["Candidate" "Amount" "Created" "Status"]}] + (map (fn [invoice] + [[:span (:name invoice)] + [:span (:amount invoice)] + [:span (:timestamp invoice)] + [:span (:status invoice)]]) + invoices))])) + + +(defn c-employer-feedback + [contract-address] (let [feedback-query [:job {:job/id contract-address} [[:job/employer [[:employer/feedback @@ -114,21 +117,21 @@ :image-url (-> feedback :feedback/from-user :user/profile-image)}) feedback-raw)] [:div.feedback-listing - [:div.label "Feedback for employer"] - (if (empty? feedback) - [:label "No feedback yet for this employer"] - (into [c-carousel-old {}] (map #(c-feedback-slide %) feedback)))])) + [:div.label "Feedback for employer"] + (if (empty? feedback) + [:label "No feedback yet for this employer"] + (into [c-carousel-old {}] (map #(c-feedback-slide %) feedback)))])) + -(defn c-proposals-section [job] +(defn c-proposals-section + [job] (let [contract-address (:id @(re/subscribe [:district.ui.router.subs/active-page-params])) active-user (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) - raw-token-amount (get-in job [:job/token-amount]) *bid-option (:job/bid-option job) - *job-token-type (get-in job [:job/token-type] "") - *job-token-id (get-in job [:job/token-id]) - *job-token-address (get-in job [:job/token-address]) - *job-token-amount (util.tokens/human-amount raw-token-amount *job-token-type) + *job-token-type (get job :job/token-type "") + *job-token-id (get job :job/token-id) + *job-token-address (get job :job/token-address) *token-detail-name (get-in job [:token-details :token-detail/name]) *token-detail-symbol (get-in job [:token-details :token-detail/symbol]) *proposal-token-amount (re/subscribe [:page.job-detail/proposal-token-amount]) @@ -149,7 +152,7 @@ user-query [:user {:user/id active-user} - [:user/is-registered-candidate]] + [:user/is-registered-candidate]] arbitrations-query [:job {:job/id contract-address} [[:arbitrations [[:items @@ -161,58 +164,60 @@ (not (some #(ilike= active-user (:user/id %)) (get-in result [:job :arbitrations :items]))))] [:div.proposal-listing [:div.label "Proposals"] - [c-scrollable - {:forceVisible true :autoHide false} - (into [c-table {:headers ["" "Candidate" "Rate" "Created" "Status"]}] - (map (fn [proposal] - [[:span (if (:current-user? proposal) "⭐" "")] - [:a (util.navigation/link-params - {:route :route.job/contract - :params {:job-story-id (:job-story/id proposal)}}) - [:span (:candidate-name proposal)]] - [c-token-info (:rate proposal) (:token-details job)] - [:span (format/time-ago (new js/Date (:created-at proposal)))] ; TODO: remove new js/Date after switching to district.ui.graphql that converts Date GQL type automatically - [:span (:status proposal)]]) - @proposals))] - [pagination/c-pagination-ends - {:total-count proposal-total-count - :limit proposal-limit - :offset proposal-offset - :set-offset-event :page.job-detail/set-proposal-offset}] - - (if candidate-role? - [:div.proposal-form - [:div.label "Send Proposal"] - [c-token-values {:disabled? (not can-send-proposals?) - :token-type *job-token-type - :token-amount (if my-proposal? (:rate @my-proposal) @*proposal-token-amount) - :token-id *job-token-id - :token-address *job-token-address - :token-name *token-detail-name - :token-symbol *token-detail-symbol}] - [:label "The amount is for payment type: " (str *bid-option)] - [:div.description-input - [c-textarea-input - {:disabled (not can-send-proposals?) - :placeholder "Proposal Description" - :value (if my-proposal? (:message @my-proposal) @*proposal-text) - :on-change #(re/dispatch [:page.job-detail/set-proposal-text %])}]] - - (if my-proposal-withdrawable? - [c-button {:color :warning :on-click (fn [] (>evt [:page.job-proposal/remove my-job-story-id])) - :size :small} - [c-button-label "Remove"]]) - (if (not my-proposal?) - [c-button {:style (when (not can-send-proposals?) {:background :gray}) - :on-click (fn [] - (when can-send-proposals? - (>evt [:page.job-proposal/send contract-address *job-token-type]))) - :size :small} - [c-button-label "Send"]])] - - [:div.proposal-form])])) - -(defn c-participant-info [participant-type user-id] + [c-scrollable + {:forceVisible true :autoHide false} + (into [c-table {:headers ["" "Candidate" "Rate" "Created" "Status"]}] + (map (fn [proposal] + [[:span (if (:current-user? proposal) "⭐" "")] + [:a (util.navigation/link-params + {:route :route.job/contract + :params {:job-story-id (:job-story/id proposal)}}) + [:span (:candidate-name proposal)]] + [c-token-info (:rate proposal) (:token-details job)] + [:span (format/time-ago (new js/Date (:created-at proposal)))] ; TODO: remove new js/Date after switching to district.ui.graphql that converts Date GQL type automatically + [:span (:status proposal)]]) + @proposals))] + [pagination/c-pagination-ends + {:total-count proposal-total-count + :limit proposal-limit + :offset proposal-offset + :set-offset-event :page.job-detail/set-proposal-offset}] + + (if candidate-role? + [:div.proposal-form + [:div.label "Send Proposal"] + [c-token-values {:disabled? (not can-send-proposals?) + :token-type *job-token-type + :token-amount (if my-proposal? (:rate @my-proposal) @*proposal-token-amount) + :token-id *job-token-id + :token-address *job-token-address + :token-name *token-detail-name + :token-symbol *token-detail-symbol}] + [:label "The amount is for payment type: " (str *bid-option)] + [:div.description-input + [c-textarea-input + {:disabled (not can-send-proposals?) + :placeholder "Proposal Description" + :value (if my-proposal? (:message @my-proposal) @*proposal-text) + :on-change #(re/dispatch [:page.job-detail/set-proposal-text %])}]] + + (when my-proposal-withdrawable? + [c-button {:color :warning :on-click (fn [] (>evt [:page.job-proposal/remove my-job-story-id])) + :size :small} + [c-button-label "Remove"]]) + (when (not my-proposal?) + [c-button {:style (when (not can-send-proposals?) {:background :gray}) + :on-click (fn [] + (when can-send-proposals? + (>evt [:page.job-proposal/send contract-address *job-token-type]))) + :size :small} + [c-button-label "Send"]])] + + [:div.proposal-form])])) + + +(defn c-participant-info + [participant-type user-id] (let [rating-kw (keyword participant-type :rating) query [participant-type {:user/id user-id} [rating-kw @@ -223,59 +228,62 @@ :user/name :user/profile-image]]]] results (if user-id - @(re/subscribe [::gql/query {:queries [query]}]) - {}) + @(re/subscribe [::gql/query {:queries [query]}]) + {}) *participant-name (get-in results [participant-type :user :user/name]) *participant-address (get-in results [participant-type :user/id]) *participant-rating (get-in results [participant-type rating-kw]) *participant-country (get-in results [participant-type :user :user/country]) *participant-profile-image (get-in results [participant-type :user :user/profile-image])] [:a.arbiter-detail (util.navigation/link-params - {:route :route.user/profile - :params {:address *participant-address} - :query {:tab participant-type}}) + {:route :route.user/profile + :params {:address *participant-address} + :query {:tab participant-type}}) [:div.header (clojure.string/capitalize (name participant-type))] [:div.profile-image [c-profile-image {:src *participant-profile-image}]] [:div.name *participant-name] [:div.rating [c-rating {:rating *participant-rating}]] [:div.location *participant-country]])) -(defn c-accept-arbiter-quote [] + +(defn c-accept-arbiter-quote + [] (let [arbitration-to-accept @(re/subscribe [:page.job-detail/arbitration-to-accept]) - job-address (get-in arbitration-to-accept [:job/id]) + job-address (get arbitration-to-accept :job/id) employer-address (get-in arbitration-to-accept [:job :job/employer-address]) - arbiter-address (get-in arbitration-to-accept [:arbiter :user/id]) arbiter-to-be-assigned? (= "quote-set" (:arbitration/status arbitration-to-accept))] [:div.proposal-form [:div.label "Accept arbiter quote"] - [:div.amount-input - [:div.label "Arbiter: "] - [c-text-input - {:placeholder "" - :disabled true - :value (get-in arbitration-to-accept [:arbiter :user :user/name])}]] - [:div.amount-input - [:div.label "Amount: "] - [c-text-input - {:placeholder "" - :disabled true - :value (token-utils/human-amount (get-in arbitration-to-accept [:arbitration/fee]) :eth)}] - [:label "ETH (Ether)"]] - - (when arbiter-to-be-assigned? - [c-button {:style (when (nil? arbitration-to-accept) {:background :gray}) - :size :small - :on-click (fn [] - (when arbitration-to-accept - (>evt [:page.job-detail/accept-quote-for-arbitration - {:job/id job-address - :employer employer-address - :user/id (get-in arbitration-to-accept [:arbiter :user/id]) - :job-arbiter/fee (:arbitration/fee arbitration-to-accept) - :job-arbiter/fee-currency-id :ETH}])))} - [c-button-label "Accept"]])])) - -(defn c-invite-arbiters [job-address] + [:div.amount-input + [:div.label "Arbiter: "] + [c-text-input + {:placeholder "" + :disabled true + :value (get-in arbitration-to-accept [:arbiter :user :user/name])}]] + [:div.amount-input + [:div.label "Amount: "] + [c-text-input + {:placeholder "" + :disabled true + :value (token-utils/human-amount (get arbitration-to-accept :arbitration/fee) :eth)}] + [:label "ETH (Ether)"]] + + (when arbiter-to-be-assigned? + [c-button {:style (when (nil? arbitration-to-accept) {:background :gray}) + :size :small + :on-click (fn [] + (when arbitration-to-accept + (>evt [:page.job-detail/accept-quote-for-arbitration + {:job/id job-address + :employer employer-address + :user/id (get-in arbitration-to-accept [:arbiter :user/id]) + :job-arbiter/fee (:arbitration/fee arbitration-to-accept) + :job-arbiter/fee-currency-id :ETH}])))} + [c-button-label "Accept"]])])) + + +(defn c-invite-arbiters + [job-address] (let [selected-arbiters (re/subscribe [:page.job-detail/selected-arbiters]) arbiter-fields [:arbiter/rating [:arbiter/feedback [:total-count]] @@ -293,15 +301,13 @@ {:refetch-on #{:page.job-detail/arbitrations-updated}}]) all-arbiters (get-in search-result [:arbiter-search :items]) - already-added (map #(get-in % [:arbiter]) (get-in search-result [:job :arbitrations :items])) + already-added (map #(get % :arbiter) (get-in search-result [:job :arbitrations :items])) uninvited-arbiters (clojure.set/difference (set all-arbiters) (set already-added)) employer-address (get-in search-result [:job :job/employer-address]) - arbiter-address-fn (fn [arbiter] (get-in arbiter [:user :user/id])) - nothing-added? (empty? @selected-arbiters) arbiter-info-fn (fn [arbiter] - (clojure.string.join + (clojure.string/join [(get-in arbiter [:user :user/name]) " " (apply str (repeat (:arbiter/rating arbiter) "⭐")) @@ -322,25 +328,27 @@ :allow-custom-chips? false :placeholder "Searh arbiter by name"}] - [c-button - {:style (when (or nothing-added? @invite-arbiters-tx-in-progress?) {:background :gray}) - :size :small - :on-click (fn [] - (when (and - (not (empty? @selected-arbiters)) - (not @invite-arbiters-tx-in-progress?)) - (>evt [:page.job-detail/invite-arbiters - {:job/id job-address - :employer employer-address - :arbiters (map arbiter-address-fn @selected-arbiters)}])))} - [c-button-label "Invite"]]])) - -(defn c-set-arbiter-quote [arbitration-by-current-user] + [c-button + {:style (when (or nothing-added? @invite-arbiters-tx-in-progress?) {:background :gray}) + :size :small + :on-click (fn [] + (when (and + (not (empty? @selected-arbiters)) + (not @invite-arbiters-tx-in-progress?)) + (>evt [:page.job-detail/invite-arbiters + {:job/id job-address + :employer employer-address + :arbiters (map arbiter-address-fn @selected-arbiters)}])))} + [c-button-label "Invite"]]])) + + +(defn c-set-arbiter-quote + [arbitration-by-current-user] (let [token-amount (re/subscribe [:page.job-detail/arbitration-token-amount]) token-amount-usd (re/subscribe [:page.job-detail/arbitration-token-amount-usd]) quote-set? (= "quote-set" (:arbitration/status arbitration-by-current-user)) invited? (= "invited" (:arbitration/status arbitration-by-current-user)) - job-address (get-in arbitration-by-current-user [:job/id]) + job-address (get arbitration-by-current-user :job/id) active-user (get-in arbitration-by-current-user [:arbiter :user/id])] (if invited? [:div.proposal-form @@ -363,15 +371,15 @@ :on-change #(re/dispatch [:page.job-detail/set-arbitration-token-amount-usd (js/parseFloat %)])}] [:label "USD"]] - [c-button - {:style (when quote-set? {:background :gray}) - :size :small - :on-click (fn [] - (when invited? (>evt [:page.job-detail/set-quote-for-arbitration - {:job/id job-address - :user/id active-user - :job-arbiter/fee @token-amount - :job-arbiter/fee-currency-id :ETH}])))} + [c-button + {:style (when quote-set? {:background :gray}) + :size :small + :on-click (fn [] + (when invited? (>evt [:page.job-detail/set-quote-for-arbitration + {:job/id job-address + :user/id active-user + :job-arbiter/fee @token-amount + :job-arbiter/fee-currency-id :ETH}])))} [c-button-label "Send"]]] [:div.proposal-form @@ -379,7 +387,9 @@ "You already set the quote for arbitration. Now the employer must accept, which will transfer the quoted amount to you"]]))) -(defn c-arbitrations-section [job-address] + +(defn c-arbitrations-section + [job-address] (let [limit @(re/subscribe [:page.job-detail/arbitrations-limit]) offset @(re/subscribe [:page.job-detail/arbitrations-offset]) active-user (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) @@ -426,86 +436,86 @@ arbitration-to-accept @(re/subscribe [:page.job-detail/arbitration-to-accept]) job-arbiter (get-in result [:job :job/arbiter :user/id]) job-arbitration (first (filter #(ilike= job-arbiter (get-in % [:arbiter :user/id])) arbitrations)) - arbiter-quoted-amount (:arbitration/fee job-arbitration) - employer-address (get-in result [:job :job/employer-address]) arbiter-accepted? (= "accepted" (:arbitration/status job-arbitration)) arbiter-selected? (not (nil? arbitration-to-accept)) show-invite-arbiters? @(re/subscribe [:page.job-detail/show-invite-arbiters]) arbiter-idle? @(re/subscribe [:page.job-detail/job-arbiter-idle])] - ; TODO: remove after figuring out why at events/initialize DB doesn't have web3 and contract-instance - ; setTimeout is because dispatching at first render web3 ist not ready and causes errors + ;; TODO: remove after figuring out why at events/initialize DB doesn't have web3 and contract-instance + ;; setTimeout is because dispatching at first render web3 ist not ready and causes errors (js/setTimeout #(re/dispatch [:page.job-detail/fetch-job-arbiter-status]) 1000) [:div.proposal-listing [:div.label "Arbitrations"] - [c-scrollable - {:forceVisible true :autoHide false} - (into [c-table {:headers ["" "Arbiter" "Rate" "Accepted at" "Status" ""]}] - (map (fn [arbitration] - [[:span (cond - (ilike= active-user (get-in arbitration [:arbiter :user/id])) - "⭐" - (= "accepted" (get-in arbitration [:arbitration/status])) - "✅")] - [:span (get-in arbitration [:arbiter :user :user/name])] - [c-token-info (:arbitration/fee arbitration) (:fee-token-details arbitration)] - [:span (when (:arbitration/date-arbiter-accepted arbitration) - (format/time-ago (new js/Date (:arbitration/date-arbiter-accepted arbitration))))] - [:span (:arbitration/status arbitration)] - (cond - (and (= "quote-set" (:arbitration/status arbitration)) - (or (not arbiter-accepted?) arbiter-idle?) - (= viewer-role :employer)) - [:div.button.primary.active.small - {:style {:height "2em" :background (if (ilike= arbitration-to-accept arbitration) - "orange" - "")} - :on-click #(re/dispatch [:page.job-detail/set-arbitration-to-accept arbitration])} - [:div.button-label (if (ilike= arbitration-to-accept arbitration) - "Selected" - "Select")]] - (= "invited" (:arbitration/status arbitration)) - [:div "(arbiter to set quote)"] - - (= "quote-set" (:arbitration/status arbitration)) - [:div "(employer to accept)"] - - :else - [:div ""])]) - arbitrations))] - - [pagination/c-pagination-ends - {:total-count total-count - :limit limit - :offset offset - :set-offset-event :page.job-detail/set-arbitrations-offset}] - - (case viewer-role - :invited-arbiter - [c-set-arbiter-quote arbitration-by-current-user] - - :employer - (if (and arbiter-accepted? (not show-invite-arbiters?)) - [:div.proposal-form.profiles - [c-participant-info :arbiter job-arbiter] ; TODO: Fix styling - (when arbiter-idle? - [c-info-message - "Idle arbiter" - [:div - "This arbiter has unresolved dispute for more than 30 days. You can accept new one to replace him" - [:div.button.primary.active - {:on-click (fn [] (re/dispatch [:page.job-detail/set-show-invite-arbiters true]))} - [:div.button-label "Invite arbiters"]]]])] - - (if (and arbiter-selected? - (not show-invite-arbiters?)) - [c-accept-arbiter-quote] - [c-invite-arbiters job-address])) - - :other - [:div.proposal-form])])) - -(defn c-add-funds [contract-address token-id token-details] + [c-scrollable + {:forceVisible true :autoHide false} + (into [c-table {:headers ["" "Arbiter" "Rate" "Accepted at" "Status" ""]}] + (map (fn [arbitration] + [[:span (cond + (ilike= active-user (get-in arbitration [:arbiter :user/id])) + "⭐" + (= "accepted" (get arbitration :arbitration/status)) + "✅")] + [:span (get-in arbitration [:arbiter :user :user/name])] + [c-token-info (:arbitration/fee arbitration) (:fee-token-details arbitration)] + [:span (when (:arbitration/date-arbiter-accepted arbitration) + (format/time-ago (new js/Date (:arbitration/date-arbiter-accepted arbitration))))] + [:span (:arbitration/status arbitration)] + (cond + (and (= "quote-set" (:arbitration/status arbitration)) + (or (not arbiter-accepted?) arbiter-idle?) + (= viewer-role :employer)) + [:div.button.primary.active.small + {:style {:height "2em" :background (if (ilike= arbitration-to-accept arbitration) + "orange" + "")} + :on-click #(re/dispatch [:page.job-detail/set-arbitration-to-accept arbitration])} + [:div.button-label (if (ilike= arbitration-to-accept arbitration) + "Selected" + "Select")]] + (= "invited" (:arbitration/status arbitration)) + [:div "(arbiter to set quote)"] + + (= "quote-set" (:arbitration/status arbitration)) + [:div "(employer to accept)"] + + :else + [:div ""])]) + arbitrations))] + + [pagination/c-pagination-ends + {:total-count total-count + :limit limit + :offset offset + :set-offset-event :page.job-detail/set-arbitrations-offset}] + + (case viewer-role + :invited-arbiter + [c-set-arbiter-quote arbitration-by-current-user] + + :employer + (if (and arbiter-accepted? (not show-invite-arbiters?)) + [:div.proposal-form.profiles + [c-participant-info :arbiter job-arbiter] ; TODO: Fix styling + (when arbiter-idle? + [c-info-message + "Idle arbiter" + [:div + "This arbiter has unresolved dispute for more than 30 days. You can accept new one to replace him" + [:div.button.primary.active + {:on-click (fn [] (re/dispatch [:page.job-detail/set-show-invite-arbiters true]))} + [:div.button-label "Invite arbiters"]]]])] + + (if (and arbiter-selected? + (not show-invite-arbiters?)) + [c-accept-arbiter-quote] + [c-invite-arbiters job-address])) + + :other + [:div.proposal-form])])) + + +(defn c-add-funds + [contract-address token-id token-details] (let [amount @(re/subscribe [:page.job-detail/add-funds-amount]) adding-funds? (re/subscribe [:page.job-detail/adding-funds?]) add-funds-tx-in-progress? (re/subscribe [:page.job-detail/add-funds-tx-in-progress?])] @@ -533,14 +543,15 @@ :size :small} [c-button-label "Add funds"]]]))) -(defn c-job-info-section [results] + +(defn c-job-info-section + [results] (let [active-user (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) page-params (re/subscribe [:district.ui.router.subs/active-page-params]) contract-address (:id @page-params) *title (:job/title results) *description (:job/description results) *sub-title (:job/category results) - *experience (:job/required-experience-level results) job-creation-time (.fromTimestamp goog.date.DateTime (:job/date-created results)) *posted-time-relative (str "Posted " (format/time-ago job-creation-time)) *posted-time-absolute (str "(" (format/format-local-date job-creation-time) ")") @@ -562,9 +573,9 @@ employer-id (get-in results [:job/employer :user/id]) arbiter-id (get-in results [:job/arbiter :user/id]) - has-accepted-arbiter? (not (nil? (get-in results [:job/arbiter]))) - token-details (get-in results [:token-details]) - job-balance (get-in results [:balance]) + has-accepted-arbiter? (not (nil? (get results :job/arbiter))) + token-details (get results :token-details) + job-balance (get results :balance) invoices (get-in results [:invoices :items]) unpaid-invoices (filter #(= "created" (:invoice/status %)) invoices) @@ -578,52 +589,53 @@ can-end-job? (not (or has-unpaid-invoices? has-unresolved-disputes?)) end-job-tx-in-progress? (re/subscribe [:page.job-detail/end-job-tx-in-progress?])] [:div.header - [:div.main - [:div.title *title] - [:div.sub-title *sub-title] ; TODO: where this comes from - [:div.description *description] - [:div.label "Required Skills"] - [:div.skill-listing - (for [skill *required-skills] [c-tag {:key skill} [c-tag-label skill]])] - [:div.ticket-listing - [:div.ticket - [:div.label "Available Funds"] - [c-token-info job-balance token-details]]] - - (when job-ongoing? [c-add-funds contract-address (:job/token-id results) token-details]) - [:div.profiles - [c-participant-info :employer employer-id] - (when has-accepted-arbiter? [c-participant-info :arbiter arbiter-id])]] - [:div.side - [:div.label *posted-time-relative] - [:div.label *posted-time-absolute] - (for [tag-text *job-info-tags] [c-tag {:key tag-text} [c-tag-label tag-text]]) - (when show-end-job? - [:div - [:div.button.primary.active - {:style (when (or @end-job-tx-in-progress? has-unpaid-invoices? has-unresolved-disputes?) {:background :gray}) - :disabled @end-job-tx-in-progress? - :on-click (fn [] - (when (and can-end-job? (not @end-job-tx-in-progress?)) - (re/dispatch [:page.job-detail/end-job {:job/id contract-address :employer employer-id}])))} - [:div.button-label "End job"]] - (when has-unpaid-invoices? - [c-info-message "Job has unpaid invoices" - [:ul - (for [invoice unpaid-invoices] - ^{:key (:id invoice)} - [:li [:a (link-params {:route :route.invoice/index - :params {:invoice-id (:invoice/id invoice) :job-id (:job/id invoice)}}) - (str "Invoice #" (:invoice/id invoice))]])]]) - (when has-unresolved-disputes? - [c-info-message "Job has unresolved disputes" - [:ul - (for [invoice unresolved-disputes] - ^{:key (:id invoice)} - [:li [:a (link-params {:route :route.job/contract - :params {:job-story-id (:job-story/id invoice)}}) - (str "Dispute #" (:invoice/id invoice))]])]]) - (when can-end-job? [:div "Ending the job will return all remaining funds to who contributed them"])])]])) + [:div.main + [:div.title *title] + [:div.sub-title *sub-title] ; TODO: where this comes from + [:div.description {:style {:white-space "pre"}} *description] + [:div.label "Required Skills"] + [:div.skill-listing + (for [skill *required-skills] [c-tag {:key skill} [c-tag-label skill]])] + [:div.ticket-listing + [:div.ticket + [:div.label "Available Funds"] + [c-token-info job-balance token-details]]] + + (when job-ongoing? [c-add-funds contract-address (:job/token-id results) token-details]) + [:div.profiles + [c-participant-info :employer employer-id] + (when has-accepted-arbiter? [c-participant-info :arbiter arbiter-id])]] + [:div.side + [:div.label *posted-time-relative] + [:div.label *posted-time-absolute] + (for [tag-text *job-info-tags] [c-tag {:key tag-text} [c-tag-label tag-text]]) + (when show-end-job? + [:div + [:div.button.primary.active + {:style (when (or @end-job-tx-in-progress? has-unpaid-invoices? has-unresolved-disputes?) {:background :gray}) + :disabled @end-job-tx-in-progress? + :on-click (fn [] + (when (and can-end-job? (not @end-job-tx-in-progress?)) + (re/dispatch [:page.job-detail/end-job {:job/id contract-address :employer employer-id}])))} + [:div.button-label "End job"]] + (when has-unpaid-invoices? + [c-info-message "Job has unpaid invoices" + [:ul + (for [invoice unpaid-invoices] + ^{:key (:id invoice)} + [:li [:a (link-params {:route :route.invoice/index + :params {:invoice-id (:invoice/id invoice) :job-id (:job/id invoice)}}) + (str "Invoice #" (:invoice/id invoice))]])]]) + (when has-unresolved-disputes? + [c-info-message "Job has unresolved disputes" + [:ul + (for [invoice unresolved-disputes] + ^{:key (:id invoice)} + [:li [:a (link-params {:route :route.job/contract + :params {:job-story-id (:job-story/id invoice)}}) + (str "Dispute #" (:invoice/id invoice))]])]]) + (when can-end-job? [:div "Ending the job will return all remaining funds to who contributed them"])])]])) + (defmethod page :route.job/detail [] (fn [] diff --git a/ui/src/ethlance/ui/page/job_detail/events.cljs b/ui/src/ethlance/ui/page/job_detail/events.cljs index 6e67fc6e..dd7bb28a 100644 --- a/ui/src/ethlance/ui/page/job_detail/events.cljs +++ b/ui/src/ethlance/ui/page/job_detail/events.cljs @@ -1,23 +1,25 @@ (ns ethlance.ui.page.job-detail.events - (:require [district.ui.router.effects :as router.effects] - [district.ui.router.queries :as router.queries] - [district.ui.conversion-rates.queries :as conversion-rates.queries] - [district.ui.web3.queries :as web3-queries] - [district.ui.smart-contracts.queries :as contract-queries] - [ethlance.ui.event.utils :as event.utils] - [district.ui.notification.events :as notification.events] - [ethlance.ui.util.tokens :as util.tokens] - [ethlance.shared.utils :refer [eth->wei]] - [cljs-web3-next.eth :as w3n-eth] - [district.ui.web3-tx.events :as web3-events] - [ethlance.shared.contract-constants :as contract-constants] - [district.ui.smart-contracts.queries :as contract-queries] - [ethlance.shared.utils :refer [eth->wei base58->hex]] - [district.ui.web3-accounts.queries :as accounts-queries] - [re-frame.core :as re])) + (:require + [cljs-web3-next.eth :as w3n-eth] + [district.ui.conversion-rates.queries :as conversion-rates.queries] + [district.ui.notification.events :as notification.events] + [district.ui.router.effects :as router.effects] + [district.ui.router.queries :as router.queries] + [district.ui.smart-contracts.queries :as contract-queries] + [district.ui.web3-accounts.queries :as accounts-queries] + [district.ui.web3-tx.events :as web3-events] + [district.ui.web3.queries :as web3-queries] + [ethlance.shared.contract-constants :as contract-constants] + [ethlance.shared.utils :refer [eth->wei base58->hex]] + [ethlance.ui.event.utils :as event.utils] + [ethlance.ui.util.tokens :as util.tokens] + [re-frame.core :as re])) + ;; Page State (def state-key :page.job-detail) + + (def state-default {:add-funds-tx-in-progress? false :end-job-tx-in-progress? false @@ -30,36 +32,45 @@ :job-arbiter-idle false :show-invite-arbiters false}) + (def interceptors [re/trim-v]) + (defn initialize-page "Event FX Handler. Setup listener to dispatch an event when the page is active/visited." [{:keys [db]}] - (let [page-state (get db state-key)] - { - ; :fx [[:dispatch [:page.job-detail/fetch-job-arbiter-status]]] - ::router.effects/watch-active-page - [{:id :page.job-detail/initialize-page - :name :route.job/detail - :dispatch [:page.job-detail/fetch-proposals] - }] - :db (assoc-in db [state-key] state-default)})) - -(defn set-add-funds-tx-in-progress [db in-progress?] + {::router.effects/watch-active-page + [{:id :page.job-detail/initialize-page + :name :route.job/detail + :dispatch [:page.job-detail/fetch-proposals]}] + :db (assoc db state-key state-default)}) + + +(defn set-add-funds-tx-in-progress + [db in-progress?] (assoc-in db [state-key :add-funds-tx-in-progress?] in-progress?)) -(defn set-end-job-tx-in-progress [db in-progress?] + +(defn set-end-job-tx-in-progress + [db in-progress?] (assoc-in db [state-key :end-job-tx-in-progress?] in-progress?)) -(defn set-invite-arbiters-tx-in-progress [db in-progress?] + +(defn set-invite-arbiters-tx-in-progress + [db in-progress?] (assoc-in db [state-key :invite-arbiters-tx-in-progress?] in-progress?)) + + ;; ;; Registered Events ;; (def create-assoc-handler (partial event.utils/create-assoc-handler state-key)) + + (defn create-logging-handler ([] (create-logging-handler "")) - ([text] (fn [db args] (println ">>> Received event in" state-key " " text " with args:" args)))) + ([text] (fn [_db args] (println ">>> Received event in" state-key " " text " with args:" args)))) + ;; TODO: switch based on dev environment (re/reg-event-fx :page.job-detail/initialize-page initialize-page) @@ -69,13 +80,15 @@ (re/reg-event-fx :page.job-detail/set-arbitrations-offset (create-assoc-handler :arbitrations-offset)) + (re/reg-event-db :page.job-detail/set-arbitration-token-amount (fn [db [_ token-amount]] (-> db (assoc-in ,,, [state-key :arbitration-token-amount] token-amount) (assoc-in ,,, [state-key :arbitration-token-amount-usd] - (util.tokens/round 2 (* token-amount (conversion-rates.queries/conversion-rate db :ETH :USD))))))) + (util.tokens/round 2 (* token-amount (conversion-rates.queries/conversion-rate db :ETH :USD))))))) + (re/reg-event-db :page.job-detail/set-arbitration-token-amount-usd @@ -83,10 +96,12 @@ (-> db (assoc-in ,,, [state-key :arbitration-token-amount-usd] usd-amount) (assoc-in ,,, [state-key :arbitration-token-amount] - (util.tokens/round 4 (* usd-amount (conversion-rates.queries/conversion-rate db :USD :ETH))))))) + (util.tokens/round 4 (* usd-amount (conversion-rates.queries/conversion-rate db :USD :ETH))))))) + (re/reg-event-fx :page.job-detail/set-show-invite-arbiters (create-assoc-handler :show-invite-arbiters)) + (re/reg-event-db :page.job-detail/set-arbitration-to-accept (fn [db [_ arbitration]] @@ -97,6 +112,7 @@ arbitration)] (assoc-in db db-path toggled-value)))) + (def job-story-requested-fields [:job-story/id :job/id @@ -114,12 +130,12 @@ [:creator [:user/id :user/name]]]]]) + (re/reg-event-fx :page.job-proposal/send [interceptors] (fn [{:keys [db]} [contract-address token-type]] - (let [user-address (accounts-queries/active-account db) - text (get-in db [state-key :job/proposal-text]) + (let [text (get-in db [state-key :job/proposal-text]) token-amount (util.tokens/machine-amount (get-in db [state-key :job/proposal-token-amount]) token-type) proposal {:contract contract-address :text text @@ -129,18 +145,19 @@ job-story-requested-fields]] :on-success [:page.job-detail/fetch-proposals]}]}))) + (re/reg-event-fx :page.job-proposal/remove [interceptors] (fn [{:keys [db]} [job-story-id]] - (let [user-address (accounts-queries/active-account db)] - {:db (-> db - (assoc-in ,,, [state-key :job/proposal-token-amount] nil) - (assoc-in ,,, [state-key :job/proposal-text] nil)) - :dispatch [:district.ui.graphql.events/mutation - {:queries [[:remove-job-proposal {:job-story/id job-story-id} - job-story-requested-fields]] - :on-success [:page.job-detail/fetch-proposals]}]}))) + {:db (-> db + (assoc-in ,,, [state-key :job/proposal-token-amount] nil) + (assoc-in ,,, [state-key :job/proposal-text] nil)) + :dispatch [:district.ui.graphql.events/mutation + {:queries [[:remove-job-proposal {:job-story/id job-story-id} + job-story-requested-fields]] + :on-success [:page.job-detail/fetch-proposals]}]})) + (re/reg-event-fx :page.job-detail/fetch-proposals @@ -156,15 +173,16 @@ :offset (get-in db [state-key :proposal-offset])} [:total-count [:items job-story-requested-fields]]]] - :on-success [:proposal-stories-success] - :on-error [:proposal-stories-error]}]}))) + :on-success [:proposal-stories-success] + :on-error [:proposal-stories-error]}]}))) + (re/reg-event-fx :proposal-stories-success [interceptors] (fn [{:keys [db]} data] (let [result (some :job-story-search data) - stories (get-in result [:items]) + stories (get result :items) id-mapped (reduce (fn [acc job-story] (assoc acc (:job-story/id job-story) job-story)) @@ -174,6 +192,7 @@ (assoc ,,, :job-stories id-mapped) (assoc-in ,,, [state-key :proposal-total-count] (:total-count result)))}))) + (re/reg-event-fx :proposal-stories-error [interceptors] @@ -182,7 +201,7 @@ (defn send-arbitration-data-to-ipfs - [{:keys [db]} [_ event]] + [_cofx [_ event]] (let [ipfs-arbitration (select-keys event [:job/id :user/id @@ -193,7 +212,9 @@ :on-success [:page.job-detail/arbitration-to-ipfs-success event] :on-error [:page.job-detail/arbitration-to-ipfs-failure event]}})) -(defn set-quote-for-arbitration-tx [cofx [_event-name forwarded-event-data ipfs-data]] + +(defn set-quote-for-arbitration-tx + [cofx [_event-name forwarded-event-data ipfs-data]] (let [job-address (:job/id forwarded-event-data) arbiter-address (:user/id forwarded-event-data) token-type (:job-arbiter/fee-currency-id forwarded-event-data) @@ -209,8 +230,8 @@ instance (contract-queries/instance (:db cofx) :job job-address) tx-opts {:from arbiter-address :gas 10000000} ipfs-hash (base58->hex (:Hash ipfs-data)) - ; TODO: decide if sending to IPFS would serve for anything or all the - ; information involved is already in the contract & QuoteForArbitrationEvent + ;; TODO: decide if sending to IPFS would serve for anything or all the + ;; information involved is already in the contract & QuoteForArbitrationEvent contract-args [[(clj->js offered-value)]]] {:dispatch [::web3-events/send-tx {:instance instance @@ -222,7 +243,9 @@ :on-tx-success [:page.job-detail/arbitration-tx-success "Transaction to set quote successful"] :on-tx-error [::set-quote-for-arbitration-tx-error]}]})) -(defn accept-quote-for-arbitration-tx [cofx [_event-name forwarded-event-data]] + +(defn accept-quote-for-arbitration-tx + [cofx [_event-name forwarded-event-data]] (let [job-address (:job/id forwarded-event-data) arbiter-address (:user/id forwarded-event-data) employer-address (:employer forwarded-event-data) @@ -249,15 +272,17 @@ :on-tx-success [:page.job-detail/arbitration-tx-success "Transaction to accept quote successful"] :on-tx-error [::accept-quote-for-arbitration-tx-error]}]})) -(defn invite-arbiters [{:keys [db] :as cofx} [_event-name event-data]] + +(defn invite-arbiters + [{:keys [db]} [_event-name event-data]] (let [job-address (:job/id event-data) arbiter-addresses (:arbiters event-data) employer-address (:employer event-data) instance (contract-queries/instance db :job job-address) tx-opts {:from employer-address :gas 10000000} contract-args [employer-address arbiter-addresses]] - {:db (set-invite-arbiters-tx-in-progress db true) - :dispatch [::web3-events/send-tx + {:db (set-invite-arbiters-tx-in-progress db true) + :dispatch [::web3-events/send-tx {:instance instance :fn :invite-arbiters :args contract-args @@ -267,7 +292,9 @@ :on-tx-success [:page.job-detail/arbitration-tx-success "Transaction to invite arbiters successful"] :on-tx-error [::invite-arbiters-tx-error]}]})) -(defn end-job-tx [{:keys [db] :as cofx} [_event-name event-data]] + +(defn end-job-tx + [{:keys [db] :as cofx} [_event-name event-data]] (let [job-address (:job/id event-data) employer-address (:employer event-data) instance (contract-queries/instance (:db cofx) :job job-address) @@ -283,6 +310,7 @@ :on-tx-success [:page.job-detail/end-job-tx-success] :on-tx-error [::end-job-tx-error]}]})) + (re/reg-event-fx :page.job-detail/set-quote-for-arbitration send-arbitration-data-to-ipfs) (re/reg-event-fx :page.job-detail/accept-quote-for-arbitration accept-quote-for-arbitration-tx) (re/reg-event-fx :page.job-detail/arbitration-to-ipfs-success set-quote-for-arbitration-tx) @@ -297,25 +325,32 @@ (re/reg-event-fx :page.job-detail/end-job end-job-tx) (re/reg-event-fx ::end-job-tx-hash-error (create-logging-handler)) + + (re/reg-event-fx ::end-job-tx-error (fn [{:keys [db]} _] {:db (set-end-job-tx-in-progress db false)})) + (re/reg-event-fx :page.job-detail/invite-arbiters invite-arbiters) + + (re/reg-event-db ::invite-arbiters-tx-error (fn [db _] (set-invite-arbiters-tx-in-progress db false))) + (re/reg-event-db ::invite-arbiters-tx-hash-error (fn [db _] (set-invite-arbiters-tx-in-progress db false))) + (re/reg-event-fx :page.job-detail/fetch-job-arbiter-status - (fn [cofx event] + (fn [cofx _event] (let [web3-instance (web3-queries/web3 (:db cofx)) job-address (:id (router.queries/active-page-params (:db cofx))) contract-instance (contract-queries/instance (:db cofx) :job job-address) @@ -326,45 +361,52 @@ :on-error [::job-arbiter-status-error]}] {:web3/call {:web3 web3-instance :fns [to-call]}}))) + (re/reg-event-db ::job-arbiter-status-success (fn [db [_ job-address arbiter-idle?]] (println ">>> ::job-arbiter-status-success received" job-address arbiter-idle?) (assoc-in db [state-key :job-arbiter-idle] arbiter-idle?))) + (re/reg-event-db ::job-arbiter-status-error (fn [db event] (println ">>> ::job-arbiter-status-error" event) db)) + (re/reg-event-fx :page.job-detail/arbitration-tx-success - (fn [cofx [event message]] + (fn [cofx [_event message]] {:db (-> (:db cofx) (set-invite-arbiters-tx-in-progress ,,, false) - (assoc-in ,,, [state-key] state-default)) + (assoc ,,, state-key state-default)) :fx [[:dispatch [:page.job-detail/arbitrations-updated]] [:dispatch [::notification.events/show message]]]})) + (re/reg-event-db :page.job-detail/set-selected-arbiters (fn [db [_ selection]] (assoc-in db [state-key :selected-arbiters] selection))) + (re/reg-event-fx :page.job-detail/end-job-tx-success - (fn [{:keys [db] :as cofx} event] + (fn [{:keys [db]} _event] {:db (set-end-job-tx-in-progress db false) :fx [[:dispatch [:page.job-detail/job-updated]] [:dispatch [::notification.events/show "Transaction to end job processed successfully"]]]})) + (re/reg-event-fx :page.job-detail/set-add-funds-amount (create-assoc-handler :add-funds-amount)) (re/reg-event-fx :page.job-detail/start-adding-funds (create-assoc-handler :adding-funds?)) + (re/reg-event-fx ::send-add-funds-tx - (fn [{:keys [db] :as cofx} [_ funds-params]] + (fn [{:keys [db]} [_ funds-params]] (let [token-type (:token-type funds-params) tx-opts-base {:from (:funder funds-params) :gas 10000000} offered-value (:offered-value funds-params) @@ -381,6 +423,7 @@ :on-tx-success [::add-funds-tx-success] :on-tx-error [::add-funds-tx-error]}]]]}))) + (re/reg-event-fx ::erc20-allowance-amount-success (fn [{:keys [db] :as cofx} [_ funds-params result]] @@ -403,14 +446,16 @@ [::send-add-funds-tx funds-params] increase-allowance-event)]]}))) + (re/reg-event-fx ::erc20-allowance-amount-error - (fn [cofx result] + (fn [_cofx result] (println ">>> ::erc20-allowance-amount-error" result))) + (re/reg-event-fx ::ensure-erc20-allowance - (fn [{:keys [db] :as cofx} [_ funds-params]] + (fn [{:keys [db]} [_ funds-params]] (let [offered-value (funds-params :offered-value) erc20-address (get-in offered-value [:token :tokenContract :tokenAddress]) erc20-abi (:erc20 ethlance.shared.contract-constants/abi) @@ -418,15 +463,16 @@ owner (:funder funds-params) spender (:receiver funds-params)] {:fx [[:web3/call {:fns - [{:instance erc20-instance - :fn :allowance - :args [owner spender] - :on-success [::erc20-allowance-amount-success funds-params] - :on-error [::erc20-allowance-amount-error]}]}]]}))) + [{:instance erc20-instance + :fn :allowance + :args [owner spender] + :on-success [::erc20-allowance-amount-success funds-params] + :on-error [::erc20-allowance-amount-error]}]}]]}))) + (re/reg-event-fx ::safe-transfer-with-add-funds - (fn [{:keys [db] :as cofx} [_ funds-params]] + (fn [{:keys [db]} [_ funds-params]] (let [offered-value (:offered-value funds-params) amount (:value offered-value) token-address (get-in offered-value [:token :tokenContract :tokenAddress]) @@ -458,15 +504,12 @@ :on-tx-error [::add-funds-tx-error]}]] {:fx [[:dispatch safe-transfer-with-create-job-tx]]}))) + (re/reg-event-fx :page.job-detail/finish-adding-funds (fn [{:keys [db] :as cofx} [_ job-address token-details token-id tx-amount]] (let [funder (accounts-queries/active-account (:db cofx)) - tx-opts-base {:from funder :gas 10000000} token-type (:token-detail/type token-details) - tx-opts (if (= token-type :eth) - (assoc tx-opts-base :value tx-amount) - tx-opts-base) token-address (:token-detail/id token-details) address-placeholder "0x0000000000000000000000000000000000000000" token-address (if (not (= token-type :eth)) @@ -478,7 +521,6 @@ :tokenContract {:tokenType (contract-constants/token-type->enum-val token-type) :tokenAddress token-address}}} - instance (contract-queries/instance (:db cofx) :job job-address) funds-params {:offered-value offered-value :token-type token-type :funder funder @@ -490,19 +532,20 @@ {:db (set-add-funds-tx-in-progress db true) :fx [[:dispatch [(get next-event token-type) funds-params]]]}))) + (re/reg-event-fx ::add-funds-tx-success - (fn [{:keys [db]} [event-name tx-data]] - (let [events (get-in tx-data [:events])] - {:db (-> db - (assoc-in ,,, [state-key :adding-funds?] false) - (assoc-in ,,, [state-key :add-funds-amount] nil) - (set-add-funds-tx-in-progress ,,, false)) - :fx [[:dispatch [::notification.events/show "Transaction to add funds processed successfully"]] - [:dispatch [:page.job-detail/job-updated]]]}))) + (fn [{:keys [db]} [_event-name _tx-data]] + {:db (-> db + (assoc-in ,,, [state-key :adding-funds?] false) + (assoc-in ,,, [state-key :add-funds-amount] nil) + (set-add-funds-tx-in-progress ,,, false)) + :fx [[:dispatch [::notification.events/show "Transaction to add funds processed successfully"]] + [:dispatch [:page.job-detail/job-updated]]]})) + (re/reg-event-db ::add-funds-tx-error - (fn [db event] + (fn [db _event] {:db (set-add-funds-tx-in-progress db false) :dispatch [::notification.events/show "Error with add funds to job transaction"]})) diff --git a/ui/src/ethlance/ui/page/job_detail/subscriptions.cljs b/ui/src/ethlance/ui/page/job_detail/subscriptions.cljs index 9240ef09..b4288033 100644 --- a/ui/src/ethlance/ui/page/job_detail/subscriptions.cljs +++ b/ui/src/ethlance/ui/page/job_detail/subscriptions.cljs @@ -1,10 +1,10 @@ (ns ethlance.ui.page.job-detail.subscriptions (:require - [ethlance.ui.page.job-detail.events :as job-detail.events] - [re-frame.core :as re] - [district.ui.conversion-rates.subs :as rates-subs] [ethlance.shared.utils :refer [ilike=]] - [ethlance.ui.subscription.utils :as subscription.utils])) + [ethlance.ui.page.job-detail.events :as job-detail.events] + [ethlance.ui.subscription.utils :as subscription.utils] + [re-frame.core :as re])) + (def create-get-handler #(subscription.utils/create-get-handler job-detail.events/state-key %)) @@ -23,6 +23,7 @@ (re/reg-sub :page.job-detail/job-arbiter-idle (create-get-handler :job-arbiter-idle)) + (re/reg-sub :page.job-detail/arbitration-token-amount-usd (fn [db _] @@ -30,6 +31,7 @@ "" (get-in db [job-detail.events/state-key :arbitration-token-amount-usd])))) + (re/reg-sub :page.job-detail/arbitration-token-amount (fn [db _] @@ -37,11 +39,13 @@ "" (get-in db [job-detail.events/state-key :arbitration-token-amount])))) + (re/reg-sub :page.job-detail/proposal-total-count (fn [db] (get-in db [job-detail.events/state-key :proposal-total-count]))) + (re/reg-sub :page.job-detail/all-proposals (fn [db [_ queried-job-contract]] @@ -56,22 +60,23 @@ (->> stories (map (fn [story] {:current-user? (ilike= current-user-address - (or (get-in story [:candidate :user :user/id]) - (get-in story [:proposal-message :creator :user/id]) - "")) + (or (get-in story [:candidate :user :user/id]) + (get-in story [:proposal-message :creator :user/id]) + "")) :job-story/id (:job-story/id story) - :proposal-message (get-in story [:proposal-message]) + :proposal-message (get story :proposal-message) :candidate-name (or (get-in story [:candidate :user :user/name]) (get-in story [:proposal-message :creator :user/name])) - :rate (get-in story [:job-story/proposal-rate]) + :rate (get story :job-story/proposal-rate) :message (get-in story [:proposal-message :message/text]) - :created-at (get-in story [:job-story/date-created]) - :status (get-in story [:job-story/status])})) - ; Keeps the current-user's proposal at the top of the list + :created-at (get story :job-story/date-created) + :status (get story :job-story/status)})) + ;; Keeps the current-user's proposal at the top of the list (sort-by (fn [story] [(if (:current-user? story) 1 0) (:created-at story)])) reverse)))) + (re/reg-sub :page.job-detail/active-proposals :<- [:page.job-detail/all-proposals] @@ -80,6 +85,7 @@ (not (nil? (get % :proposal-message))) (not= :deleted (:status %))) proposals))) + (defn seek "Returns first item from coll for which (pred item) returns true. Returns nil if no such item is present, or the not-found value if supplied." @@ -91,6 +97,7 @@ not-found)) not-found coll))) + (re/reg-sub :page.job-detail/my-proposal :<- [:page.job-detail/all-proposals] @@ -99,6 +106,7 @@ (filter #(not= :deleted (:status %)) ,,,) (seek :current-user? ,,,)))) + (re/reg-sub :page.job-detail/add-funds-amount (create-get-handler :add-funds-amount)) (re/reg-sub :page.job-detail/adding-funds? (create-get-handler :adding-funds?)) (re/reg-sub :page.job-detail/add-funds-tx-in-progress? (create-get-handler :add-funds-tx-in-progress?)) diff --git a/ui/src/ethlance/ui/page/jobs.cljs b/ui/src/ethlance/ui/page/jobs.cljs index 23d1184e..4352b5bf 100644 --- a/ui/src/ethlance/ui/page/jobs.cljs +++ b/ui/src/ethlance/ui/page/jobs.cljs @@ -1,69 +1,65 @@ (ns ethlance.ui.page.jobs "General Job Listings on ethlance" - (:require [cuerdas.core :as str] - [district.format :as format] - [district.ui.component.page :refer [page]] - [ethlance.ui.component.pagination :refer [c-pagination]] - [district.ui.router.events :as router-events] - [inflections.core :as inflections] - [district.ui.graphql.subs :as gql] - [ethlance.shared.constants :as constants] - [ethlance.shared.enumeration.currency-type :as enum.currency] - [ethlance.ui.component.info-message :refer [c-info-message]] - [ethlance.ui.util.navigation :as util.navigation] - [ethlance.ui.component.currency-input :refer [c-currency-input]] - [ethlance.ui.component.inline-svg :refer [c-inline-svg]] - [ethlance.ui.component.loading-spinner :refer [c-loading-spinner]] - [ethlance.ui.component.main-layout :refer [c-main-layout]] - [ethlance.ui.component.token-info :refer [c-token-info]] - [ethlance.ui.util.tokens :as tokens] - [ethlance.ui.component.mobile-search-filter - :refer - [c-mobile-search-filter]] - [ethlance.ui.component.radio-select - :refer - [c-radio-search-filter-element c-radio-select]] - [ethlance.ui.component.rating :refer [c-rating]] - [ethlance.ui.component.search-input :refer [c-chip-search-input]] - [ethlance.ui.component.select-input :refer [c-select-input]] - [ethlance.ui.component.tag :refer [c-tag c-tag-label]] - [ethlance.ui.component.text-input :refer [c-text-input]] - [re-frame.core :as re])) + (:require + [cuerdas.core :as str] + [district.format :as format] + [district.ui.component.page :refer [page]] + [district.ui.graphql.subs :as gql] + [district.ui.router.events :as router-events] + [ethlance.shared.constants :as constants] + [ethlance.shared.enumeration.currency-type :as enum.currency] + [ethlance.ui.component.currency-input :refer [c-currency-input]] + [ethlance.ui.component.info-message :refer [c-info-message]] + [ethlance.ui.component.inline-svg :refer [c-inline-svg]] + [ethlance.ui.component.loading-spinner :refer [c-loading-spinner]] + [ethlance.ui.component.main-layout :refer [c-main-layout]] + [ethlance.ui.component.mobile-search-filter + :refer + [c-mobile-search-filter]] + [ethlance.ui.component.pagination :refer [c-pagination]] + [ethlance.ui.component.radio-select + :refer + [c-radio-search-filter-element c-radio-select]] + [ethlance.ui.component.rating :refer [c-rating]] + [ethlance.ui.component.search-input :refer [c-chip-search-input]] + [ethlance.ui.component.select-input :refer [c-select-input]] + [ethlance.ui.component.tag :refer [c-tag c-tag-label]] + [ethlance.ui.component.text-input :refer [c-text-input]] + [ethlance.ui.component.token-info :refer [c-token-info]] + [ethlance.ui.util.navigation :as util.navigation] + [inflections.core :as inflections] + [re-frame.core :as re])) + (defn c-user-employer-detail [employer] (let [name (get-in employer [:user :user/name]) - rating (get-in employer [:employer/rating]) + rating (get employer :employer/rating) rating-count (get-in employer [:employer/feedback :total-count]) country (get-in employer [:user :user/country]) - address (get-in employer [:user/id])] + address (get employer :user/id)] [:div.user-detail.employer - {:on-click (util.navigation/create-handler {:route :route.user/profile - :params {:address address} - :query {:tab :employer}}) - :href (util.navigation/resolve-route {:route :route.user/profile - :params {:address address} - :query {:tab :employer}})} + (util.navigation/link-params {:route :route.user/profile + :params {:address address} + :query {:tab :employer}}) [:div.name name] [:div.rating-container [c-rating {:size :small :color :primary :default-rating rating}] (when rating-count [:div.rating-label (str "(" rating-count ")")])] [:div.location country]])) + (defn c-user-arbiter-detail [arbiter] (let [name (get-in arbiter [:user :user/name]) - rating (get-in arbiter [:employer/rating]) + rating (get arbiter :employer/rating) rating-count (get-in arbiter [:arbiter/feedback :total-count]) country (get-in arbiter [:user :user/country]) - address (get-in arbiter [:user/id])] + address (get arbiter :user/id)] [:div.user-detail.arbiter - {:on-click (util.navigation/create-handler {:route :route.user/profile - :params {:address address} - :query {:tab :arbiter}}) - :href (util.navigation/resolve-route {:route :route.user/profile - :params {:address address} - :query {:tab :arbiter}})} + (util.navigation/link-params {:route :route.user/profile + :params {:address address} + :query {:tab :arbiter}}) [c-inline-svg {:class "arbiter-icon" :src "images/svg/hammer.svg"}] [:div.name name] [:div.rating-container @@ -71,6 +67,7 @@ (when rating-count [:div.rating-label (str "(" rating-count ")")])] [:div.location country]])) + (defn c-job-detail-table [{:job/keys [bid-option required-experience-level estimated-project-length required-availability] :as job}] (let [experience-level (keyword required-experience-level) @@ -78,7 +75,7 @@ :beginner "Novice ($)" :intermediate "Professional ($$)" :expert "Expert ($$$)") - token-details (get-in job [:token-details]) + token-details (get job :token-details) amount (:job/token-amount job)] [:div.job-detail-table [:div.name "Payment Type"] @@ -96,6 +93,7 @@ [:div.name "Availability"] [:div.value (str/title required-availability)]])) + (defn cf-job-search-filter "Component Fragment for the job search filter." [] @@ -185,21 +183,22 @@ "A single job element component composed from the job data." [{:job/keys [title description date-created required-skills arbiter employer id] :as job}] (let [proposals-count (get-in job [:job-stories :total-count]) - ; TODO: remove new js/Date after switching to district.ui.graphql that converts Date GQL type automatically + ;; TODO: remove new js/Date after switching to district.ui.graphql that converts Date GQL type automatically relative-ago (format/time-ago (new js/Date date-created)) pluralized-proposals (inflections/pluralize proposals-count "proposal")] [:div.job-element - [:div.title {:on-click (fn [event] (re/dispatch [::router-events/navigate :route.job/detail {:id id}]))} + [:a.title (util.navigation/link-params {:route :route.job/detail + :params {:id id}}) title] [:div.description description] [:div.date (str "Posted " relative-ago " | " pluralized-proposals)] [:div.tags (doall - (for [skill-label required-skills] - ^{:key (str "tag-" skill-label)} - [c-tag {:on-click #(re/dispatch [:page.jobs/add-skill skill-label]) - :title (str "Add '" skill-label "' to Search")} - [c-tag-label skill-label]]))] + (for [skill-label required-skills] + ^{:key (str "tag-" skill-label)} + [c-tag {:on-click #(re/dispatch [:page.jobs/add-skill skill-label]) + :title (str "Add '" skill-label "' to Search")} + [c-tag-label skill-label]]))] [:div.users [c-user-employer-detail employer] @@ -209,7 +208,8 @@ [c-job-detail-table job]]])) -(defn c-job-listing [] +(defn c-job-listing + [] (fn [] (let [query-params (re/subscribe [:page.jobs/job-search-params]) query [:job-search @query-params @@ -277,11 +277,12 @@ [c-job-element job]))) (when (seq job-listing) - [c-pagination - {:total-count total-count - :limit (or @*limit 10) - :offset (or @*offset 0) - :set-offset-event :page.jobs/set-offset}])]))) + [c-pagination + {:total-count total-count + :limit (or @*limit 10) + :offset (or @*offset 0) + :set-offset-event :page.jobs/set-offset}])]))) + (defmethod page :route.job/jobs [] (let [*skills (re/subscribe [:page.jobs/skills])] diff --git a/ui/src/ethlance/ui/page/jobs/events.cljs b/ui/src/ethlance/ui/page/jobs/events.cljs index 2a2fc11c..c1b9c54b 100644 --- a/ui/src/ethlance/ui/page/jobs/events.cljs +++ b/ui/src/ethlance/ui/page/jobs/events.cljs @@ -1,16 +1,17 @@ (ns ethlance.ui.page.jobs.events - (:require [district.parsers :refer [parse-int]] - [district.ui.router.effects :as router.effects] - [ethlance.shared.constants :as constants] - [district.ui.graphql.events :as gql-events] - [ethlance.ui.event.templates :as event.templates] - [ethlance.ui.event.utils :as event.utils] - [re-frame.core :as re])) + (:require + [district.parsers :refer [parse-int]] + [ethlance.ui.event.templates :as event.templates] + [ethlance.ui.event.utils :as event.utils] + [re-frame.core :as re])) + ;; Page State (def state-key :page.jobs) + + (def state-default - {; Job Listing Query Parameters + {;; Job Listing Query Parameters :skills #{} :category ["All Categories" nil]; constants/category-default :feedback-min-rating nil @@ -23,22 +24,25 @@ :limit 10 :experience-level nil}) + (defn initialize-page "Event FX Handler. Setup listener to dispatch an event when the page is active/visited." [{:keys [db]} _] - (let [page-state (get db state-key)] - {:db (assoc-in db [state-key] state-default)})) + {:db (assoc db state-key state-default)}) + (defn add-skill "Event FX Handler. Append skill to skill listing." [{:keys [db]} [_ new-skill]] {:db (update-in db [state-key :skills] conj new-skill)}) + ;; ;; Registered Events ;; (def create-assoc-handler (partial event.utils/create-assoc-handler state-key)) + ;; TODO: switch based on dev environment (re/reg-event-fx :page.jobs/set-offset (create-assoc-handler :offset)) (re/reg-event-fx :page.jobs/set-limit (create-assoc-handler :limit)) diff --git a/ui/src/ethlance/ui/page/jobs/subscriptions.cljs b/ui/src/ethlance/ui/page/jobs/subscriptions.cljs index 67e4bc5f..22219813 100644 --- a/ui/src/ethlance/ui/page/jobs/subscriptions.cljs +++ b/ui/src/ethlance/ui/page/jobs/subscriptions.cljs @@ -1,10 +1,9 @@ (ns ethlance.ui.page.jobs.subscriptions (:require - [re-frame.core :as re] - - [ethlance.ui.util.graphql :as util.graphql] - [ethlance.ui.page.jobs.events :as jobs.events] - [ethlance.ui.subscription.utils :as subscription.utils])) + [ethlance.ui.page.jobs.events :as jobs.events] + [ethlance.ui.subscription.utils :as subscription.utils] + [ethlance.ui.util.graphql :as util.graphql] + [re-frame.core :as re])) (def create-get-handler #(subscription.utils/create-get-handler jobs.events/state-key %)) @@ -16,8 +15,6 @@ (re/reg-sub :page.jobs/offset (create-get-handler :offset)) (re/reg-sub :page.jobs/limit (create-get-handler :limit)) -(re/reg-sub :page.jobs/job-listing (create-get-handler :job-listing)) -(re/reg-sub :page.jobs/job-listing-state (create-get-handler :job-listing/state)) (re/reg-sub :page.jobs/skills (create-get-handler :skills)) (re/reg-sub :page.jobs/category (create-get-handler :category)) (re/reg-sub :page.jobs/feedback-max-rating (create-get-handler :feedback-max-rating)) @@ -28,10 +25,11 @@ (re/reg-sub :page.jobs/payment-type (create-get-handler :payment-type)) (re/reg-sub :page.jobs/experience-level (create-get-handler :experience-level)) + (re/reg-sub :page.jobs/job-search-params (fn [db _] - (let [page-state (get-in db [jobs.events/state-key] {}) + (let [page-state (get db jobs.events/state-key {}) filters [[:skills #(into [] %)] [:category second] [:feedback-max-rating] diff --git a/ui/src/ethlance/ui/page/me.cljs b/ui/src/ethlance/ui/page/me.cljs index 163a171d..0e7f1f3e 100644 --- a/ui/src/ethlance/ui/page/me.cljs +++ b/ui/src/ethlance/ui/page/me.cljs @@ -1,23 +1,24 @@ (ns ethlance.ui.page.me - (:require [district.ui.component.page :refer [page]] - [district.ui.router.subs :as router.subs] - [district.ui.router.events :as router.events] - [ethlance.ui.component.circle-button :refer [c-circle-icon-button]] - [ethlance.ui.component.pagination :refer [c-pagination-ends]] - [ethlance.ui.component.main-layout :refer [c-main-layout]] - [ethlance.ui.component.mobile-sidebar :refer [c-mobile-sidebar]] - [ethlance.ui.component.table :refer [c-table]] - [ethlance.ui.component.tabular-layout :refer [c-tabular-layout]] - [ethlance.ui.component.button :refer [c-button c-button-label]] - [ethlance.ui.component.loading-spinner :refer [c-loading-spinner]] - [ethlance.ui.component.info-message :refer [c-info-message]] - [ethlance.ui.util.navigation :refer [link-params] :as util.navigation] - [ethlance.ui.util.dates :refer [relative-ago formatted-date]] - [district.ui.graphql.subs :as gql] - [ethlance.ui.util.tokens :as tokens] - [re-frame.core :as re])) - -(defn c-nav-sidebar-element [label id-value] + (:require + [district.ui.component.page :refer [page]] + [district.ui.graphql.subs :as gql] + [district.ui.router.events :as router.events] + [district.ui.router.subs :as router.subs] + [ethlance.ui.component.info-message :refer [c-info-message]] + [ethlance.ui.component.loading-spinner :refer [c-loading-spinner]] + [ethlance.ui.component.main-layout :refer [c-main-layout]] + [ethlance.ui.component.mobile-sidebar :refer [c-mobile-sidebar]] + [ethlance.ui.component.pagination :refer [c-pagination-ends]] + [ethlance.ui.component.table :refer [c-table]] + [ethlance.ui.component.tabular-layout :refer [c-tabular-layout]] + [ethlance.ui.util.dates :refer [formatted-date]] + [ethlance.ui.util.navigation :refer [link-params] :as util.navigation] + [ethlance.ui.util.tokens :as tokens] + [re-frame.core :as re])) + + +(defn c-nav-sidebar-element + [label id-value] (let [*active-page (re/subscribe [::router.subs/active-page])] (fn [] (let [{active-page :name @@ -59,6 +60,7 @@ :on-click (util.navigation/create-handler {:route active-page :params active-params :query updated-query})} label]])))) + (defn c-table-listing "Produces tabl ewith headers @@ -92,26 +94,32 @@ :offset offset :set-offset-event :page.me/set-pagination-offset}]])) -(defn tab-navigate-handler [sidebar tab] + +(defn tab-navigate-handler + [sidebar tab] (fn [] (re/dispatch [::router.events/navigate :route.me/index {} {:sidebar sidebar :tab tab}]))) -(defn spinner-until-data-ready [loading-states component-when-loading-finished] + +(defn spinner-until-data-ready + [loading-states component-when-loading-finished] (if (not-every? false? loading-states) [c-loading-spinner] component-when-loading-finished)) -(defn c-job-listing [user-type] + +(defn c-job-listing + [user-type] (let [active-user (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) url-query @(re/subscribe [::router.subs/active-page-query]) tab (or (:tab url-query) "active") tab-to-index {"active" 0 "finished" 1} - ; Currently nothing sets the job status as finished (only job-story status) - ; Withdrawing all funds (on job details page) sets job to "ended" status - ; Alternatively the :status search param could accept array + ;; Currently nothing sets the job status as finished (only job-story status) + ;; Withdrawing all funds (on job details page) sets job to "ended" status + ;; Alternatively the :status search param could accept array status-search-param (if (= tab "finished") "ended" tab) tab-index (get tab-to-index tab 0) limit @(re/subscribe [:page.me/pagination-limit]) @@ -139,11 +147,12 @@ result @(re/subscribe [::gql/query {:queries [job-query]}]) [loading? processing?] (map result [:graphql/loading? :graphql/preprocessing?]) jobs (get-in result [:job-search :items]) - remuneration (fn [job] (str (tokens/human-amount - (:job/token-amount job) - (:job/token-type job) - (get-in job [:token-details :token-detail/decimals] )) - " " (-> job :token-details :token-detail/symbol))) + remuneration (fn [job] + (str (tokens/human-amount + (:job/token-amount job) + (:job/token-type job) + (get-in job [:token-details :token-detail/decimals])) + " " (-> job :token-details :token-detail/symbol))) arbitration-info (fn [job] (let [arbitration-status (:arbitration/status (first (get-in job [:arbitrations :items])))] (when arbitration-status (str "Arbitration: " arbitration-status)))) @@ -174,12 +183,15 @@ [:div.listing.my-employer-job-listing [c-table-listing jobs-table jobs job-link-fn pagination]])])) -(defn c-my-employer-job-listing [] + +(defn c-my-employer-job-listing + [] [c-job-listing :creator]) -(defn c-contract-listing [user-type user-address] - (let [active-user (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) - url-query @(re/subscribe [::router.subs/active-page-query]) + +(defn c-contract-listing + [user-type user-address] + (let [url-query @(re/subscribe [::router.subs/active-page-query]) tab (or (:tab url-query) "invitation") tab-to-index {"invitation" 0 "proposal" 1 "active" 2 "finished" 3} tab-index (get tab-to-index tab 0) @@ -243,9 +255,10 @@ [:div.listing.my-employer-job-listing [c-table-listing jobs-table jobs contract-link-fn pagination]])])) -(defn c-invoice-listing [user-type user-address] - (let [active-user (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) - url-query @(re/subscribe [::router.subs/active-page-query]) + +(defn c-invoice-listing + [user-type user-address] + (let [url-query @(re/subscribe [::router.subs/active-page-query]) tab (or (:tab url-query) "pending") tab-to-index {"pending" 0 "paid" 1} tab-index (get tab-to-index tab 0) @@ -291,12 +304,12 @@ (str (tokens/human-amount (:invoice/amount-requested invoice) (get-in invoice [:job-story :job :job/token-type]) (get-in invoice [:job-story :job :token-details :token-detail/decimals])) - " " (get-in invoice [:job-story :job :token-details :token-detail/symbol]))) + " " (get-in invoice [:job-story :job :token-details :token-detail/symbol]))) table [{:title "Job Title" :source #(get-in % [:job-story :job :job/title])} {:title "Candidate" :source user-name-fn} {:title "Amount Requested" :source amount-requested-fn} {:title "Created at" :source invoice-date-created-fn} - {:title "Status" :source #(get-in % [:invoice/status])}] + {:title "Status" :source #(get % :invoice/status)}] invoice-link-fn (fn [invoice] {:route :route.invoice/index :params {:job-id (:job/id invoice) :invoice-id (:invoice/id invoice)}}) @@ -318,7 +331,9 @@ [:div.listing.my-employer-job-listing [c-table-listing table invoices invoice-link-fn pagination]])])) -(defn c-dispute-listing [user-type] + +(defn c-dispute-listing + [user-type] (let [active-user (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) url-query @(re/subscribe [::router.subs/active-page-query]) tab (or (:tab url-query) "dispute-raised") @@ -361,19 +376,18 @@ :offset offset} disputes (get-in disputes-result [:dispute-search :items]) candidate-name-fn (fn [invoice] (get-in invoice [:candidate :user :user/name])) - arbiter-name-fn (fn [invoice] (get-in invoice [:arbiter :user :user/name])) arbiter-name-fn (fn [invoice] (get-in invoice [:dispute-resolved-message :creator :user/name])) - dispute-date-created-fn (partial formatted-date #(get-in % [:dispute/date-created])) - dispute-date-resolved-fn (partial formatted-date #(get-in % [:dispute/date-resolved])) + dispute-date-created-fn (partial formatted-date #(get % :dispute/date-created)) + dispute-date-resolved-fn (partial formatted-date #(get % :dispute/date-resolved)) contract-link-fn (fn [dispute] {:route :route.job/contract :params {:job-story-id (:job-story/id dispute)}}) amount-fn (fn [amount-source invoice] - (str (tokens/human-amount - (amount-source invoice) - (get-in invoice [:job :job/token-type]) - (get-in invoice [:job :token-details :token-detail/decimals])) - " " (get-in invoice [:job :token-details :token-detail/symbol]))) + (str (tokens/human-amount + (amount-source invoice) + (get-in invoice [:job :job/token-type]) + (get-in invoice [:job :token-details :token-detail/decimals])) + " " (get-in invoice [:job :token-details :token-detail/symbol]))) truncated-dispute-fn (fn [text-source invoice] - (let [text (get-in invoice [text-source] "") + (let [text (get invoice text-source "") max-chars 20] (if (empty? text) "" @@ -409,42 +423,59 @@ [:div.listing.my-employer-job-listing [c-table-listing resolved-table disputes contract-link-fn pagination]])])) -(defn c-my-employer-contract-listing [] + +(defn c-my-employer-contract-listing + [] (let [active-user (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session]))] [c-contract-listing :employer active-user])) -(defn c-my-employer-invoice-listing [] + +(defn c-my-employer-invoice-listing + [] (let [employer (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session]))] [c-invoice-listing :employer employer])) -(defn c-my-employer-dispute-listing [] + +(defn c-my-employer-dispute-listing + [] (c-dispute-listing :employer)) + ;; ;; Candidate Sections ;; -(defn c-my-candidate-contract-listing [] +(defn c-my-candidate-contract-listing + [] (let [active-user (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session]))] [c-contract-listing :candidate active-user])) -(defn c-my-candidate-invoice-listing [] + +(defn c-my-candidate-invoice-listing + [] (let [candidate (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session]))] [c-invoice-listing :candidate candidate])) -(defn c-my-candidate-dispute-listing [] + +(defn c-my-candidate-dispute-listing + [] [c-dispute-listing :candidate]) + ;; ;; Arbiter Sections ;; -(defn c-my-arbiter-job-listing [] +(defn c-my-arbiter-job-listing + [] [c-job-listing :arbiter]) -(defn c-my-arbiter-dispute-listing [] + +(defn c-my-arbiter-dispute-listing + [] [c-dispute-listing :arbiter]) + (defn c-sidebar [] [:div.sidebar @@ -466,18 +497,19 @@ [c-nav-sidebar-element "My Jobs" :my-arbiter-job-listing] [c-nav-sidebar-element "My Disputes" :my-arbiter-dispute-listing]]]) + (defn c-mobile-navigation [] (fn [] [c-mobile-sidebar [c-sidebar]])) -(defn c-listing [] + +(defn c-listing + [] (let [active-page (re/subscribe [::router.subs/active-page])] (fn [] - (let [{page :name - params :param - query :query} @active-page + (let [{query :query} @active-page *current-sidebar-choice (or (keyword (:sidebar query)) :my-employer-job-listing)] [:div.listing (case *current-sidebar-choice @@ -496,7 +528,8 @@ :my-arbiter-job-listing [c-my-arbiter-job-listing] :my-arbiter-dispute-listing [c-my-arbiter-dispute-listing] - (throw (ex-info "Unable to determine sidebar choice" *current-sidebar-choice)))])))) + (throw (ex-info "Unable to determine sidebar choice" {:current-sidebar-choice *current-sidebar-choice})))])))) + (defmethod page :route.me/index [] (fn [] diff --git a/ui/src/ethlance/ui/page/me/events.cljs b/ui/src/ethlance/ui/page/me/events.cljs index 8b3db4a0..f8e7bbb6 100644 --- a/ui/src/ethlance/ui/page/me/events.cljs +++ b/ui/src/ethlance/ui/page/me/events.cljs @@ -1,25 +1,29 @@ (ns ethlance.ui.page.me.events (:require - [ethlance.ui.event.utils :as event.utils] - [district.ui.router.effects :as router.effects] - [re-frame.core :as re])) + [ethlance.ui.event.utils :as event.utils] + [re-frame.core :as re])) + ;; ;; Page State ;; (def state-key :page.me) + + (def state-default {:pagination-limit 10 :pagination-offset 0}) + (def create-assoc-handler (partial event.utils/create-assoc-handler state-key)) + (defn initialize-page "Event FX Handler. Setup listener to dispatch an event when the page is active/visited." [{:keys [db]} _] - (let [page-state (get db state-key)] - {:db (assoc-in db [state-key] state-default)})) + {:db (assoc db state-key state-default)}) + ;; ;; Page Events diff --git a/ui/src/ethlance/ui/page/me/subscriptions.cljs b/ui/src/ethlance/ui/page/me/subscriptions.cljs index c27bcb5d..8b7b78bc 100644 --- a/ui/src/ethlance/ui/page/me/subscriptions.cljs +++ b/ui/src/ethlance/ui/page/me/subscriptions.cljs @@ -1,12 +1,11 @@ (ns ethlance.ui.page.me.subscriptions (:require - [re-frame.core :as re] - [ethlance.ui.subscription.utils :as subscription.utils] - [ethlance.ui.page.me.events :as me.events])) + [ethlance.ui.page.me.events :as me.events] + [ethlance.ui.subscription.utils :as subscription.utils] + [re-frame.core :as re])) + (def create-get-handler #(subscription.utils/create-get-handler me.events/state-key %)) (re/reg-sub :page.me/pagination-offset (create-get-handler :pagination-offset)) (re/reg-sub :page.me/pagination-limit (create-get-handler :pagination-limit)) - - diff --git a/ui/src/ethlance/ui/page/new_invoice.cljs b/ui/src/ethlance/ui/page/new_invoice.cljs index 612cb271..7c9aef75 100644 --- a/ui/src/ethlance/ui/page/new_invoice.cljs +++ b/ui/src/ethlance/ui/page/new_invoice.cljs @@ -1,12 +1,14 @@ (ns ethlance.ui.page.new-invoice - (:require [district.ui.component.page :refer [page]] - [district.ui.graphql.subs :as gql] - [ethlance.ui.component.icon :refer [c-icon]] - [ethlance.ui.component.main-layout :refer [c-main-layout]] - [ethlance.ui.component.select-input :refer [c-select-input]] - [ethlance.ui.component.textarea-input :refer [c-textarea-input]] - [ethlance.ui.component.token-amount-input :refer [c-token-amount-input]] - [re-frame.core :as re])) + (:require + [district.ui.component.page :refer [page]] + [district.ui.graphql.subs :as gql] + [ethlance.ui.component.icon :refer [c-icon]] + [ethlance.ui.component.main-layout :refer [c-main-layout]] + [ethlance.ui.component.select-input :refer [c-select-input]] + [ethlance.ui.component.textarea-input :refer [c-textarea-input]] + [ethlance.ui.component.token-amount-input :refer [c-token-amount-input]] + [re-frame.core :as re])) + (defmethod page :route.invoice/new [] (let [active-user (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) @@ -49,7 +51,6 @@ :items (sort-by :job-story/date-created ,,,) reverse) - token-display-name (-> @job-token :symbol (or ,,, "") name) token-display-name (name (or (@job-token :symbol) (@job-token :type) "")) job-token-decimals (get-in @*invoiced-job [:job :token-details :token-detail/decimals])] [c-main-layout {:container-opts {:class :new-invoice-main-container}} diff --git a/ui/src/ethlance/ui/page/new_invoice/events.cljs b/ui/src/ethlance/ui/page/new_invoice/events.cljs index ddb2e8be..e76231bf 100644 --- a/ui/src/ethlance/ui/page/new_invoice/events.cljs +++ b/ui/src/ethlance/ui/page/new_invoice/events.cljs @@ -1,27 +1,24 @@ (ns ethlance.ui.page.new-invoice.events - (:require [district.parsers :refer [parse-float parse-int]] - [district.ui.router.effects :as router.effects] - [ethlance.shared.utils :refer [eth->wei base58->hex]] - [ethlance.ui.event.utils :as event.utils] - [ethlance.ui.util.tokens :as util.tokens] - [district.ui.web3-tx.events :as web3-events] - [district.ui.notification.events :as notification.events] - [district.ui.router.events :as router-events] - [district.ui.smart-contracts.queries :as contract-queries] - [ethlance.shared.contract-constants :as contract-constants] - [district.ui.web3-accounts.queries :as accounts-queries] - [re-frame.core :as re] - - ; TODO: extract for Event decoding - - [district.ui.smart-contracts.queries :as smart-contracts.queries] - [district.ui.web3.queries :as web3-queries] - [cljs-web3-next.eth :as web3-eth] - [cljs-web3-next.helpers :as web3-helpers] - )) + (:require + [district.parsers :refer [parse-float parse-int]] + [district.ui.notification.events :as notification.events] + [district.ui.router.effects :as router.effects] + [district.ui.router.events :as router-events] + ;; TODO: extract for Event decoding + [district.ui.smart-contracts.queries :as smart-contracts.queries] + [district.ui.web3-accounts.queries :as accounts-queries] + [district.ui.web3-tx.events :as web3-events] + [district.ui.web3.queries :as web3-queries] + [ethlance.shared.contract-constants :as contract-constants] + [ethlance.shared.utils :refer [base58->hex]] + [ethlance.ui.event.utils :as event.utils] + [ethlance.ui.util.tokens :as util.tokens] + [re-frame.core :as re])) + (def state-key :page.new-invoice) + (def state-default {:invoiced-job nil :hours-worked nil @@ -29,8 +26,10 @@ :invoice-amount nil :message nil}) + (def create-assoc-handler (partial event.utils/create-assoc-handler state-key)) + (defn initialize-page "Event FX Handler. Setup listener to dispatch an event when the page is active/visited." [] @@ -39,12 +38,14 @@ :name :route.invoice/new :dispatch []}]}) + (re/reg-event-fx :page.new-invoice/initialize-page initialize-page) (re/reg-event-fx :page.new-invoice/set-hours-worked (create-assoc-handler :hours-worked parse-int)) (re/reg-event-fx :page.new-invoice/set-hourly-rate (create-assoc-handler :hourly-rate parse-float)) (re/reg-event-fx :page.new-invoice/set-invoice-amount (create-assoc-handler :invoice-amount)) (re/reg-event-fx :page.new-invoice/set-message (create-assoc-handler :message)) + (re/reg-event-fx :page.new-invoice/set-invoiced-job (fn [cofx [_ job]] @@ -53,13 +54,14 @@ load-eth-rate [:dispatch [:district.ui.conversion-rates.events/load-conversion-rates {:from-currencies [token-type] :to-currencies [:USD]}]]] (if (= token-type :eth) - (assoc-in updated-cofx [:fx] [load-eth-rate]) + (assoc updated-cofx :fx [load-eth-rate]) updated-cofx)))) + (re/reg-event-fx :page.new-invoice/send (fn [{:keys [db]}] - (let [db-invoice (get-in db [state-key]) + (let [db-invoice (get db state-key) ipfs-invoice {:invoice/amount-requested (get-in db-invoice [:invoice-amount :token-amount]) :invoice/hours-worked (:hours-worked db-invoice) :invoice/hourly-rate (:hourly-rate db-invoice) @@ -71,6 +73,7 @@ :on-success [:invoice-to-ipfs-success ipfs-invoice] :on-error [:invoice-to-ipfs-failure ipfs-invoice]}}))) + (re/reg-event-fx :invoice-to-ipfs-success (fn [cofx [_event ipfs-job ipfs-event]] @@ -93,7 +96,7 @@ tx-opts {:from creator :gas 10000000} ipfs-hash (-> ipfs-event :Hash base58->hex)] {:dispatch [::web3-events/send-tx - {:instance (contract-queries/instance (:db cofx) :job contract-address) + {:instance (smart-contracts.queries/instance (:db cofx) :job contract-address) :fn :create-invoice :args [[(clj->js offered-value)] ipfs-hash] :tx-opts tx-opts @@ -102,35 +105,33 @@ :on-tx-success [::send-invoice-tx-success ipfs-job] :on-tx-error [::send-invoice-tx-error ipfs-job]}]}))) + (re/reg-event-db ::invoice-to-ipfs-failure - (fn [db event] - (println ">>> ethlance.ui.page.new-invoice.events EVENT :invoice-to-ipfs-failure" event) - db)) + (fn [_db event] + (println ">>> ethlance.ui.page.new-invoice.events EVENT :invoice-to-ipfs-failure" event))) + (re/reg-event-fx ::tx-hash - (fn [db event] (println ">>> ethlance.ui.page.new-invoice.events :tx-hash" event))) + (fn [_db event] (println ">>> ethlance.ui.page.new-invoice.events :tx-hash" event))) + (re/reg-event-fx ::web3-tx-localstorage - (fn [db event] (println ">>> ethlance.ui.page.new-invoice.events :web3-tx-localstorage" event))) + (fn [_db event] (println ">>> ethlance.ui.page.new-invoice.events :web3-tx-localstorage" event))) -(def invoice-data (atom nil)) (re/reg-event-fx ::send-invoice-tx-success - (fn [{:keys [db]} [event-name ipfs-job tx-data]] - (let [web3 (web3-queries/web3 db) - contract-instance (smart-contracts.queries/instance db :ethlance) - raw-event (get-in tx-data [:events :0 :raw]) - invoice-created (util.tokens/parse-event web3 contract-instance raw-event :Invoice-created) - job-story-id (:job-story/id ipfs-job)] + (fn [{:keys [db]} [_event-name ipfs-job _tx-data]] + (let [job-story-id (:job-story/id ipfs-job)] (re/dispatch [::router-events/navigate :route.job/contract {:job-story-id job-story-id}]) {:dispatch [::notification.events/show "Transaction to create invoice processed successfully"] - :db (assoc-in db [state-key] state-default)}))) + :db (assoc db state-key state-default)}))) + (re/reg-event-db ::send-invoice-tx-error - (fn [db event] + (fn [_db event] (println ">>> got :create-job-tx-error event:" event))) diff --git a/ui/src/ethlance/ui/page/new_invoice/subscriptions.cljs b/ui/src/ethlance/ui/page/new_invoice/subscriptions.cljs index babd43d8..4def9a90 100644 --- a/ui/src/ethlance/ui/page/new_invoice/subscriptions.cljs +++ b/ui/src/ethlance/ui/page/new_invoice/subscriptions.cljs @@ -1,12 +1,10 @@ (ns ethlance.ui.page.new-invoice.subscriptions (:require - [re-frame.core :as re] - - [ethlance.ui.util.tokens :as util.tokens] - [district.ui.conversion-rates.queries :as rates-queries] - [district.ui.conversion-rates.subs :as rates-subs] - [ethlance.ui.page.new-invoice.events :as new-invoice.events] - [ethlance.ui.subscription.utils :as subscription.utils])) + [district.ui.conversion-rates.subs :as rates-subs] + [ethlance.ui.page.new-invoice.events :as new-invoice.events] + [ethlance.ui.subscription.utils :as subscription.utils] + [ethlance.ui.util.tokens :as util.tokens] + [re-frame.core :as re])) (def create-get-handler #(subscription.utils/create-get-handler new-invoice.events/state-key %)) @@ -23,6 +21,7 @@ (re/reg-sub :page.new-invoice/invoice-amount (create-get-handler :invoice-amount)) (re/reg-sub :page.new-invoice/message (create-get-handler :message)) + (re/reg-sub :page.new-invoice/job-token :<- [:page.new-invoice/invoiced-job] @@ -32,7 +31,8 @@ :amount (-> job :job :job/token-amount) :id (-> job :job :job/token-id) :name (-> job :job :token-details :token-detail/name) - :symbol (or (keyword (-> job :job :token-details :token-detail/symbol)))})) + :symbol (keyword (-> job :job :token-details :token-detail/symbol))})) + (re/reg-sub :page.new-invoice/estimated-usd diff --git a/ui/src/ethlance/ui/page/new_job.cljs b/ui/src/ethlance/ui/page/new_job.cljs index 9b08972e..8e3894b0 100644 --- a/ui/src/ethlance/ui/page/new_job.cljs +++ b/ui/src/ethlance/ui/page/new_job.cljs @@ -1,29 +1,27 @@ (ns ethlance.ui.page.new-job - (:require [district.ui.component.page :refer [page]] - [ethlance.shared.constants :as constants] - [ethlance.ui.component.button :refer [c-button c-button-label]] - [ethlance.ui.component.icon :refer [c-icon]] - [ethlance.ui.component.main-layout :refer [c-main-layout]] - [ethlance.ui.component.profile-image :refer [c-profile-image]] - [district.ui.graphql.subs :as gql] - [district.ui.web3-tx.events :as tx-events] - [district.ui.smart-contracts.queries :as contract-queries] - [ethlance.ui.page.new-job.events :as new-job.events] - [ethlance.ui.component.radio-select - :refer - [c-radio-secondary-element c-radio-select]] - [ethlance.ui.component.rating :refer [c-rating]] - [ethlance.ui.component.search-input :refer [c-chip-search-input]] - [ethlance.ui.component.select-input :refer [c-select-input]] - [ethlance.ui.component.text-input :refer [c-text-input]] - [ethlance.ui.component.token-amount-input :refer [c-token-amount-input]] - [ethlance.ui.component.textarea-input :refer [c-textarea-input]] - [ethlance.ui.util.component :refer [evt]] - [ethlance.ui.subscriptions :as subs] - [ethlance.ui.util.navigation :as navigation] - [ethlance.ui.util.job :as util.job] - [re-frame.core :as re] - [clojure.spec.alpha :as s])) + (:require + [clojure.spec.alpha :as s] + [district.ui.component.page :refer [page]] + [district.ui.graphql.subs :as gql] + [ethlance.shared.constants :as constants] + [ethlance.ui.component.button :refer [c-button c-button-label]] + [ethlance.ui.component.icon :refer [c-icon]] + [ethlance.ui.component.main-layout :refer [c-main-layout]] + [ethlance.ui.component.profile-image :refer [c-profile-image]] + [ethlance.ui.component.radio-select + :refer + [c-radio-secondary-element c-radio-select]] + [ethlance.ui.component.rating :refer [c-rating]] + [ethlance.ui.component.search-input :refer [c-chip-search-input]] + [ethlance.ui.component.select-input :refer [c-select-input]] + [ethlance.ui.component.text-input :refer [c-text-input]] + [ethlance.ui.component.textarea-input :refer [c-textarea-input]] + [ethlance.ui.component.token-amount-input :refer [c-token-amount-input]] + [ethlance.ui.util.component :refer [>evt]] + [ethlance.ui.util.job :as util.job] + [ethlance.ui.util.navigation :as navigation] + [re-frame.core :as re])) + (defn c-arbiter-for-hire [arbiter] @@ -45,27 +43,24 @@ :on-click #(re/dispatch [:page.new-job/uninvite-arbiter (:user/id arbiter)])} [c-button-label "Uninvite"]])])) -(defn- c-submit-button [{:keys [:on-submit :disabled?]}] - [:div.form-submit - {:class (when disabled? "disabled") - :on-click (fn [] (when-not disabled? (>evt on-submit)))} - [:span "Create"] - [c-icon {:name :ic-arrow-right :size :smaller}]]) -(defn radio-options-from-vector [options] +(defn radio-options-from-vector + [options] (map (fn [[kw desc]] [kw [c-radio-secondary-element desc]]) options)) -(defn c-job-creation-form [] + +(defn c-job-creation-form + [] (let [arbiters-query [:arbiter-search {:limit 1000} [[:items [:user/id [:user [:user/id :user/name :user/profile-image]] - :arbiter/bio - :arbiter/professional-title - :arbiter/rating - :arbiter/fee - :arbiter/fee-currency-id]]]] + :arbiter/bio + :arbiter/professional-title + :arbiter/rating + :arbiter/fee + :arbiter/fee-currency-id]]]] arbiters-result (re/subscribe [::gql/query {:queries [arbiters-query]}]) *bid-option (re/subscribe [:page.new-job/bid-option]) *category (re/subscribe [:page.new-job/category]) @@ -86,7 +81,6 @@ (fn [] (let [arbiters (get-in @arbiters-result [:arbiter-search :items]) with-token? (#{:erc20 :erc721 :erc1155} @*token-type) - with-nft? (#{:erc721} @*token-type) token-with-amount? (#{:erc20 :erc1155 :eth} @*token-type) token-with-id? (#{:erc721 :erc1155} @*token-type) disabled? (or @@ -172,7 +166,7 @@ [:erc1155 [c-radio-secondary-element "Multi-Token (ERC-1155)"]]] (when token-with-amount? [:div.token-address-input - ; Specialized for tokens (takes account the decimals from the contract) + ;; Specialized for tokens (takes account the decimals from the contract) [c-token-amount-input {:value @*token-amount :decimals @token-decimals @@ -211,13 +205,16 @@ [:div.label "Create"] [c-icon {:name :ic-arrow-right :size :small}]]])))) -(defn c-invite-to-create-employer-profile [user-id] + +(defn c-invite-to-create-employer-profile + [] [c-main-layout {:container-opts {:class :new-job-main-container}} [:div "Set up your employer profile to be able to create new jobs"] [:div.button {:on-click (navigation/create-handler {:route :route.me/sign-up :query {:tab "employer"}})} "Go to employer profile page"]]) + (defmethod page :route.job/new [] (let [active-user (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) query [:employer {:user/id active-user} diff --git a/ui/src/ethlance/ui/page/new_job/events.cljs b/ui/src/ethlance/ui/page/new_job/events.cljs index 6fe936c5..09af09d2 100644 --- a/ui/src/ethlance/ui/page/new_job/events.cljs +++ b/ui/src/ethlance/ui/page/new_job/events.cljs @@ -1,27 +1,31 @@ (ns ethlance.ui.page.new-job.events (:require - [alphabase.hex :as hex] + [cljs-web3-next.eth :as w3n-eth] + [district.ui.notification.events :as notification.events] [district.ui.router.effects :as router.effects] [district.ui.router.events :as router-events] - [cljs-web3-next.eth :as w3n-eth] - [ethlance.ui.event.utils :as event.utils] - [ethlance.shared.utils :refer [eth->wei base58->hex js-obj->clj-map]] - [re-frame.core :as re] - ["web3" :as w3] [district.ui.smart-contracts.queries :as contract-queries] [district.ui.web3-accounts.queries :as accounts-queries] - [district.ui.web3.queries :as web3-queries] [district.ui.web3-tx.events :as web3-events] - [district.ui.notification.events :as notification.events] - [ethlance.shared.contract-constants :as contract-constants])) + [district.ui.web3.queries :as web3-queries] + [ethlance.shared.contract-constants :as contract-constants] + [ethlance.shared.utils :refer [base58->hex js-obj->clj-map]] + [ethlance.ui.event.utils :as event.utils] + [re-frame.core :as re])) + (def state-key :page.new-job) (def new-job-params-path [state-key :job-creation-params]) -(defn get-job-creation-param [db param-key] + + +(defn get-job-creation-param + [db param-key] (get-in db (conj new-job-params-path param-key))) + (def interceptors [re/trim-v]) + (def state-default {:job/title "Rauamaak on meie saak" :job/description "Tee t88d ja n2e vaeva" @@ -39,6 +43,7 @@ :job/invited-arbiters #{} :job/token-decimals 18}) + (defn initialize-page "Event FX Handler. Setup listener to dispatch an event when the page is active/visited." [] @@ -47,12 +52,15 @@ :name :route.job/new :dispatch [:page.new-job/auto-fill-form]}]}) + (def create-assoc-handler (partial event.utils/create-assoc-handler state-key)) + (re/reg-event-db :page.new-job/auto-fill-form (fn [db] - (assoc-in db [state-key] state-default))) + (assoc db state-key state-default))) + (re/reg-event-fx :page.new-job/initialize-page initialize-page) (re/reg-event-fx :page.new-job/set-bid-option (create-assoc-handler :job/bid-option)) @@ -65,12 +73,14 @@ (re/reg-event-fx :page.new-job/set-required-skills (create-assoc-handler :job/required-skills)) (re/reg-event-fx :page.new-job/set-with-arbiter? (create-assoc-handler :job/with-arbiter?)) + (re/reg-event-db :page.new-job/invite-arbiter (fn [db [_ arbiter]] (assoc-in db [state-key :job/invited-arbiters] (conj (get-in db [state-key :job/invited-arbiters]) arbiter)))) + (re/reg-event-db :page.new-job/uninvite-arbiter (fn [db [_ arbiter]] @@ -78,8 +88,8 @@ (disj (get-in db [state-key :job/invited-arbiters]) arbiter)))) -; The simple setter implementation for eventual production -; (re/reg-event-fx :page.new-job/set-token-type (create-assoc-handler :job/token-type)) +;; The simple setter implementation for eventual production +;; (re/reg-event-fx :page.new-job/set-token-type (create-assoc-handler :job/token-type)) (re/reg-event-db :page.new-job/decimals-response @@ -87,7 +97,8 @@ (println ">>> DECIMALS RESPONSE" decimals) (assoc-in db [state-key :job/token-decimals] decimals))) -; Implementation to auto-fill testnet token data with address + +;; Implementation to auto-fill testnet token data with address (re/reg-event-fx :page.new-job/set-token-type (fn [{:keys [db] :as cofx} [_ token-type]] @@ -123,6 +134,7 @@ (re/reg-event-fx :page.new-job/set-token-address (create-assoc-handler :job/token-address)) (re/reg-event-fx :page.new-job/set-token-id (create-assoc-handler :job/token-id)) + (def db->ipfs-mapping {:job/bid-option :job/bid-option :job/category :job/category @@ -134,6 +146,7 @@ :job/title :job/title :job/invited-arbiters :job/invited-arbiters}) + (defn- db-job->ipfs-job "Useful for renaming map keys by reducing over a map of keyword -> keyword where: key is the name of the resulting map key and value is the key of the @@ -144,23 +157,27 @@ [job-data acc ipfs-key db-key] (assoc acc ipfs-key (job-data db-key))) -(defn set-tx-in-progress [db in-progress?] + +(defn set-tx-in-progress + [db in-progress?] (assoc-in db [state-key :tx-in-progress?] in-progress?)) + (re/reg-event-fx :page.new-job/create [interceptors] (fn [{:keys [db]}] - (let [db-job (get-in db [state-key]) + (let [db-job (get db state-key) ipfs-job (reduce-kv (partial db-job->ipfs-job db-job) {} db->ipfs-mapping)] {:ipfs/call {:func "add" :args [(js/Blob. [ipfs-job])] :on-success [:job-to-ipfs-success] :on-error [:job-to-ipfs-failure]}}))) + (re/reg-event-fx ::send-create-job-tx - (fn [{:keys [db] :as cofx} _] + (fn [{:keys [db]} _] (let [employer (get-job-creation-param db :employer) offered-value (get-job-creation-param db :offered-value) ipfs-hash (get-job-creation-param db :ipfs-hash) @@ -182,31 +199,35 @@ :on-tx-success [::create-job-tx-success] :on-tx-error [::create-job-tx-error]}]]]}))) + (re/reg-event-fx ::create-job-tx-receipt - (fn [cofx result] + (fn [_cofx result] (println ">>> ::create-job-tx-receipt" result))) + (re/reg-event-fx ::tx-hash-error - (fn [cofx result] + (fn [_cofx result] (println ">>> ⚠️ ⚠️ ::tx-hash-error" result))) (re/reg-event-fx ::erc20-allowance-approval-success - (fn [cofx result] + (fn [_cofx result] (println ">>> ::erc20-allowance-approval-success" result) {:fx [[:dispatch [::send-create-job-tx]]]})) + (re/reg-event-fx ::erc20-allowance-approval-error - (fn [cofx result] + (fn [_cofx result] (println ">>> ::erc20-allowance-approval-error" result))) + (re/reg-event-fx ::erc20-allowance-amount-success - (fn [{:keys [db] :as cofx} [_ result]] + (fn [{:keys [db]} [_ result]] (println ">>> ::erc20-allowance-amount-success" result (type result)) (let [offered-value (get-job-creation-param db :offered-value) amount (:value offered-value) @@ -228,14 +249,16 @@ [::send-create-job-tx] increase-allowance-event)]]}))) + (re/reg-event-fx ::erc20-allowance-amount-error - (fn [cofx result] + (fn [_cofx result] (println ">>> ::erc20-allowance-amount-error" result))) + (re/reg-event-fx ::ensure-erc20-allowance - (fn [{:keys [db] :as cofx} _] + (fn [{:keys [db] :as _cofx} _] (let [offered-value (get-job-creation-param db :offered-value) erc20-address (get-in offered-value [:token :tokenContract :tokenAddress]) erc20-abi (:erc20 ethlance.shared.contract-constants/abi) @@ -243,20 +266,23 @@ owner (get-job-creation-param db :employer) spender (contract-queries/contract-address db :ethlance)] {:fx [[:web3/call {:fns - [{:instance erc20-instance - :fn :allowance - :args [owner spender] - :on-success [::erc20-allowance-amount-success] ; handler needs to read app-db - :on-error [::erc20-allowance-amount-error]}]}]]}))) + [{:instance erc20-instance + :fn :allowance + :args [owner spender] + :on-success [::erc20-allowance-amount-success] ; handler needs to read app-db + :on-error [::erc20-allowance-amount-error]}]}]]}))) + -(defn job-creation-params [db] +(defn job-creation-params + [_db] {:offered-value nil :arbiters nil :ipfs-hash nil}) + (re/reg-event-fx ::safe-transfer-with-create-job - (fn [{:keys [db] :as cofx} _] + (fn [{:keys [db] :as _cofx} _] (let [offered-value (get-job-creation-param db :offered-value) amount (:value offered-value) token-address (get-in offered-value [:token :tokenContract :tokenAddress]) @@ -286,10 +312,10 @@ {:db (set-tx-in-progress db true) :fx [[:dispatch safe-transfer-with-create-job-tx]]}))) + (re/reg-event-fx :job-to-ipfs-success (fn [cofx event] - (println ">>> :job-to-ipfs-success" event) (let [creator (accounts-queries/active-account (:db cofx)) job-fields (get-in cofx [:db state-key]) token-type (:job/token-type job-fields) @@ -315,37 +341,39 @@ next-event {:eth ::send-create-job-tx :erc20 ::ensure-erc20-allowance :erc721 ::safe-transfer-with-create-job - :erc1155 ::safe-transfer-with-create-job} - ] + :erc1155 ::safe-transfer-with-create-job}] {:db (assoc-in (:db cofx) new-job-params-path new-job-params) :fx [[:dispatch [(get next-event token-type)]]]}))) + (re/reg-event-fx ::tx-hash - (fn [db event] (println ">>> ethlance.ui.page.new-job.events :tx-hash" event))) + (fn [_db event] (println ">>> ethlance.ui.page.new-job.events :tx-hash" event))) + + +(defn async-request-event + [{:keys [contract event block-number callback]}] + (-> (w3n-eth/get-past-events contract event {:from-block block-number :to-block block-number}) + (.then ,,, callback))) -(defn async-request-event [{:keys [contract event block-number callback]}] - (let [] - (-> (w3n-eth/get-past-events contract event {:from-block block-number :to-block block-number}) - (.then ,,, callback)))) (re/reg-event-fx ::create-job-tx-success - (fn [{:keys [db]} [event-name tx-data]] + (fn [{:keys [db]} [_event-name tx-data]] (let [job-from-event (get-in tx-data [:events :Job-created :return-values :job])] (println ">>> ::create-job-tx-success" tx-data) {:db (set-tx-in-progress db false) :fx [[:dispatch [::notification.events/show "Transaction to create job processed successfully"]] - ; When creating job via ERC721/1155 callback (onERC{721,1155}Received), the event data is part of the - ; tx-receipt, but doesn't have event name, making it difficult to find and decode. Thus this workaround: - ; - requesting the JobCreated event directly from Ethlance and receiving it correctly decoded + ;; When creating job via ERC721/1155 callback (onERC{721,1155}Received), the event data is part of the + ;; tx-receipt, but doesn't have event name, making it difficult to find and decode. Thus this workaround: + ;; - requesting the JobCreated event directly from Ethlance and receiving it correctly decoded (if job-from-event [:dispatch-later [{:ms 1000 :dispatch [::router-events/navigate :route.job/detail {:id (get-in tx-data [:events :Job-created :return-values :job])}]}]] (async-request-event {:event "allEvents" :block-number (:block-number tx-data) :contract (district.ui.smart-contracts.queries/instance db :ethlance) :callback (fn [result] - ; Delaying navigation to give server time to process the contract event + ;; Delaying navigation to give server time to process the contract event (js/setTimeout #(re/dispatch [::router-events/navigate @@ -353,6 +381,7 @@ {:id (get (js-obj->clj-map (.-returnValues (first result))) "job")}]) 1000))}))]}))) + (re/reg-event-db ::create-job-tx-error (fn [db event] @@ -360,7 +389,8 @@ {:db (set-tx-in-progress db false) :dispatch [::notification.events/show "Error with creating new job transaction"]})) + (re/reg-event-fx ::job-to-ipfs-failure (fn [_ _] - {:dispatch [::notification.events/show "Error with loading new job data to IPFS"]})) + {:dispatch [::notification.events/show "Error with loading new job data to IPFS"]})) diff --git a/ui/src/ethlance/ui/page/new_job/subscriptions.cljs b/ui/src/ethlance/ui/page/new_job/subscriptions.cljs index 152ac54f..2c2cfcac 100644 --- a/ui/src/ethlance/ui/page/new_job/subscriptions.cljs +++ b/ui/src/ethlance/ui/page/new_job/subscriptions.cljs @@ -1,9 +1,8 @@ (ns ethlance.ui.page.new-job.subscriptions (:require - [re-frame.core :as re] - - [ethlance.ui.page.new-job.events :as new-job.events] - [ethlance.ui.subscription.utils :as subscription.utils])) + [ethlance.ui.page.new-job.events :as new-job.events] + [ethlance.ui.subscription.utils :as subscription.utils] + [re-frame.core :as re])) (def create-get-handler #(subscription.utils/create-get-handler new-job.events/state-key %)) @@ -24,12 +23,15 @@ (re/reg-sub :page.new-job/token-type (create-get-handler :job/token-type)) (re/reg-sub :page.new-job/token-decimals (create-get-handler :job/token-decimals)) -; (re/reg-sub :page.new-job/token-amount (create-get-handler :job/token-amount)) + + +;; (re/reg-sub :page.new-job/token-amount (create-get-handler :job/token-amount)) (re/reg-sub :page.new-job/token-amount (fn [db _] (get-in db [new-job.events/state-key :job/token-amount :human-amount]))) + (re/reg-sub :page.new-job/token-address (create-get-handler :job/token-address)) (re/reg-sub :page.new-job/token-id (create-get-handler :job/token-id)) diff --git a/ui/src/ethlance/ui/page/profile.cljs b/ui/src/ethlance/ui/page/profile.cljs index 184841ed..9efb272c 100644 --- a/ui/src/ethlance/ui/page/profile.cljs +++ b/ui/src/ethlance/ui/page/profile.cljs @@ -1,50 +1,58 @@ (ns ethlance.ui.page.profile - (:require [district.ui.component.page :refer [page]] - [ethlance.ui.page.profile.subscriptions] - [ethlance.ui.component.button :refer [c-button c-button-label c-button-icon-label]] - [ethlance.ui.component.carousel :refer [c-carousel c-feedback-slide]] - [ethlance.ui.component.circle-button :refer [c-circle-icon-button]] - [ethlance.ui.component.main-layout :refer [c-main-layout]] - [ethlance.ui.component.profile-image :refer [c-profile-image]] - [ethlance.ui.component.rating :refer [c-rating]] - [ethlance.ui.component.scrollable :refer [c-scrollable]] - [ethlance.ui.component.table :refer [c-table]] - [ethlance.ui.component.tabular-layout :refer [c-tabular-layout]] - [ethlance.ui.component.select-input :refer [c-select-input]] - [ethlance.ui.component.tag :refer [c-tag c-tag-label]] - [ethlance.ui.component.textarea-input :refer [c-textarea-input]] - [ethlance.ui.component.pagination :refer [c-pagination-ends]] - [district.ui.router.subs :as router-subs] - [ethlance.ui.util.navigation :as navigation] - [district.ui.router.events :as router-events] - [ethlance.ui.page.profile.events :as profile-events] - [ethlance.shared.utils :refer [ilike= ilike!=]] - [district.format :as format] - [cljsjs.graphql] - [clojure.string :as string] - [ethlance.ui.util.dates :as util.dates] - [district.graphql-utils :as utils] - [district.ui.graphql.subs :as gql] - [re-frame.core :as re])) - -(defn c-tag-list [name tags] + (:require + [cljsjs.graphql] + [clojure.string :as string] + [district.ui.component.page :refer [page]] + [district.ui.graphql.subs :as gql] + [district.ui.router.events :as router-events] + [district.ui.router.subs :as router-subs] + [ethlance.shared.utils :refer [ilike=]] + [ethlance.ui.component.button :refer [c-button c-button-label c-button-icon-label]] + [ethlance.ui.component.carousel :refer [c-carousel c-feedback-slide]] + [ethlance.ui.component.main-layout :refer [c-main-layout]] + [ethlance.ui.component.pagination :refer [c-pagination-ends]] + [ethlance.ui.component.profile-image :refer [c-profile-image]] + [ethlance.ui.component.rating :refer [c-rating]] + [ethlance.ui.component.scrollable :refer [c-scrollable]] + [ethlance.ui.component.select-input :refer [c-select-input]] + [ethlance.ui.component.table :refer [c-table]] + [ethlance.ui.component.tabular-layout :refer [c-tabular-layout]] + [ethlance.ui.component.tag :refer [c-tag c-tag-label]] + [ethlance.ui.component.textarea-input :refer [c-textarea-input]] + [ethlance.ui.page.profile.events :as profile-events] + [ethlance.ui.page.profile.subscriptions] + [ethlance.ui.util.dates :as util.dates] + [ethlance.ui.util.navigation :as navigation] + [re-frame.core :as re])) + + +(defn c-tag-list + [name tags] (let [container [:div {:class (string/lower-case name)} [:span name]]] (into container (map #(vector c-tag {} [c-tag-label %]) tags)))) -(defn- format-date-looking-column [column value] + +(defn- format-date-looking-column + [column value] (if (string/ends-with? (name column) "-date") (util.dates/formatted-date value) value)) -(defn c-job-activity-row [job column-names] + +(defn c-job-activity-row + [job column-names] (map #(conj [] :span (format-date-looking-column % (get job %))) column-names)) -(defn prepare-candidate-jobs [story] + +(defn prepare-candidate-jobs + [story] {:title (get-in story [:job :job/title]) - :start-date (get-in story [:job-story/date-created]) - :status (get-in story [:job-story/status])}) + :start-date (get story :job-story/date-created) + :status (get story :job-story/status)}) + -(defn c-job-activity [user-role] +(defn c-job-activity + [user-role] (let [keys-headers {:title "Title" :start-date "Created" :status "Status"} headers (map last keys-headers) column-names (map first keys-headers) @@ -70,24 +78,28 @@ total-count (get-in results [:job-story-search :total-count]) jobs (map prepare-candidate-jobs (get-in results [:job-story-search :items]))] [:div.job-listing - [:div.title "Job Activity"] - [c-scrollable - {:forceVisible true :autoHide false} - (into [c-table {:headers headers}] (map #(c-job-activity-row % column-names) jobs))] - - [c-pagination-ends - {:total-count total-count - :limit limit - :offset offset - :set-offset-event :page.profile/set-pagination-offset}]])) - -(defn prepare-arbitrations [arbitration] + [:div.title "Job Activity"] + [c-scrollable + {:forceVisible true :autoHide false} + (into [c-table {:headers headers}] (map #(c-job-activity-row % column-names) jobs))] + + [c-pagination-ends + {:total-count total-count + :limit limit + :offset offset + :set-offset-event :page.profile/set-pagination-offset}]])) + + +(defn prepare-arbitrations + [arbitration] {:title (get-in arbitration [:job :job/title]) - :start-date (get-in arbitration [:arbitration/date-arbiter-accepted]) ; - :fee (str (get-in arbitration [:arbitration/fee]) " " (get-in arbitration [:arbitration/fee-currency-id])) - :status (get-in arbitration [:arbitration/status])}) + :start-date (get arbitration :arbitration/date-arbiter-accepted) ; + :fee (str (get arbitration :arbitration/fee) " " (get arbitration :arbitration/fee-currency-id)) + :status (get arbitration :arbitration/status)}) + -(defn c-arbitration-activity [] +(defn c-arbitration-activity + [] (let [keys-headers {:title "Title" :start-date "Hired" :fee "Fee" :status "Status"} headers (map last keys-headers) column-names (map first keys-headers) @@ -96,33 +108,34 @@ limit @(re/subscribe [:page.profile/pagination-limit]) offset @(re/subscribe [:page.profile/pagination-offset]) query [:arbiter {:user/id user-address} - [ - [:arbitrations {:limit limit :offset offset} + [[:arbitrations {:limit limit :offset offset} [:total-count [:items [:id :arbitration/date-arbiter-accepted - :arbitration/fee - :arbitration/fee-currency-id - :arbitration/status - [:job - [:job/title]]]]]]]] + :arbitration/fee + :arbitration/fee-currency-id + :arbitration/status + [:job + [:job/title]]]]]]]] results @(re/subscribe [::gql/query {:queries [query]} {:refetch-on #{::profile-events/invite-arbiter-tx-success}}]) total-count (get-in results [:arbiter :arbitrations :total-count]) arbitrations (map prepare-arbitrations (get-in results [:arbiter :arbitrations :items]))] [:div.job-listing - [:div.title "Arbitrations"] - [c-scrollable - {:forceVisible true :autoHide false} - (into [c-table {:headers headers}] (map #(c-job-activity-row % column-names) arbitrations))] - - [c-pagination-ends - {:total-count total-count - :limit limit - :offset offset - :set-offset-event :page.profile/set-pagination-offset}]])) - -(defn c-invite-candidate [] + [:div.title "Arbitrations"] + [c-scrollable + {:forceVisible true :autoHide false} + (into [c-table {:headers headers}] (map #(c-job-activity-row % column-names) arbitrations))] + + [c-pagination-ends + {:total-count total-count + :limit limit + :offset offset + :set-offset-event :page.profile/set-pagination-offset}]])) + + +(defn c-invite-candidate + [] (let [{:keys [_ params _]} @(re/subscribe [::router-subs/active-page]) candidate-address (:address params) active-user (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) @@ -150,75 +163,71 @@ " (candidate already proposed)")) jobs (sort-by :job/date-created #(compare %2 %1) (reduce (fn [acc job] - (if (some #(ilike= candidate-address (-> % :candidate :user/id)) (-> job :job-stories :items)) - (conj acc (merge job {:comment (existing-relation job) - :job-story-exists? true})) - (conj acc job))) - [] - all-jobs)) + (if (some #(ilike= candidate-address (-> % :candidate :user/id)) (-> job :job-stories :items)) + (conj acc (merge job {:comment (existing-relation job) + :job-story-exists? true})) + (conj acc job))) + [] + all-jobs)) job-for-invitation (re/subscribe [:page.profile/job-for-invitation]) invitation-text (re/subscribe [:page.profile/invitation-text]) preselected-job (or @job-for-invitation (first jobs)) job-story-exists? (:job-story-exists? preselected-job)] [:div.job-listing - [:div.title "Invite to a job"] - [c-select-input - {:selections jobs - :value-fn :job/id - :label-fn #(str (:job/title %) (:comment %)) - :selection preselected-job - :on-select #(re/dispatch [:page.profile/set-job-for-invitation %])}] - [c-textarea-input {:value @invitation-text - :disabled job-story-exists? - :placeholder "Briefly describe to what and why you're inviting the candidate" - :on-change #(re/dispatch [:page.profile/set-invitation-text %])}] - [c-button {:color :primary - :disabled? job-story-exists? - :on-click (fn [] - (when-not job-story-exists? - (re/dispatch [:page.profile/invite-candidate + [:div.title "Invite to a job"] + [c-select-input + {:selections jobs + :value-fn :job/id + :label-fn #(str (:job/title %) (:comment %)) + :selection preselected-job + :on-select #(re/dispatch [:page.profile/set-job-for-invitation %])}] + [c-textarea-input {:value @invitation-text + :disabled job-story-exists? + :placeholder "Briefly describe to what and why you're inviting the candidate" + :on-change #(re/dispatch [:page.profile/set-invitation-text %])}] + [c-button {:color :primary + :disabled? job-story-exists? + :on-click (fn [] + (when-not job-story-exists? + (re/dispatch [:page.profile/invite-candidate {:candidate candidate-address :text @invitation-text :job preselected-job :employer active-user}])))} - [c-button-label "Invite"]]])) + [c-button-label "Invite"]]])) + -(defn c-rating-box [rating] +(defn c-rating-box + [rating] [:div.rating [c-rating {:rating (:average rating) :color :primary}] [:span (str "(" (:count rating) ")")]]) -(defn c-feedback-listing [sub-title feedback-list] - [:div.feedback-listing - [:div.title "Feedback"] - [:div.sub-title sub-title] - (if (not (empty? feedback-list)) - (into [c-carousel {}] (map #(c-feedback-slide %) feedback-list)) - [:div.info-message "This user is yet to receive feedback"])]) -(def log (.-log js/console)) +(defn c-feedback-listing + [sub-title feedback-list] + [:div.feedback-listing + [:div.title "Feedback"] + [:div.sub-title sub-title] + (if (not-empty feedback-list) + (into [c-carousel {}] (map #(c-feedback-slide %) feedback-list)) + [:div.info-message "This user is yet to receive feedback"])]) -(defn prepare-ratings [rating] - {:rating (:feedback/rating rating) - :from (get-in rating [:feedback/from-user :user/name]) - :text (:feedback/text rating)}) -(defn prepare-feedback-cards [item] +(defn prepare-feedback-cards + [item] {:rating (:feedback/rating item) :text (:feedback/text item) :image-url (-> item :feedback/from-user :user/profile-image) :author (get-in item [:feedback/from-user :user/name])}) -(defn prepare-employer-jobs [story] - {:title (get-in story [:job :job/title]) - :start-date (get-in story [:job-story/date-created]) - :status (get-in story [:job :job/status])}) -(defn c-missing-profile-notification [profile-type] +(defn c-missing-profile-notification + [profile-type] (let [viewing-user-address (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) viewed-user-address @(re/subscribe [:page.profile/viewed-user-address]) viewing-own-profile? (ilike= viewing-user-address viewed-user-address) - subject (if viewing-own-profile? "You have" "This user has" ) + subject (if viewing-own-profile? "You have" "This user has") posessive (if viewing-own-profile? "your" "their") role-str (name profile-type)] [:div.candidate-profile @@ -229,9 +238,10 @@ :query {:tab profile-type}})) [c-button-label "Go to profile setup"]])])) -(defn c-candidate-profile [] - (let [page-params (re/subscribe [::router-subs/active-page-params]) - user-address @(re/subscribe [:page.profile/viewed-user-address]) + +(defn c-candidate-profile + [] + (let [user-address @(re/subscribe [:page.profile/viewed-user-address]) query [:candidate {:user/id user-address} [:candidate/professional-title :candidate/skills @@ -288,7 +298,9 @@ (when has-candidate-profile? [c-invite-candidate]) (c-feedback-listing professional-title feedback-list)])) -(defn c-employer-profile [] + +(defn c-employer-profile + [] (let [user-address (re/subscribe [:page.profile/viewed-user-address]) query [:employer {:user/id @user-address} [:employer/professional-title @@ -336,7 +348,9 @@ (when has-employer-profile? (c-job-activity :employer)) (c-feedback-listing professional-title feedback-list)])) -(defn c-invite-arbiter [] + +(defn c-invite-arbiter + [] (let [{:keys [_ params _]} @(re/subscribe [::router-subs/active-page]) invitee-address (:address params) @@ -356,39 +370,41 @@ all-jobs (get-in (first result) [:job-search :items] []) jobs (sort-by :job/date-created #(compare %2 %1) (reduce (fn [acc job] - (if (some #(ilike= invitee-address (-> % :arbiter :user/id)) (-> job :arbitrations :items)) - acc ; To show them in the list: (conj acc (merge job {:comment "(already invited)" :job-story-exists? true})) - (conj acc job))) - [] - all-jobs)) + (if (some #(ilike= invitee-address (-> % :arbiter :user/id)) (-> job :arbitrations :items)) + acc ; To show them in the list: (conj acc (merge job {:comment "(already invited)" :job-story-exists? true})) + (conj acc job))) + [] + all-jobs)) job-for-invitation (re/subscribe [:page.profile/job-for-invitation]) invitation-text (re/subscribe [:page.profile/invitation-text]) preselected-job (or @job-for-invitation (first jobs)) job-story-exists? (:job-story-exists? preselected-job)] [:div.job-listing - [:div.title "Invite Arbiter"] - [c-select-input - {:selections jobs - :value-fn :job/id - :label-fn #(str (:job/title %) (:comment %)) - :selection preselected-job - :on-select #(re/dispatch [:page.profile/set-job-for-invitation %])}] - [c-textarea-input {:value @invitation-text - :disabled job-story-exists? - :placeholder "Briefly describe to what and why you're inviting the arbiter" - :on-change #(re/dispatch [:page.profile/set-invitation-text %])}] - [c-button {:color :primary - :disabled? job-story-exists? - :on-click (fn [] - (when-not job-story-exists? - (re/dispatch [:page.profile/invite-arbiter + [:div.title "Invite Arbiter"] + [c-select-input + {:selections jobs + :value-fn :job/id + :label-fn #(str (:job/title %) (:comment %)) + :selection preselected-job + :on-select #(re/dispatch [:page.profile/set-job-for-invitation %])}] + [c-textarea-input {:value @invitation-text + :disabled job-story-exists? + :placeholder "Briefly describe to what and why you're inviting the arbiter" + :on-change #(re/dispatch [:page.profile/set-invitation-text %])}] + [c-button {:color :primary + :disabled? job-story-exists? + :on-click (fn [] + (when-not job-story-exists? + (re/dispatch [:page.profile/invite-arbiter {:arbiter invitee-address :text @invitation-text :job preselected-job :employer active-user}])))} - [c-button-label "Invite"]]])) + [c-button-label "Invite"]]])) + -(defn c-arbiter-profile [] +(defn c-arbiter-profile + [] (let [user-address (re/subscribe [:page.profile/viewed-user-address]) query [:arbiter {:user/id @user-address} [:arbiter/professional-title @@ -404,12 +420,12 @@ [:arbiter/feedback [:total-count [:items - [:message/id - :feedback/text - :feedback/rating - [:feedback/from-user - [:user/name - :user/profile-image]]]]]]]] + [:message/id + :feedback/text + :feedback/rating + [:feedback/from-user + [:user/name + :user/profile-image]]]]]]]] results (re/subscribe [::gql/query {:queries [query]}]) name (get-in @results [:arbiter :user :user/name]) location (get-in @results [:arbiter :user :user/country]) @@ -439,15 +455,17 @@ (when has-arbiter-profile? (c-arbitration-activity)) (c-feedback-listing professional-title feedback-list)])) + (defmethod page :route.user/profile [] - (let [{:keys [name params query]} @(re/subscribe [::router-subs/active-page]) + (let [{:keys [_name _params query]} @(re/subscribe [::router-subs/active-page]) user-address @(re/subscribe [:page.profile/viewed-user-address]) tabs {"candidate" 0 "employer" 1 "arbiter" 2} default-tab (get tabs (:tab query) 0) - navigate-to (fn [tab name params] (when name (re/dispatch [::router-events/navigate - :route.user/profile - {:address user-address} - (merge query {:tab tab})]))) + navigate-to (fn [tab name _params] + (when name (re/dispatch [::router-events/navigate + :route.user/profile + {:address user-address} + (merge query {:tab tab})]))) navigate-to-candidate (partial navigate-to "candidate" user-address) navigate-to-employer (partial navigate-to "employer" user-address) navigate-to-arbiter (partial navigate-to "arbiter" user-address)] diff --git a/ui/src/ethlance/ui/page/profile/events.cljs b/ui/src/ethlance/ui/page/profile/events.cljs index d487f46a..ce1fea0e 100644 --- a/ui/src/ethlance/ui/page/profile/events.cljs +++ b/ui/src/ethlance/ui/page/profile/events.cljs @@ -1,34 +1,40 @@ (ns ethlance.ui.page.profile.events - (:require [district.ui.router.effects :as router.effects] - [district.ui.router.queries :refer [active-page-params]] - [ethlance.ui.event.utils :as event.utils] - [ethlance.shared.utils :refer [eth->wei base58->hex]] - [district.ui.notification.events :as notification.events] - [district.ui.smart-contracts.queries :as contract-queries] - [district.ui.web3-tx.events :as web3-events] - [re-frame.core :as re])) + (:require + [district.ui.notification.events :as notification.events] + [district.ui.router.effects :as router.effects] + [district.ui.smart-contracts.queries :as contract-queries] + [district.ui.web3-tx.events :as web3-events] + [ethlance.shared.utils :refer [base58->hex]] + [ethlance.ui.event.utils :as event.utils] + [re-frame.core :as re])) + ;; Page State (def state-key :page.profile) + + (def state-default {:pagination-limit 5 :pagination-offset 0}) + (defn initialize-page "Event FX Handler. Setup listener to dispatch an event when the page is active/visited." [{:keys [db]} _] - (let [page-state (get db state-key)] - {::router.effects/watch-active-page - [{:id :page.profile/initialize-page - :name :route.user/profile - :dispatch []}] - :db (assoc-in db [state-key] state-default)})) - -(defn clear-forms [db] + {::router.effects/watch-active-page + [{:id :page.profile/initialize-page + :name :route.user/profile + :dispatch []}] + :db (assoc db state-key state-default)}) + + +(defn clear-forms + [db] (let [field-names [:invitation-text :job-for-invitation]] (reduce (fn [acc field] (assoc-in acc [state-key field] nil)) db field-names))) + ;; ;; Registered Events ;; @@ -39,9 +45,10 @@ (re/reg-event-fx :page.profile/set-invitation-text (create-assoc-handler :invitation-text)) (re/reg-event-fx :page.profile/set-pagination-offset (create-assoc-handler :pagination-offset)) + (re/reg-event-fx :page.profile/invite-candidate - (fn [{:keys [db]} [_ invitation-data]] + (fn [_cofx [_ invitation-data]] (let [ipfs-invitation {:candidate (:candidate invitation-data) :employer (:employer invitation-data) :job-story-message/type :invitation @@ -53,6 +60,7 @@ :on-success [:invitation-to-ipfs-success ipfs-invitation] :on-error [:invitation-to-ipfs-failure ipfs-invitation]}}))) + (re/reg-event-fx :invitation-to-ipfs-success (fn [{:keys [db]} [_event ipfs-invitation ipfs-event]] @@ -61,7 +69,7 @@ job-contract-address (:job/id ipfs-invitation) candidate (:candidate ipfs-invitation) tx-opts {:from creator :gas 10000000}] - {:dispatch [::web3-events/send-tx + {:dispatch [::web3-events/send-tx {:instance (contract-queries/instance db :job job-contract-address) :fn :add-candidate :args [candidate ipfs-hash] @@ -71,6 +79,7 @@ :on-tx-success [::invite-candidate-tx-success] :on-tx-error [::invite-candidate-tx-failure]}]}))) + (re/reg-event-fx :page.profile/invite-arbiter (fn [cofx [_ event-data]] @@ -80,7 +89,7 @@ instance (contract-queries/instance (:db cofx) :job job-address) tx-opts {:from employer-address :gas 10000000} contract-args [employer-address [arbiter-address]]] - {:dispatch [::web3-events/send-tx + {:dispatch [::web3-events/send-tx {:instance instance :fn :invite-arbiters :args contract-args @@ -90,31 +99,34 @@ :on-tx-success [::invite-arbiter-tx-success] :on-tx-error [::invite-arbiters-tx-error]}]}))) + (re/reg-event-db ::tx-hash-error - (fn [db event] + (fn [_db event] (println ">>>ui.page.profile ::tx-hash-error" event))) + (re/reg-event-db ::invitation-to-ipfs-failure - (fn [db event] - (println ">>> :invitation-to-ipfs-failure" event) - db)) + (fn [_db event] + (println ">>> :invitation-to-ipfs-failure" event))) + (re/reg-event-fx ::invite-candidate-tx-success - (fn [{:keys [db]} event] + (fn [{:keys [db]} _event] {:db (clear-forms db) :dispatch [::notification.events/show "Transaction to invite candidate processed successfully"]})) + (re/reg-event-db ::invite-candidate-tx-failure - (fn [db event] - (println ">>> ::invite-candidate-tx-failure" event) - db)) + (fn [_db event] + (println ">>> ::invite-candidate-tx-failure" event))) + (re/reg-event-fx ::invite-arbiter-tx-success - (fn [{:keys [db] :as cofx} event] + (fn [{:keys [db]} _event] {:db (clear-forms db) :dispatch [::notification.events/show "Transaction to invite arbiter processed successfully"]})) diff --git a/ui/src/ethlance/ui/page/profile/subscriptions.cljs b/ui/src/ethlance/ui/page/profile/subscriptions.cljs index d38522d0..b39722dd 100644 --- a/ui/src/ethlance/ui/page/profile/subscriptions.cljs +++ b/ui/src/ethlance/ui/page/profile/subscriptions.cljs @@ -1,9 +1,10 @@ (ns ethlance.ui.page.profile.subscriptions - (:require [ethlance.ui.page.profile.events :as profile.events] - [re-frame.core :as re] - [district.ui.router.subs :as router-subs] - [ethlance.ui.subscriptions :as ethlance-subs] - [ethlance.ui.subscription.utils :as subscription.utils])) + (:require + [district.ui.router.subs :as router-subs] + [ethlance.ui.page.profile.events :as profile.events] + [ethlance.ui.subscription.utils :as subscription.utils] + [ethlance.ui.subscriptions :as ethlance-subs] + [re-frame.core :as re])) (def create-get-handler #(subscription.utils/create-get-handler profile.events/state-key %)) @@ -13,6 +14,7 @@ (re/reg-sub :page.profile/pagination-offset (create-get-handler :pagination-offset)) (re/reg-sub :page.profile/pagination-limit (create-get-handler :pagination-limit)) + (re/reg-sub :page.profile/viewed-user-address :<- [::router-subs/active-page-params] diff --git a/ui/src/ethlance/ui/page/sign_up.cljs b/ui/src/ethlance/ui/page/sign_up.cljs index 29c215a1..0b63b6cf 100644 --- a/ui/src/ethlance/ui/page/sign_up.cljs +++ b/ui/src/ethlance/ui/page/sign_up.cljs @@ -1,13 +1,12 @@ (ns ethlance.ui.page.sign-up (:require + [clojure.spec.alpha :as s] [cuerdas.core :as str] [district.ui.component.page :refer [page]] - [ethlance.ui.component.modal.events] - [district.ui.router.events :as router-events] - [ethlance.ui.page.sign-up.events :as sign-up.events] + [district.ui.graphql.subs :as gql] [district.ui.notification.subs :as noti-subs] + [district.ui.router.events :as router-events] [district.ui.router.subs :as router.subs] - [district.ui.graphql.subs :as gql] [ethlance.shared.constants :as constants] [ethlance.shared.spec :refer [validate-keys]] [ethlance.ui.component.button :refer [c-button c-button-icon-label]] @@ -16,22 +15,23 @@ [ethlance.ui.component.file-drag-input :refer [c-file-drag-input]] [ethlance.ui.component.icon :refer [c-icon]] [ethlance.ui.component.main-layout :refer [c-main-layout]] + [ethlance.ui.component.modal.events] [ethlance.ui.component.search-input :refer [c-chip-search-input]] [ethlance.ui.component.select-input :refer [c-select-input]] [ethlance.ui.component.tabular-layout :refer [c-tabular-layout]] [ethlance.ui.component.text-input :refer [c-text-input]] [ethlance.ui.component.textarea-input :refer [c-textarea-input]] + [ethlance.ui.page.sign-up.events :as sign-up.events] [ethlance.ui.page.sign-up.subscriptions] - [ethlance.ui.subscriptions :as subs] - [ethlance.ui.util.component :refer [evt]] + [ethlance.ui.util.component :refer [>evt]] [ethlance.ui.util.navigation :as navigation-utils] [re-frame.core :as re] [reagent.core :as r] - [taoensso.timbre :as log] - [clojure.spec.alpha :as s])) + [taoensso.timbre :as log])) -(defn- c-upload-image [] +(defn- c-upload-image + [] (let [form-data (r/atom {})] (fn [] [:div.upload-image @@ -51,7 +51,8 @@ (log/warn "Rejected file" {:name name :type type :size size} ::file-rejected))}]]))) -(defn- c-user-name-input [{:keys [:form-values :form-validation]}] +(defn- c-user-name-input + [{:keys [:form-values :form-validation]}] [:div.form-name [c-text-input {:placeholder "Name" @@ -60,7 +61,8 @@ :on-change #(>evt [:page.sign-up/set-user-name %])}]]) -(defn- c-user-email-input [{:keys [:form-values :form-validation]}] +(defn- c-user-email-input + [{:keys [:form-values :form-validation]}] [:div.form-email [c-email-input {:placeholder "Email" @@ -69,7 +71,8 @@ :on-change #(>evt [:page.sign-up/set-user-email %])}]]) -(defn- c-user-country-input [{:keys [:form-values]}] +(defn- c-user-country-input + [{:keys [:form-values]}] [:div.form-country [c-select-input {:label "Select Country" @@ -80,34 +83,8 @@ :default-search-text "Search Countries"}]]) -(defn- c-user-github-input [{:keys [:form-values :gh-client-id :root-url]}] - [:div.form-connect-github - [c-button - {:size :large - :disabled? (not (nil? (:user/github-username form-values))) - :href (str "https://github.com/login/oauth/authorize?" - "client_id=" gh-client-id - "&scope=user" - "&redirect_uri=" - (navigation-utils/url-encode (str root-url "/me/sign-up?tab=candidate&social=github")))} - [c-button-icon-label {:icon-name :github :label-text "Connect Github" :inline? false}]]]) - - -(defn- c-user-linkedin-input [{:keys [:form-values :linkedin-client-id :root-url]}] - [:div.form-connect-linkedin - [c-button - {:size :large - :disabled? (not (nil? (:user/linkedin-username form-values))) - :href (str "https://www.linkedin.com/oauth/v2/authorization?" - "client_id=" linkedin-client-id - "&scope=r_liteprofile%20r_emailaddress" - "&response_type=code" - "&redirect_uri=" - (navigation-utils/url-encode (str root-url "/me/sign-up?tab=candidate&social=linkedin")))} - [c-button-icon-label {:icon-name :linkedin :label-text "Connect LinkedIn" :inline? false}]]]) - - -(defn- c-user-languages-input [{:keys [:form-values]}] +(defn- c-user-languages-input + [{:keys [:form-values]}] [:<> [:div.label [:h2 "Languages You Speak"]] [c-chip-search-input @@ -119,7 +96,8 @@ :on-chip-listing-change #(>evt [:page.sign-up/set-user-languages %])}]]) -(defn- c-bio [{:keys [:on-change :value]}] +(defn- c-bio + [{:keys [:on-change :value]}] [:<> [:div.label [:h2 "Your Biography"]] [c-textarea-input @@ -128,21 +106,24 @@ :on-change on-change}]]) -(defn- c-submit-button [{:keys [:on-submit :disabled?]}] +(defn- c-submit-button + [{:keys [:on-submit :disabled?]}] [:div.form-submit {:class (when disabled? "disabled") :on-click (fn [] (when-not disabled? (>evt on-submit)))} [:span "Save"] [c-icon {:name :ic-arrow-right :size :smaller}]]) -(defn c-candidate-sign-up [] + +(defn c-candidate-sign-up + [] (let [user-id (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) candidate-query [:candidate {:user/id user-id} sign-up.events/candidate-fields] user-query [:user {:user/id user-id} sign-up.events/user-fields] results (re/subscribe [::gql/query {:queries [candidate-query user-query]}]) sign-up-form (re/subscribe [:page.sign-up/form]) - form-values (merge (get-in @results [:candidate]) - (get-in @results [:user]) + form-values (merge (get @results :candidate) + (get @results :user) @sign-up-form) form-validation (validate-keys form-values)] [:div.candidate-sign-up @@ -202,14 +183,15 @@ :disabled? (not (s/valid? :page.sign-up/update-candidate form-values))}]])) -(defn c-employer-sign-up [] +(defn c-employer-sign-up + [] (let [user-id (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) employer-query [:employer {:user/id user-id} sign-up.events/employer-fields] user-query [:user {:user/id user-id} sign-up.events/user-fields] results (re/subscribe [::gql/query {:queries [employer-query user-query]}]) sign-up-form (re/subscribe [:page.sign-up/form]) - form-values (merge (get-in @results [:employer]) - (get-in @results [:user]) + form-values (merge (get @results :employer) + (get @results :user) @sign-up-form) form-validation (validate-keys form-values)] [:div.employer-sign-up @@ -242,56 +224,61 @@ {:on-submit [:page.sign-up/update-employer form-values] :disabled? (not (s/valid? :page.sign-up/update-employer form-values))}]])) -(defn c-arbiter-sign-up [] + +(defn c-arbiter-sign-up + [] (let [user-id (:user/id @(re/subscribe [:ethlance.ui.subscriptions/active-session])) arbiter-query [:arbiter {:user/id user-id} sign-up.events/arbiter-fields] user-query [:user {:user/id user-id} sign-up.events/user-fields] results (re/subscribe [::gql/query {:queries [arbiter-query user-query]}]) sign-up-form (re/subscribe [:page.sign-up/form]) - form-values (merge (get-in @results [:arbiter]) - (get-in @results [:user]) - @sign-up-form) + form-values (merge (get @results :arbiter) + (get @results :user) + @sign-up-form) form-validation (validate-keys form-values)] - [:div.arbiter-sign-up - [:div.form-container - [:div.label "Sign Up"] - [:div.first-forms - [:div.form-image - [c-upload-image]] - [c-user-name-input - {:form-values form-values - :form-validation form-validation}] - [c-user-email-input - {:form-values form-values - :form-validation form-validation}] - [:div.form-professional-title - [c-text-input - {:placeholder "Professional Title" - :value (:arbiter/professional-title form-values) - :on-change #(>evt [:page.sign-up/set-arbiter-professional-title %])}]] - [c-user-country-input - {:form-values form-values}] - [:div.form-hourly-rate - [c-currency-input - {:placeholder "Fixed Rate Per A Dispute" :color :primary - :value (:arbiter/fee form-values) - :on-change #(>evt [:page.sign-up/set-arbiter-fee (js/parseInt %)])}]]] - [:div.second-forms - [c-user-languages-input - {:form-values form-values}] - [c-bio - {:value (:arbiter/bio form-values) - :on-change #(>evt [:page.sign-up/set-arbiter-bio %]) - :error? (not (:arbiter/bio form-validation))}]]] - [c-submit-button - {:on-submit [:page.sign-up/update-arbiter form-values] - :disabled? (not (s/valid? :page.sign-up/update-arbiter form-values))}]])) + [:div.arbiter-sign-up + [:div.form-container + [:div.label "Sign Up"] + [:div.first-forms + [:div.form-image + [c-upload-image]] + [c-user-name-input + {:form-values form-values + :form-validation form-validation}] + [c-user-email-input + {:form-values form-values + :form-validation form-validation}] + [:div.form-professional-title + [c-text-input + {:placeholder "Professional Title" + :value (:arbiter/professional-title form-values) + :on-change #(>evt [:page.sign-up/set-arbiter-professional-title %])}]] + [c-user-country-input + {:form-values form-values}] + [:div.form-hourly-rate + [c-currency-input + {:placeholder "Fixed Rate Per A Dispute" :color :primary + :value (:arbiter/fee form-values) + :on-change #(>evt [:page.sign-up/set-arbiter-fee (js/parseInt %)])}]]] + [:div.second-forms + [c-user-languages-input + {:form-values form-values}] + [c-bio + {:value (:arbiter/bio form-values) + :on-change #(>evt [:page.sign-up/set-arbiter-bio %]) + :error? (not (:arbiter/bio form-validation))}]]] + [c-submit-button + {:on-submit [:page.sign-up/update-arbiter form-values] + :disabled? (not (s/valid? :page.sign-up/update-arbiter form-values))}]])) -(defn c-api-error-notification [message open?] + +(defn c-api-error-notification + [message open?] [:div {:class ["notification-box" (when (not open?) "hidden")]} - [:div {:class ["ui negative message"]} - [:div {:class "header"} "Error"] - [:p message]]]) + [:div {:class ["ui negative message"]} + [:div {:class "header"} "Error"] + [:p message]]]) + (defmethod page :route.me/sign-up [] (let [active-page (re/subscribe [::router.subs/active-page])] diff --git a/ui/src/ethlance/ui/page/sign_up/events.cljs b/ui/src/ethlance/ui/page/sign_up/events.cljs index 37b603b0..9ea77140 100644 --- a/ui/src/ethlance/ui/page/sign_up/events.cljs +++ b/ui/src/ethlance/ui/page/sign_up/events.cljs @@ -1,19 +1,20 @@ (ns ethlance.ui.page.sign-up.events (:require - [district.parsers :as parsers] + [district.ui.graphql.events :as gql-events] [district.ui.logging.events :as logging] - [district.ui.web3-accounts.events :as accounts-events] [district.ui.web3-accounts.queries :as accounts-queries] [ethlance.ui.event.utils :as event.utils] - [district.ui.graphql.events :as gql-events] - [ethlance.ui.util.component :refer [>evt]] [re-frame.core :as re])) + (def state-key :page.sign-up) + + (def state-default {:candidate/rate-currency-id :USD :arbiter/fee-currency-id :USD}) + (def create-assoc-handler (partial event.utils/create-assoc-handler state-key)) (re/reg-event-fx :page.sign-up/set-user-name (create-assoc-handler :user/name)) @@ -35,15 +36,11 @@ (def interceptors [re/trim-v]) -(re/reg-event-fx + +(re/reg-event-db :page.sign-up/initialize-page - (fn [{:keys [db]}] - {:db (assoc-in db [state-key] state-default) - ; :forward-events - ; {:register ::accounts-loaded? - ; :events #{::accounts-events/accounts-changed} - ; :dispatch-to [:page.sign-up/initial-query]} - })) + (fn [db _] + (assoc db state-key state-default))) (re/reg-event-fx @@ -51,9 +48,12 @@ (fn [] {:forward-events {:unregister ::initial-query?}})) -(defn- fallback-data [db section address] + +(defn- fallback-data + [db section address] (merge (get-in db [:users address]) (get-in db [section address]))) + (def user-fields [:user/id :user/email @@ -62,6 +62,7 @@ :user/languages :user/profile-image]) + (def candidate-fields [:candidate/professional-title :candidate/rate @@ -70,17 +71,21 @@ :candidate/skills :candidate/rate-currency-id]) + (def employer-fields [:employer/professional-title :employer/bio]) + (def arbiter-fields [:arbiter/professional-title :arbiter/bio :arbiter/fee :arbiter/fee-currency-id]) -(defn remove-nil-vals-from-map [input-map] + +(defn remove-nil-vals-from-map + [input-map] (reduce (fn [acc [k v]] (if (nil? v) acc @@ -88,6 +93,7 @@ {} input-map)) + (re/reg-event-fx :page.sign-up/update-candidate [interceptors] @@ -107,6 +113,7 @@ {:queries [query] :on-success [:navigate-to-profile user-address "candidate"]}]}))) + (re/reg-event-fx :page.sign-up/update-employer [interceptors] @@ -124,6 +131,7 @@ {:queries [query] :on-success [:navigate-to-profile user-address "employer"]}]}))) + (re/reg-event-fx :page.sign-up/update-arbiter [interceptors] @@ -141,12 +149,14 @@ {:queries [query] :on-success [:navigate-to-profile user-address "arbiter"]}]}))) + (re/reg-event-fx :navigate-to-profile - (fn [cofx [_ address tab]] + (fn [_cofx [_ address tab]] {:fx [[:dispatch [:district.ui.router.events/navigate :route.user/profile {:address address} {:tab tab}]] [:dispatch [:district.ui.user-profile-updated]]]})) + (re/reg-event-fx :page.sign-up/upload-user-image [interceptors] @@ -156,9 +166,9 @@ :on-success [::upload-user-image-success data] :on-error [::logging/error "Error uploading user image" {:data data}]}})) + (re/reg-event-fx ::upload-user-image-success [interceptors] (fn [_ [_ ipfs-resp]] {:dispatch [:page.sign-up/set-user-profile-image (:Hash ipfs-resp)]})) - diff --git a/ui/src/ethlance/ui/page/sign_up/subscriptions.cljs b/ui/src/ethlance/ui/page/sign_up/subscriptions.cljs index b5b910f0..2c610cd5 100644 --- a/ui/src/ethlance/ui/page/sign_up/subscriptions.cljs +++ b/ui/src/ethlance/ui/page/sign_up/subscriptions.cljs @@ -1,16 +1,9 @@ (ns ethlance.ui.page.sign-up.subscriptions (:require [ethlance.ui.page.sign-up.events :as sign-up.events] - [ethlance.ui.subscriptions :as ethlance-subs] - [ethlance.ui.util.urls :as util.urls] [re-frame.core :as re])) -(re/reg-sub - :page.sign-up/user-profile-image - (fn [db] - (get-in db [sign-up.events/state-key :user/profile-image]))) - (re/reg-sub :page.sign-up/form (fn [db] diff --git a/ui/src/ethlance/ui/pages.cljs b/ui/src/ethlance/ui/pages.cljs index 2269bffb..e6dfeea8 100644 --- a/ui/src/ethlance/ui/pages.cljs +++ b/ui/src/ethlance/ui/pages.cljs @@ -1,34 +1,26 @@ (ns ethlance.ui.pages (:require - - ;; Splash Page - [ethlance.ui.page.home] - - ;; Main Listing Pages - [ethlance.ui.page.jobs] - [ethlance.ui.page.arbiters] - [ethlance.ui.page.candidates] - [ethlance.ui.page.employers] - - ;; User Pages - [ethlance.ui.page.profile] - - ;; Job Pages - [ethlance.ui.page.job-contract] - [ethlance.ui.page.job-detail] - [ethlance.ui.page.new-job] - [ethlance.ui.page.new-invoice] - [ethlance.ui.page.invoices] - - ;; Me Pages - [ethlance.ui.page.sign-up] - [ethlance.ui.page.me] - - ;; Misc Pages - [ethlance.ui.page.how-it-works] - [ethlance.ui.page.about] - - ;; Development Pages - [ethlance.ui.page.devcard] - [ethlance.ui.page.dev.contract-ops])) - + [ethlance.ui.page.about] + [ethlance.ui.page.arbiters] + [ethlance.ui.page.candidates] + [ethlance.ui.page.dev.contract-ops] + ;; Development Pages + [ethlance.ui.page.devcard] + [ethlance.ui.page.employers] + ;; Splash Page + [ethlance.ui.page.home] + ;; Misc Pages + [ethlance.ui.page.how-it-works] + [ethlance.ui.page.invoices] + ;; Job Pages + [ethlance.ui.page.job-contract] + [ethlance.ui.page.job-detail] + ;; Main Listing Pages + [ethlance.ui.page.jobs] + [ethlance.ui.page.me] + [ethlance.ui.page.new-invoice] + [ethlance.ui.page.new-job] + ;; User Pages + [ethlance.ui.page.profile] + ;; Me Pages + [ethlance.ui.page.sign-up])) diff --git a/ui/src/ethlance/ui/subscription/utils.cljs b/ui/src/ethlance/ui/subscription/utils.cljs index 7c93096d..793cb6f7 100644 --- a/ui/src/ethlance/ui/subscription/utils.cljs +++ b/ui/src/ethlance/ui/subscription/utils.cljs @@ -1,5 +1,6 @@ (ns ethlance.ui.subscription.utils) + (defn create-get-handler [state-key key] (fn [db _] diff --git a/ui/src/ethlance/ui/subscriptions.cljs b/ui/src/ethlance/ui/subscriptions.cljs index 90e18eeb..2ff6dfae 100644 --- a/ui/src/ethlance/ui/subscriptions.cljs +++ b/ui/src/ethlance/ui/subscriptions.cljs @@ -1,6 +1,7 @@ (ns ethlance.ui.subscriptions (:require [district.cljs-utils :as cljs-utils] + [ethlance.shared.utils :refer [ilike=]] [district.ui.web3-accounts.subs :as accounts-subs] [ethlance.ui.component.modal.subscriptions] [ethlance.ui.page.arbiters.subscriptions] @@ -15,20 +16,23 @@ [ethlance.ui.page.new-job.subscriptions] [re-frame.core :as re])) + (re/reg-sub ::config (fn [db _] (get db :ethlance/config))) + (re/reg-sub ::active-session (fn [db _] (get db :active-session))) + (re/reg-sub ::active-account-has-session? :<- [::active-session] :<- [::accounts-subs/active-account] (fn [[active-session active-account]] (and (cljs-utils/not-nil? (:user/id active-session)) - (ethlance.shared.utils/ilike= (:user/id active-session) active-account)))) + (ilike= (:user/id active-session) active-account)))) diff --git a/ui/src/ethlance/ui/util/component.cljs b/ui/src/ethlance/ui/util/component.cljs index fbbefdb5..5948ce69 100644 --- a/ui/src/ethlance/ui/util/component.cljs +++ b/ui/src/ethlance/ui/util/component.cljs @@ -1,9 +1,12 @@ (ns ethlance.ui.util.component - (:require [re-frame.core :as re])) + (:require + [re-frame.core :as re])) + (def evt re/dispatch) + (defn unwrap-seq "Unwraps a sequence argument if it contains one element. diff --git a/ui/src/ethlance/ui/util/dates.cljs b/ui/src/ethlance/ui/util/dates.cljs index 263425b6..697a7e31 100644 --- a/ui/src/ethlance/ui/util/dates.cljs +++ b/ui/src/ethlance/ui/util/dates.cljs @@ -1,13 +1,15 @@ (ns ethlance.ui.util.dates (:require - - [district.format :as format] + [cljs-time.coerce :as t-coerce] [cljs-time.core :as t-core] - [cljs-time.coerce :as t-coerce])) + [district.format :as format])) + -(defn relative-ago [get-date-field data] +(defn relative-ago + [get-date-field data] (format/time-ago (t-core/minus (t-core/now) (t-coerce/from-long (get-date-field data))))) + (defn formatted-date ([data] (formatted-date identity data)) ([get-date-field data] diff --git a/ui/src/ethlance/ui/util/graphql.cljs b/ui/src/ethlance/ui/util/graphql.cljs index d0a4660a..f7e15118 100644 --- a/ui/src/ethlance/ui/util/graphql.cljs +++ b/ui/src/ethlance/ui/util/graphql.cljs @@ -1,9 +1,11 @@ (ns ethlance.ui.util.graphql) -(defn prepare-search-params [page-state search-fields] + +(defn prepare-search-params + [page-state search-fields] (reduce (fn [acc [filter-key & transformers]] (let [filter-val (reduce #(%2 %1) - (get-in page-state [filter-key]) + (get page-state filter-key) (or transformers []))] (if (or (nil? filter-val) ; Don't add nil or empty collections to the search (and (sequential? filter-val) diff --git a/ui/src/ethlance/ui/util/injection.cljs b/ui/src/ethlance/ui/util/injection.cljs index 62bbce63..5665b211 100644 --- a/ui/src/ethlance/ui/util/injection.cljs +++ b/ui/src/ethlance/ui/util/injection.cljs @@ -1,7 +1,9 @@ (ns ethlance.ui.util.injection - (:require [goog.functions :refer [throttle]])) + (:require + [goog.functions :refer [throttle]])) -(def default-debounce-interval 200) ;; ms + +(def default-debounce-interval 200) ; ms (defn- handle-inject-data-scroll! "Injects window.scrollY as as datasource, which allows you to use css @@ -22,6 +24,7 @@ scroll-y (aget elnode "scrollTop")] (aset elnode "dataset" "scroll" scroll-y)))) + (defn inject-data-scroll! [{:keys [injection-selector debounce-interval] :or {injection-selector "#app" diff --git a/ui/src/ethlance/ui/util/job.cljs b/ui/src/ethlance/ui/util/job.cljs index 435a92eb..75e96330 100644 --- a/ui/src/ethlance/ui/util/job.cljs +++ b/ui/src/ethlance/ui/util/job.cljs @@ -1,21 +1,25 @@ (ns ethlance.ui.util.job) + (def experience-level [[:beginner "Beginner ($)"] [:intermediate "Intermediate ($$)"] [:expert "Expert ($$$)"]]) + (def bid-option [[:hourly-rate "Hourly rate"] [:fixed-price "Fixed price"] [:annual-salary "Annual salary"]]) + (def estimated-durations [[:day "Hours or days"] [:week "Weeks"] [:month "Months"] [:year ">6 months"]]) + (def required-availability [[:full-time "Full time"] [:part-time "Part time"]]) diff --git a/ui/src/ethlance/ui/util/navigation.cljs b/ui/src/ethlance/ui/util/navigation.cljs index 6ca8d743..9cebe580 100644 --- a/ui/src/ethlance/ui/util/navigation.cljs +++ b/ui/src/ethlance/ui/util/navigation.cljs @@ -1,7 +1,8 @@ (ns ethlance.ui.util.navigation (:require - [district.ui.router.events :as router.events] - [re-frame.core :as re])) + [district.ui.router.events :as router.events] + [re-frame.core :as re])) + (defn create-handler "Generate a re-frame dispatch function for buttons to navigate to other pages. @@ -28,6 +29,7 @@ (.preventDefault event) (re/dispatch [::router.events/navigate route params query]))) + (defn resolve-route "Resolve a given route with the given params and query @@ -37,11 +39,13 @@ [{:keys [route params query]}] @(re/subscribe [:district.ui.router.subs/resolve route params query])) + (defn url-encode [string] (some-> string str (js/encodeURIComponent) (.replace "+" "%20"))) -(defn link-params [{:keys [route params query]}] +(defn link-params + [{:keys [route params query]}] {:on-click (create-handler {:route route :params params :query query}) :href (resolve-route {:route route :params params :query query})}) diff --git a/ui/src/ethlance/ui/util/tokens.cljs b/ui/src/ethlance/ui/util/tokens.cljs index 300fd5cd..04c0dcb5 100644 --- a/ui/src/ethlance/ui/util/tokens.cljs +++ b/ui/src/ethlance/ui/util/tokens.cljs @@ -1,52 +1,68 @@ (ns ethlance.ui.util.tokens (:require - [re-frame.core :as re] - [clojure.math] + [cljs-web3-next.eth :as w3-eth] [cljs-web3-next.helpers :as web3-helpers] - [ethlance.shared.utils :refer [wei->eth eth->wei]] - [cljs-web3-next.eth :as w3-eth])) + [clojure.math] + [clojure.walk] + [ethlance.shared.utils :refer [wei->eth eth->wei]])) + -(defn round [decimals amount] +(defn round + [decimals amount] (let [multiplier (clojure.math/pow 10 decimals)] (/ (.round js/Math (* multiplier amount)) multiplier))) -(defn human-amount [amount token-type & [decimals]] + +(defn human-amount + [amount token-type & [decimals]] (if (not (nil? decimals)) (round decimals (/ amount (clojure.math/pow 10 decimals))) (case (keyword token-type) :eth (wei->eth amount) amount))) -(defn machine-amount [amount token-type] + +(defn machine-amount + [amount token-type] (case (keyword token-type) :eth (eth->wei amount) amount)) -(defn fiat-amount-with-symbol [currency-id amount] + +(defn fiat-amount-with-symbol + [currency-id amount] (case currency-id :usd (str "$ " amount) :eur (str amount " €") amount)) -(defn address->token-info-url [address] + +(defn address->token-info-url + [address] (str "https://ethplorer.io/address/" address)) -(defn- remove-unnecessary-keys [k-v] + +(defn- remove-unnecessary-keys + [k-v] (let [length-key "__length__" positional-arguments-count (int (get k-v length-key "0")) numeric-keys (map str (range positional-arguments-count))] (apply dissoc (into [k-v length-key] numeric-keys)))) -(defn obj->clj [obj] + +(defn obj->clj + [obj] (js->clj (-> obj js/JSON.stringify js/JSON.parse))) -(defn parse-event [web3 contract-instance raw-event event-name] + +(defn parse-event + [web3 contract-instance raw-event event-name] (let [event-interface (web3-helpers/event-interface contract-instance event-name) event-data (:data raw-event) event-topics (:topics raw-event) decoded-event (w3-eth/decode-log web3 (:inputs event-interface) event-data (drop 1 event-topics))] - (-> decoded-event - obj->clj - remove-unnecessary-keys - web3-helpers/js->cljkk - clojure.walk/keywordize-keys))) + (-> decoded-event + obj->clj + remove-unnecessary-keys + web3-helpers/js->cljkk + clojure.walk/keywordize-keys))) diff --git a/ui/src/ethlance/ui/util/urls.cljs b/ui/src/ethlance/ui/util/urls.cljs index 02fbd408..3e380480 100644 --- a/ui/src/ethlance/ui/util/urls.cljs +++ b/ui/src/ethlance/ui/util/urls.cljs @@ -1,21 +1,22 @@ (ns ethlance.ui.util.urls (:require - [re-frame.core :as re] - [ethlance.ui.config :as ui-config])) + [clojure.string] + [re-frame.core :as re])) + (defn ipfs-hash->gateway-url - [ipfs-hash] - ;TODO: This URL should come from configuration - (let [config @(re/subscribe [:ethlance.ui.subscriptions/config]) - ipfs-gateway (get-in config [:ipfs :gateway])] - (cond - (nil? ipfs-hash) - ipfs-hash + [ipfs-hash] + ;; TODO: This URL should come from configuration + (let [config @(re/subscribe [:ethlance.ui.subscriptions/config]) + ipfs-gateway (get-in config [:ipfs :gateway])] + (cond + (nil? ipfs-hash) + ipfs-hash - (or - (clojure.string/starts-with? ipfs-hash "http") - (clojure.string/starts-with? ipfs-hash "/")) - ipfs-hash + (or + (clojure.string/starts-with? ipfs-hash "http") + (clojure.string/starts-with? ipfs-hash "/")) + ipfs-hash - :else - (str ipfs-gateway "/" ipfs-hash)))) + :else + (str ipfs-gateway "/" ipfs-hash)))) diff --git a/ui/src/ethlance/ui/util/users.cljs b/ui/src/ethlance/ui/util/users.cljs deleted file mode 100644 index 4e7541e6..00000000 --- a/ui/src/ethlance/ui/util/users.cljs +++ /dev/null @@ -1,14 +0,0 @@ -(ns ethlance.ui.util.users) - -(defn job->participants [job-story] - {:candidate (or - (get-in job-story [:job-story/candidate]) - (get-in job-story [:candidate :user/id])) - :employer (get-in job-story [:job :job/employer :user/id]) - :arbiter (get-in job-story [:job :job/arbiter :user/id])}) - -(defn user-type [active-user involved-users] - (reduce (fn [acc [user-type address]] - (if (and (nil? acc) (ilike= address active-user)) user-type acc)) - nil ; initial value - involved-users)) diff --git a/ui/src/soda_ash/core.cljs b/ui/src/soda_ash/core.cljs index 98a49e6a..1d186026 100644 --- a/ui/src/soda_ash/core.cljs +++ b/ui/src/soda_ash/core.cljs @@ -1,12 +1,12 @@ (ns soda-ash.core - (:require-macros - [soda-ash.macros :refer [export-semantic-ui-react-components]]) (:require - [cljsjs.semantic-ui-react] - [reagent.core])) + [cljsjs.semantic-ui-react] + [reagent.core]) + (:require-macros + [soda-ash.macros :refer [export-semantic-ui-react-components]])) -; Turned off soda-ash because -; 1) it didn't seem to be used in any crucial places -; 2) It causes errors -; (export-semantic-ui-react-components) +;; Turned off soda-ash because +;; 1) it didn't seem to be used in any crucial places +;; 2) It causes errors +;; (export-semantic-ui-react-components) diff --git a/ui/src/soda_ash/macros.clj b/ui/src/soda_ash/macros.clj index a56d6615..e59655ba 100644 --- a/ui/src/soda_ash/macros.clj +++ b/ui/src/soda_ash/macros.clj @@ -2,9 +2,8 @@ (def semantic-ui-react-tags - ; List from https://github.com/Semantic-Org/Semantic-UI-React/blob/master/src/index.js - '[ - Confirm + ;; List from https://github.com/Semantic-Org/Semantic-UI-React/blob/master/src/index.js + '[Confirm Pagination PaginationItem Portal @@ -14,7 +13,7 @@ TextArea TransitionablePortal - ; Collections + ;; Collections Breadcrumb BreadcrumbDivider BreadcrumbSection @@ -53,7 +52,7 @@ TableHeaderCell TableRow - ; Elements + ;; Elements Button ButtonContent ButtonGroup @@ -112,7 +111,7 @@ StepGroup StepTitle - ; Modules + ;; Modules Accordion AccordionAccordion AccordionContent @@ -169,7 +168,7 @@ Transition TransitionGroup - ; Views + ;; Views Advertisement Card @@ -212,24 +211,28 @@ Statistic StatisticGroup StatisticLabel - StatisticValue + StatisticValue]) - ]) - -(def reserved-tags #{"Comment" - "List"}) +(def reserved-tags + #{"Comment" + "List"}) (println ">>> ETHLANCE semantic-ash evaluated") -(defn create-semantic-ui-react-component [tag] + + +(defn create-semantic-ui-react-component + [tag] (let [tag-name (if (reserved-tags (name tag)) (-> tag name (str "SA") symbol) tag)] - `(def ~tag-name (reagent.core/adapt-react-class - (aget js/semanticUIReact ~(name tag)))))) + `(def ~tag-name + (reagent.core/adapt-react-class + (aget js/semanticUIReact ~(name tag)))))) -(defmacro export-semantic-ui-react-components [] +(defmacro export-semantic-ui-react-components + [] `(do ~@(map create-semantic-ui-react-component semantic-ui-react-tags)))