Merge branch 'develop' of https://github.com/status-im/open-bounty into 193-hide-myself
|
@ -0,0 +1,3 @@
|
||||||
|
docker-compose.yml
|
||||||
|
Dockerfile
|
||||||
|
Jenkinsfile
|
|
@ -23,4 +23,7 @@ profiles.clj
|
||||||
.idea
|
.idea
|
||||||
resources/contracts
|
resources/contracts
|
||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
/config-prod.edn
|
||||||
|
/config-dev.edn
|
||||||
|
/config-test.edn
|
||||||
|
/src/java
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
FROM clojure as builder
|
||||||
|
|
||||||
|
WORKDIR /tmp
|
||||||
|
|
||||||
|
RUN wget -O /usr/local/bin/solc https://github.com/ethereum/solidity/releases/download/v0.4.15/solc-static-linux
|
||||||
|
RUN chmod +x /usr/local/bin/solc
|
||||||
|
|
||||||
|
RUN wget https://github.com/web3j/web3j/releases/download/v2.3.0/web3j-2.3.0.tar
|
||||||
|
RUN tar -xf web3j-2.3.0.tar
|
||||||
|
RUN cp -r web3j-2.3.0/* /usr/local/
|
||||||
|
|
||||||
|
|
||||||
|
COPY . /usr/src/app
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
ENV LEIN_SNAPSHOTS_IN_RELEASE=1
|
||||||
|
|
||||||
|
|
||||||
|
RUN lein less once
|
||||||
|
RUN lein uberjar
|
||||||
|
|
||||||
|
|
||||||
|
FROM clojure
|
||||||
|
WORKDIR /root/
|
||||||
|
|
||||||
|
COPY --from=builder /usr/src/app/target/uberjar/commiteth.jar .
|
||||||
|
|
||||||
|
CMD [""]
|
||||||
|
ENTRYPOINT ["/usr/bin/java", "-Duser.timezone=UTC", "-Dconf=config-test.edn", "-jar", "/root/commiteth.jar"]
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
#!/usr/bin/env groovy
|
||||||
|
|
||||||
|
node('linux') {
|
||||||
|
checkout scm
|
||||||
|
|
||||||
|
def dockerreponame = "statusim/openbounty-app"
|
||||||
|
|
||||||
|
try {
|
||||||
|
stage('Build & push') {
|
||||||
|
|
||||||
|
GIT_COMMIT_HASH = sh (script: "git rev-parse --short HEAD | tr -d '\n'", returnStdout: true)
|
||||||
|
|
||||||
|
docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-statusvan') {
|
||||||
|
def openbountyApp = docker.build("${dockerreponame}:${env.BUILD_NUMBER}")
|
||||||
|
openbountyApp.push("${env.BRANCH_NAME}")
|
||||||
|
if (env.BRANCH_NAME == 'develop') {
|
||||||
|
openbountyApp.push("develop")
|
||||||
|
} else if (env.BRANCH_NAME == 'master') {
|
||||||
|
openbountyApp.push("master")
|
||||||
|
} else {
|
||||||
|
println "Not named branch have no custom tag"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Deploy') {
|
||||||
|
build job: 'status-openbounty/openbounty-cluster', parameters: [[$class: 'StringParameterValue', name: 'DEPLOY_ENVIRONMENT', value: "dev"], [$class: 'StringParameterValue', name: 'BRANCH', value: env.BRANCH_NAME]]
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
// slackSend color: 'bad', message: REPO + ":" + BRANCH_NAME + ' failed to build. ' + env.BUILD_URL
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
68
README.md
|
@ -1,15 +1,14 @@
|
||||||
# Commiteth
|
# Status Open Bounty
|
||||||
|
|
||||||
Allows you to set bounties for Github issues, paid out in Ether.
|
Allows you to set bounties for Github issues, paid out in Ether or any ERC-20 token.
|
||||||
|
|
||||||
More information:
|
More information:
|
||||||
http://wiki.status.im/proposals/commiteth/
|
https://wiki.status.im/Status_Open_Bounty
|
||||||
|
|
||||||
Live beta version:
|
Live production version:
|
||||||
https://openbounty.status.im
|
https://openbounty.status.im
|
||||||
The `master` branch is automatically deployed here.
|
The `master` branch is automatically deployed here.
|
||||||
|
|
||||||
|
|
||||||
Live testnet (Ropsten) version:
|
Live testnet (Ropsten) version:
|
||||||
https://openbounty.status.im:444
|
https://openbounty.status.im:444
|
||||||
The `develop` branch is automatically deployed here.
|
The `develop` branch is automatically deployed here.
|
||||||
|
@ -17,10 +16,28 @@ The `develop` branch is automatically deployed here.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
You will need [Leiningen](https://github.com/technomancy/leiningen) 2.0 or above installed.
|
You will need [Leiningen](https://github.com/technomancy/leiningen) 2.0 or above installed. Also, make sure that you have [wkhtmltoimage](https://wkhtmltopdf.org/downloads.html) available in your PATH. On macOS, it can be installed via `brew cask install wkhtmltopdf`.
|
||||||
|
|
||||||
### PostgreSQL
|
### PostgreSQL
|
||||||
|
|
||||||
|
Make sure you install [PostgreSQL](https://www.postgresql.org/) and properly set it up:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo -u postgres psql -c "CREATE USER commiteth WITH PASSWORD 'commiteth';"
|
||||||
|
sudo -u postgres createdb commiteth
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
Launch following commands each in its own shell:
|
||||||
|
|
||||||
|
```
|
||||||
|
lein run
|
||||||
|
lein figwheel
|
||||||
|
lein less auto
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
Make sure you install [PostgreSQL](https://www.postgresql.org/) and properly set it up:
|
Make sure you install [PostgreSQL](https://www.postgresql.org/) and properly set it up:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -36,9 +53,40 @@ Solidity compiler [0.4.15](https://github.com/ethereum/solidity/releases/tag/v0.
|
||||||
|
|
||||||
Web3j [2.3.0](https://github.com/web3j/web3j/releases/tag/v2.3.0) is required and the command line tools need to be in $PATH.
|
Web3j [2.3.0](https://github.com/web3j/web3j/releases/tag/v2.3.0) is required and the command line tools need to be in $PATH.
|
||||||
|
|
||||||
## Running
|
## Application config
|
||||||
|
|
||||||
Make sure `env/dev/resources/config.edn` is correctly populated.
|
Make sure to create `/config-dev.edn` and populate it correctly, which is based on `env/dev/resources/config.edn`. Description of config fields is given below:
|
||||||
|
|
||||||
|
Key | Description
|
||||||
|
--- | ---
|
||||||
|
dev | Currently specifies whether Swagger UI endpoints should be added to routes
|
||||||
|
port | HTTP port for the Ring web app
|
||||||
|
nrepl-port | nREPL port for development
|
||||||
|
jdbc-database-url | PostgreSQL database URL. For instance, URL to local db would be `jdbc:postgresql://localhost/commiteth?user=commiteth&password=commiteth`
|
||||||
|
server-address | URL and port of local server that can be resolved from public internet. It will be used as a redirect URI during GitHub OAuth authorization process
|
||||||
|
eth-account | Ethereum account ID for the bot
|
||||||
|
eth-password | Ethereum account password for the bot
|
||||||
|
eth-rpc-url | RPC URL to Ethereum node, e.g. Geth. Either local or remote
|
||||||
|
eth-wallet-file | Location of wallet file. If Geth is run with the parameters as given below, it will reside under `$HOME/.ropsten/keystore`
|
||||||
|
tokenreg-base-format | Should be set to `:status`
|
||||||
|
github-client-id | Related to OAuth. Copied from GitHub account Settings->Developer settings->OAuth Apps
|
||||||
|
github-client-secret | Related to OAuth. Copied from GitHub account Settings->Developer settings->OAuth Apps
|
||||||
|
github-user | GitHub username for bot account. It is used for posting bounty comments
|
||||||
|
github-password | GitHub password for bot account
|
||||||
|
webhook-secret | Secret string to be used when creating a GitHub App
|
||||||
|
user-whitelist | Set of GitHub user/org IDs to be whitelisted. E.g. `#{"status-im" "your_org"}`
|
||||||
|
testnet-token-data | Token data map, useful if there are Geth connectivity problems
|
||||||
|
|
||||||
|
## GitHub integration
|
||||||
|
Open Bounty uses both OAuth App and GitHub App integration.
|
||||||
|
|
||||||
|
### OAuth App
|
||||||
|
Follow the steps [here](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/). Specify the value of `:server-address` as "Homepage URL", and `:server-address` + `/callback` as "Authorization callback URL". Be sure to copy Client ID and Client Secret values in the config file.
|
||||||
|
|
||||||
|
### GitHub App
|
||||||
|
Follow the steps [here](https://developer.github.com/apps/building-github-apps/creating-a-github-app/). Be sure to specify `:server-address` + `/webhook-app` as "Webhook URL", and `:webhook-secret` as "Webhook Secret".
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
Lauch a local geth node with the bot account unlocked:
|
Lauch a local geth node with the bot account unlocked:
|
||||||
|
|
||||||
|
@ -121,6 +169,10 @@ Landing page is static and different CSS and JS due to time constraints.
|
||||||
|
|
||||||
This copies over necessary artifacts to `resources` dir.
|
This copies over necessary artifacts to `resources` dir.
|
||||||
|
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
See the [Cookbook](doc/cookbook.md).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Licensed under the [Affero General Public License v3.0](https://github.com/status-im/commiteth/blob/master/LICENSE.md)
|
Licensed under the [Affero General Public License v3.0](https://github.com/status-im/commiteth/blob/master/LICENSE.md)
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Cookbook
|
||||||
|
|
||||||
|
Here some common tasks/issues are listed along with their solutions.
|
||||||
|
|
||||||
|
## Change config and restart the service
|
||||||
|
- ssh to the host running commiteth
|
||||||
|
- go to `/opt/commiteth` for prod or `/opt/commiteth-test` for test environment.
|
||||||
|
- there edit `config.edn`
|
||||||
|
- restart the service with `sudo service commiteth-test restart`
|
||||||
|
|
||||||
|
## Manually add a GitHub repo to SOB app
|
||||||
|
Sometimes SOB will not
|
||||||
|
- connect to Postgres instance. Get DB name and username/password from respective `config.edn` (see previous answer). These are set in `:jdbc-database-url` field.
|
||||||
|
- execute the query, e.g.
|
||||||
|
```
|
||||||
|
insert into repositories(repo_id,user_id,owner,repo,hook_id,state,hook_secret,owner_avatar_url) values(116971984,447328, 'aragon', 'aragon-monthly', 0, 2, '', 'https://avatars1.githubusercontent.com/u/24612534?v=4');
|
||||||
|
```
|
||||||
|
Note that the value for repo_id field is GitHub's internal repo ID. One way of obtaining it is navigating to `api.github.com/repos/:owner/:repo_name`, e.g. `https://api.github.com/repos/aragon/aragon-monthly`.
|
|
@ -10,7 +10,7 @@ For testing you will need:
|
||||||
* a Github account with administrative access to one or more repositories
|
* a Github account with administrative access to one or more repositories
|
||||||
* for approving bounty payouts you will additionally need access to an Ethereum wallet. So far, Mist and [MetaMask](https://metamask.io/) have been used, but anything that provides the web3 javascript interface should work.
|
* for approving bounty payouts you will additionally need access to an Ethereum wallet. So far, Mist and [MetaMask](https://metamask.io/) have been used, but anything that provides the web3 javascript interface should work.
|
||||||
|
|
||||||
The developers can be reached on the `#commiteth` channel in the [Status slack](http://slack.status.im/).
|
The developers can be reached on the `#openbounty` channel in the [Status slack](http://slack.status.im/).
|
||||||
|
|
||||||
### Signing up
|
### Signing up
|
||||||
|
|
||||||
|
@ -29,13 +29,9 @@ You should now see `Bounties`, `Activity`, `Repositories` and `Manage Payouts` t
|
||||||
|
|
||||||
### Creating bounty issues
|
### Creating bounty issues
|
||||||
|
|
||||||
Before you can create bounties, you need to have administrative access to one or more GitHub repositories. These can be either in the scope of your personal user account or in the scope of a Github orgnazation.
|
Before you can create bounties, you need to add Open Bounty GitHub App to your account or repos. Go to https://github.com/apps/status-open-bounty-app-test (or link to another GitHub App you've created for testing, as described in the [README](README.md) and click Install. Specify whether access to all org repos or specific repos is granted. This will install webhooks for SOB in your repos.
|
||||||
|
|
||||||
* click the `Repositories` tab
|
* Request for your account to be whitelisted. Contact [Riot](https://chat.status.im) for more information
|
||||||
* click on the button `Enable Github Account`
|
|
||||||
* If you have 1 or more Organisation repositories then grant Organisation access to each of them by clicking on the button `Grant`
|
|
||||||
* grant Status Open Bounty the needed addtional permissions for managing repository webhooks, adding and modifying comments by clicking on the button `Authorize status-open-bounty`
|
|
||||||
* now you should see your repositories on the `Repositories` tab, click `Add` on one. If your account isn't whitelisted you will see instructions how to request an access. If your account is whitelisted then new `bounty` label will become available in the GitHub repository's labels and a new webhook should now exist for the repository.
|
|
||||||
* now, add the `bounty` label to a new or an existing issue. This should cause Status Open Bounty to post a new comment for the issue containing an image with text `Deploying contract, please wait`
|
* now, add the `bounty` label to a new or an existing issue. This should cause Status Open Bounty to post a new comment for the issue containing an image with text `Deploying contract, please wait`
|
||||||
* once the contract has been mined, the comment will be updated to contain the bounty contract's address and a QR code
|
* once the contract has been mined, the comment will be updated to contain the bounty contract's address and a QR code
|
||||||
|
|
||||||
|
@ -60,6 +56,14 @@ To remove issue from the Bounties list you can close it in GitHub.
|
||||||
|
|
||||||
### Reporting bugs
|
### Reporting bugs
|
||||||
|
|
||||||
All bugs should be reported as issues in the [CommitETH Github repository](https://github.com/status-im/commiteth/issues).
|
All bugs should be reported as issues in the [OpenBounty Github repository](https://github.com/status-im/open-bounty/issues).
|
||||||
|
|
||||||
Please first check that there is not already a duplicate issue. Issues should contain exact and minimal step-by-step instructions for reproducing the problem.
|
Please first check that there is not already a duplicate issue. Issues should contain exact and minimal step-by-step instructions for reproducing the problem.
|
||||||
|
|
||||||
|
### Status Open Bounty end-to-end tests
|
||||||
|
|
||||||
|
Framework for testing located in: `open-bounty/test/end-to-end`
|
||||||
|
|
||||||
|
Full installation and configuring manual: [Status Open Bounty end-to-end tests](https://wiki.status.im/Status_Open_Bounty_end-to-end_tests)
|
||||||
|
|
||||||
|
Currently supports local and Jenkins environment running (you can find example of JenkinsFile in `open-bounty/test` )
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
(ns user
|
(ns user
|
||||||
(:require [mount.core :as mount]
|
(:require [mount.core :as mount]
|
||||||
[commiteth.figwheel :refer [start-fw stop-fw cljs]]
|
[commiteth.figwheel :refer [start-fw stop-fw cljs]]))
|
||||||
commiteth.core))
|
|
||||||
|
|
||||||
(defn start []
|
(defn start []
|
||||||
(mount/start-without #'commiteth.core/repl-server))
|
(require 'commiteth.core)
|
||||||
|
(mount/start-without (ns-resolve 'commiteth.core 'repl-server)))
|
||||||
|
|
||||||
(defn stop []
|
(defn stop []
|
||||||
(mount/stop-except #'commiteth.core/repl-server))
|
(require 'commiteth.core)
|
||||||
|
(mount/stop-except (ns-resolve 'commiteth.core 'repl-server)))
|
||||||
|
|
||||||
(defn restart []
|
(defn restart []
|
||||||
(stop)
|
(stop)
|
||||||
|
|
|
@ -11,16 +11,18 @@
|
||||||
:eth-password "XXX"
|
:eth-password "XXX"
|
||||||
|
|
||||||
;; RPC URL to ethereum node to be used
|
;; RPC URL to ethereum node to be used
|
||||||
:eth-rpc-url "http://localhost:8547"
|
:eth-rpc-url "http://localhost:8545"
|
||||||
:eth-wallet-file "/some/location"
|
:eth-wallet-file "/some/location"
|
||||||
|
|
||||||
;; address of token registry to be used
|
;; address of token registry to be used
|
||||||
:tokenreg-addr "0x..."
|
;; this is the default value for ropsten
|
||||||
|
:tokenreg-addr "0x7d127a3e3b5e72cd8f15e7dee650abe4fcced2b9"
|
||||||
;; format of tokenreg records' base field, possible values :status, :parity
|
;; format of tokenreg records' base field, possible values :status, :parity
|
||||||
:tokenreg-base-format :parity
|
:tokenreg-base-format :status
|
||||||
|
|
||||||
;; address of factory contract used for deploying bounty contracts
|
;; address of factory contract used for deploying bounty contracts
|
||||||
:contract-factory-addr "0x..."
|
;; this is the default value for ropsten
|
||||||
|
:contract-factory-addr "0x3B9A3c062Bdb640b5039C0cCda4157737d732F95"
|
||||||
|
|
||||||
;; commiteth-test-tpatja
|
;; commiteth-test-tpatja
|
||||||
:github-client-id "CLIENT ID"
|
:github-client-id "CLIENT ID"
|
||||||
|
@ -43,5 +45,7 @@
|
||||||
;; needeed when :hubspot-contact-create-enabled
|
;; needeed when :hubspot-contact-create-enabled
|
||||||
:hubspot-api-key "xxxxxxx-xxxx-x-xxxx-xxxx"
|
:hubspot-api-key "xxxxxxx-xxxx-x-xxxx-xxxx"
|
||||||
|
|
||||||
|
:user-whitelist #{}
|
||||||
|
|
||||||
;; used for blacklisting tokens from token registry data
|
;; used for blacklisting tokens from token registry data
|
||||||
:token-blacklist #{}}
|
:token-blacklist #{}}
|
||||||
|
|
|
@ -33,7 +33,11 @@
|
||||||
<AppenderRef ref="FILE"/>
|
<AppenderRef ref="FILE"/>
|
||||||
</logger>
|
</logger>
|
||||||
<root level="DEBUG">
|
<root level="DEBUG">
|
||||||
<appender-ref ref="STDOUT"/>
|
<!-- <appender-ref ref="STDOUT"/> -->
|
||||||
|
<appender-ref ref="FILE"/>
|
||||||
|
</root>
|
||||||
|
<root level="INFO">
|
||||||
|
<!-- <appender-ref ref="STDOUT"/> -->
|
||||||
<appender-ref ref="FILE"/>
|
<appender-ref ref="FILE"/>
|
||||||
</root>
|
</root>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|
15
project.clj
|
@ -93,7 +93,8 @@
|
||||||
|
|
||||||
|
|
||||||
:profiles
|
:profiles
|
||||||
{:uberjar {:omit-source true
|
{:uberjar {:jvm-opts ["-server" "-Dconf=config-prod.edn"]
|
||||||
|
:omit-source true
|
||||||
:prep-tasks ["build-contracts" "javac" "compile" ["cljsbuild" "once" "min"] ["less" "once"]]
|
:prep-tasks ["build-contracts" "javac" "compile" ["cljsbuild" "once" "min"] ["less" "once"]]
|
||||||
:cljsbuild
|
:cljsbuild
|
||||||
{:builds
|
{:builds
|
||||||
|
@ -115,11 +116,8 @@
|
||||||
:uberjar-name "commiteth.jar"
|
:uberjar-name "commiteth.jar"
|
||||||
:source-paths ["env/prod/clj"]
|
:source-paths ["env/prod/clj"]
|
||||||
:resource-paths ["env/prod/resources"]}
|
:resource-paths ["env/prod/resources"]}
|
||||||
;; :precomp profile allows to compile classes from commiteth.eth.contracts
|
:dev {:jvm-opts ["-server" "-Dconf=config-dev.edn"]
|
||||||
;; namespace before compiling clojure code. Otherwise ClassNotFound exception
|
:dependencies [[prone "1.1.4"]
|
||||||
;; will be thrown
|
|
||||||
:precomp {:target-path "target/base+system+user+dev/" }
|
|
||||||
:dev {:dependencies [[prone "1.1.4"]
|
|
||||||
[ring/ring-mock "0.3.1"]
|
[ring/ring-mock "0.3.1"]
|
||||||
[ring/ring-devel "1.6.2"]
|
[ring/ring-devel "1.6.2"]
|
||||||
[pjstadig/humane-test-output "0.8.3"]
|
[pjstadig/humane-test-output "0.8.3"]
|
||||||
|
@ -145,7 +143,7 @@
|
||||||
:optimizations :none
|
:optimizations :none
|
||||||
:pretty-print true}}]}
|
:pretty-print true}}]}
|
||||||
|
|
||||||
:prep-tasks ["build-contracts" ["with-profile" "precomp" "javac"]]
|
:prep-tasks ["build-contracts" "javac"]
|
||||||
:doo {:build "test"}
|
:doo {:build "test"}
|
||||||
:source-paths ["env/dev/clj" "test/clj"]
|
:source-paths ["env/dev/clj" "test/clj"]
|
||||||
:resource-paths ["env/dev/resources"]
|
:resource-paths ["env/dev/resources"]
|
||||||
|
@ -153,7 +151,8 @@
|
||||||
:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
|
:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
|
||||||
:injections [(require 'pjstadig.humane-test-output)
|
:injections [(require 'pjstadig.humane-test-output)
|
||||||
(pjstadig.humane-test-output/activate!)]}
|
(pjstadig.humane-test-output/activate!)]}
|
||||||
:test {:resource-paths ["env/dev/resources" "env/test/resources"]
|
:test {:jvm-opts ["-server" "-Dconf=config-test.edn"]
|
||||||
|
:resource-paths ["env/dev/resources" "env/test/resources"]
|
||||||
:dependencies [[devcards "0.2.4"]]
|
:dependencies [[devcards "0.2.4"]]
|
||||||
:cljsbuild
|
:cljsbuild
|
||||||
{:builds
|
{:builds
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<rect width="20" height="20" fill="#000" opacity=".5" rx="6"/>
|
||||||
|
<path fill="#57A7ED" d="M10 8.812L6.436 5.247a.839.839 0 0 0-1.19 0 .839.839 0 0 0 .001 1.189L8.812 10l-3.565 3.564a.839.839 0 0 0 0 1.19c.33.33.86.327 1.189-.001L10 11.188l3.564 3.565c.33.33.861.328 1.19 0a.839.839 0 0 0-.001-1.189L11.188 10l3.565-3.564a.839.839 0 0 0 0-1.19.839.839 0 0 0-1.189.001L10 8.812z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 533 B |
|
@ -1 +1 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>icon_sl</title><g fill="#000"><path d="M12.632 3.316H7.893a2.365 2.365 0 0 0-2.368 2.369v11.821a2.365 2.365 0 0 0 2.368 2.369 2.366 2.366 0 0 0 2.369-2.369v-2.369h2.37c3.254 0 5.91-2.656 5.91-5.911 0-3.254-2.656-5.91-5.91-5.91z"/><path d="M7.905 19.277a1.771 1.771 0 0 1-1.771-1.771V5.673a1.77 1.77 0 0 1 1.759-1.772h4.739a5.336 5.336 0 0 1 5.325 5.325 5.334 5.334 0 0 1-5.325 5.324H9.676v2.956c0 .967-.794 1.771-1.771 1.771z"/><path d="M7.905 18.68a1.185 1.185 0 0 1-1.185-1.186V5.673c0-.645.517-1.173 1.161-1.185h4.751a4.734 4.734 0 0 1 4.726 4.727 4.74 4.74 0 0 1-4.726 4.726H9.09v3.541c0 .666-.529 1.198-1.185 1.198z"/><path d="M7.905 18.094a.596.596 0 0 1-.598-.6V5.673c0-.322.252-.587.574-.598h4.751a4.144 4.144 0 0 1 4.14 4.14 4.145 4.145 0 0 1-4.14 4.14h-4.13v4.139c0 .334-.264.6-.597.6z"/><path d="M7.905 12.768h4.727a3.556 3.556 0 0 0 3.554-3.552 3.55 3.55 0 0 0-3.554-3.554H7.905v7.106z"/><path d="M8.502 12.182V6.26h4.14a2.953 2.953 0 0 1 2.955 2.956 2.954 2.954 0 0 1-2.955 2.956h-4.14v.01z"/><path d="M9.09 11.595V6.857h3.542a2.366 2.366 0 0 1 2.369 2.369 2.366 2.366 0 0 1-2.369 2.369H9.09z"/><path d="M9.676 11.01V7.444h2.956a1.783 1.783 0 0 1 0 3.566H9.676z"/><path d="M10.262 10.411v-2.38h2.37c.654 0 1.195.53 1.195 1.195 0 .668-.528 1.196-1.195 1.196h-2.37v-.011z"/><path d="M10.861 9.825V8.617h1.771c.333 0 .599.265.599.599a.595.595 0 0 1-.599.598h-1.771v.011z"/><ellipse cx="7.827" cy="5.678" rx="2.369" ry="2.369"/><path d="M18.036 16.137a2.361 2.361 0 1 1-3.863 2.716l-3.542-5.062a2.362 2.362 0 0 1 3.865-2.713l3.54 5.059z"/></g></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>icon_sl</title><g fill="#FFF"><path d="M12.632 3.316H7.893a2.365 2.365 0 0 0-2.368 2.369v11.821a2.365 2.365 0 0 0 2.368 2.369 2.366 2.366 0 0 0 2.369-2.369v-2.369h2.37c3.254 0 5.91-2.656 5.91-5.911 0-3.254-2.656-5.91-5.91-5.91z"/><path d="M7.905 19.277a1.771 1.771 0 0 1-1.771-1.771V5.673a1.77 1.77 0 0 1 1.759-1.772h4.739a5.336 5.336 0 0 1 5.325 5.325 5.334 5.334 0 0 1-5.325 5.324H9.676v2.956c0 .967-.794 1.771-1.771 1.771z"/><path d="M7.905 18.68a1.185 1.185 0 0 1-1.185-1.186V5.673c0-.645.517-1.173 1.161-1.185h4.751a4.734 4.734 0 0 1 4.726 4.727 4.74 4.74 0 0 1-4.726 4.726H9.09v3.541c0 .666-.529 1.198-1.185 1.198z"/><path d="M7.905 18.094a.596.596 0 0 1-.598-.6V5.673c0-.322.252-.587.574-.598h4.751a4.144 4.144 0 0 1 4.14 4.14 4.145 4.145 0 0 1-4.14 4.14h-4.13v4.139c0 .334-.264.6-.597.6z"/><path d="M7.905 12.768h4.727a3.556 3.556 0 0 0 3.554-3.552 3.55 3.55 0 0 0-3.554-3.554H7.905v7.106z"/><path d="M8.502 12.182V6.26h4.14a2.953 2.953 0 0 1 2.955 2.956 2.954 2.954 0 0 1-2.955 2.956h-4.14v.01z"/><path d="M9.09 11.595V6.857h3.542a2.366 2.366 0 0 1 2.369 2.369 2.366 2.366 0 0 1-2.369 2.369H9.09z"/><path d="M9.676 11.01V7.444h2.956a1.783 1.783 0 0 1 0 3.566H9.676z"/><path d="M10.262 10.411v-2.38h2.37c.654 0 1.195.53 1.195 1.195 0 .668-.528 1.196-1.195 1.196h-2.37v-.011z"/><path d="M10.861 9.825V8.617h1.771c.333 0 .599.265.599.599a.595.595 0 0 1-.599.598h-1.771v.011z"/><ellipse cx="7.827" cy="5.678" rx="2.369" ry="2.369"/><path d="M18.036 16.137a2.361 2.361 0 1 1-3.863 2.716l-3.542-5.062a2.362 2.362 0 0 1 3.865-2.713l3.54 5.059z"/></g></svg>
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path fill="#57A7ED" fill-rule="evenodd" d="M7.223 11.293l7.068-7.067a.999.999 0 1 1 1.414 1.414l-6.377 6.377 6.377 6.376a.999.999 0 1 1-1.414 1.415L7.223 12.74a1.001 1.001 0 0 1-.288-.827 1 1 0 0 1 .288-.62z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 307 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="7" viewBox="0 0 12 7">
|
||||||
|
<path fill="#8D99A4" fill-rule="evenodd" d="M6 3.828L2.462.291A1.003 1.003 0 0 0 1.05.293a.996.996 0 0 0-.002 1.412l4.247 4.247c.192.192.447.289.702.29a.981.981 0 0 0 .708-.29l4.247-4.247A1.003 1.003 0 0 0 10.95.293.996.996 0 0 0 9.538.29L6 3.828z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 344 B |
|
@ -72,7 +72,7 @@ height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||||
ga('create', 'UA-79146816-2', 'auto');
|
ga('create', 'UA-79146816-1', 'auto');
|
||||||
ga('send', 'pageview');
|
ga('send', 'pageview');
|
||||||
</script>
|
</script>
|
||||||
<script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script>
|
<script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script>
|
||||||
|
|
|
@ -68,7 +68,6 @@
|
||||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-WHLZ2RZ"
|
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-WHLZ2RZ"
|
||||||
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||||
<!-- End Google Tag Manager (noscript) -->
|
<!-- End Google Tag Manager (noscript) -->
|
||||||
|
|
||||||
<div class="news">
|
<div class="news">
|
||||||
<div class="news-inner">
|
<div class="news-inner">
|
||||||
<strong>$1M</strong> Bounty fund for open source projects
|
<strong>$1M</strong> Bounty fund for open source projects
|
||||||
|
@ -291,6 +290,25 @@
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
</script>
|
</script>
|
||||||
<!-- /Google Analytics -->
|
<!-- /Google Analytics -->
|
||||||
|
|
||||||
|
<!-- Facebook Pixel Code -->
|
||||||
|
<script>
|
||||||
|
!function(f,b,e,v,n,t,s)
|
||||||
|
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||||
|
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
||||||
|
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
||||||
|
n.queue=[];t=b.createElement(e);t.async=!0;
|
||||||
|
t.src=v;s=b.getElementsByTagName(e)[0];
|
||||||
|
s.parentNode.insertBefore(t,s)}(window, document,'script',
|
||||||
|
'https://connect.facebook.net/en_US/fbevents.js');
|
||||||
|
fbq('init', '293089407869419');
|
||||||
|
fbq('track', 'PageView');
|
||||||
|
</script>
|
||||||
|
<noscript><img height="1" width="1" style="display:none"
|
||||||
|
src="https://www.facebook.com/tr?id=293089407869419&ev=PageView&noscript=1"
|
||||||
|
/></noscript>
|
||||||
|
<!-- End Facebook Pixel Code -->
|
||||||
|
|
||||||
<!-- Start of HubSpot Embed Code -->
|
<!-- Start of HubSpot Embed Code -->
|
||||||
<script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script>
|
<script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script>
|
||||||
<!-- End of HubSpot Embed Code -->
|
<!-- End of HubSpot Embed Code -->
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||||
})(window,document,'script','dataLayer','GTM-WHLZ2RZ');</script>
|
})(window,document,'script','dataLayer','GTM-WHLZ2RZ');</script>
|
||||||
<!-- End Google Tag Manager -->
|
<!-- End Google Tag Manager -->
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -169,6 +168,10 @@
|
||||||
src="https://www.facebook.com/tr?id=293089407869419&ev=PageView&noscript=1"
|
src="https://www.facebook.com/tr?id=293089407869419&ev=PageView&noscript=1"
|
||||||
/></noscript>
|
/></noscript>
|
||||||
<!-- End Facebook Pixel Code -->
|
<!-- End Facebook Pixel Code -->
|
||||||
|
|
||||||
|
<!-- Start of HubSpot Embed Code -->
|
||||||
|
<script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script>
|
||||||
|
<!-- End of HubSpot Embed Code -->
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
:confirmation (eth/event-sig->topic-id "Confirmation(address,uint256)")})
|
:confirmation (eth/event-sig->topic-id "Confirmation(address,uint256)")})
|
||||||
|
|
||||||
(defn factory-contract-addr []
|
(defn factory-contract-addr []
|
||||||
(env :contract-factory-addr "0x47F56FD26EEeCda4FdF5DB5843De1fe75D2A64A6"))
|
(env :contract-factory-addr))
|
||||||
|
|
||||||
(defn tokenreg-base-format
|
(defn tokenreg-base-format
|
||||||
;; status tokenreg uses eg :base 18, while parity uses :base 1000000000000
|
;; status tokenreg uses eg :base 18, while parity uses :base 1000000000000
|
||||||
|
|
|
@ -312,12 +312,13 @@
|
||||||
|
|
||||||
(defn wrap-in-try-catch [func]
|
(defn wrap-in-try-catch [func]
|
||||||
(try
|
(try
|
||||||
|
(func)
|
||||||
(catch Throwable t
|
(catch Throwable t
|
||||||
(log/error t))))
|
(log/error t))))
|
||||||
|
|
||||||
(defn run-tasks [tasks]
|
(defn run-tasks [tasks]
|
||||||
(doall
|
(doall
|
||||||
(map (fn [func] (wrap-in-try-catch (func)))
|
(map (fn [func] (wrap-in-try-catch func))
|
||||||
tasks)))
|
tasks)))
|
||||||
|
|
||||||
(defn run-1-min-interval-tasks [time]
|
(defn run-1-min-interval-tasks [time]
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
(:require [re-frame.core :as rf]
|
(:require [re-frame.core :as rf]
|
||||||
[reagent.core :as r]
|
[reagent.core :as r]
|
||||||
[commiteth.common :refer [moment-timestamp
|
[commiteth.common :refer [moment-timestamp
|
||||||
|
items-per-page
|
||||||
|
display-data-page
|
||||||
issue-url]]))
|
issue-url]]))
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,23 +56,29 @@
|
||||||
(str (subs (str tla) 1) " " balance)])])
|
(str (subs (str tla) 1) " " balance)])])
|
||||||
[:div.time (moment-timestamp updated)]]]])
|
[:div.time (moment-timestamp updated)]]]])
|
||||||
|
|
||||||
|
(defn activity-list [{:keys [items item-count page-number total-count]
|
||||||
|
:as activity-page-data}
|
||||||
(defn activity-list [activity-items]
|
container-element]
|
||||||
[:div.ui.container.activity-container
|
(if (empty? (:items activity-page-data))
|
||||||
(if (empty? activity-items)
|
|
||||||
[:div.view-no-data-container
|
[:div.view-no-data-container
|
||||||
[:p "No recent activity yet"]]
|
[:p "No recent activity yet"]]
|
||||||
(into [:div.ui.items]
|
[:div
|
||||||
(for [item activity-items]
|
(let [left (inc (* (dec page-number) items-per-page))
|
||||||
^{:key item} [activity-item item])))] )
|
right (dec (+ left item-count))]
|
||||||
|
[:div.item-counts-label
|
||||||
|
[:span (str "Showing " left "-" right " of " total-count)]])
|
||||||
|
(display-data-page activity-page-data activity-item container-element)]))
|
||||||
|
|
||||||
(defn activity-page []
|
(defn activity-page []
|
||||||
(let [activity-items (rf/subscribe [:activity-feed])
|
(let [activity-page-data (rf/subscribe [:activities-page])
|
||||||
activity-feed-loading? (rf/subscribe [:get-in [:activity-feed-loading?]])]
|
activity-feed-loading? (rf/subscribe [:get-in [:activity-feed-loading?]])
|
||||||
|
container-element (atom nil)]
|
||||||
(fn []
|
(fn []
|
||||||
(if @activity-feed-loading?
|
(if @activity-feed-loading?
|
||||||
[:div.view-loading-container
|
[:div.view-loading-container
|
||||||
[:div.ui.active.inverted.dimmer
|
[:div.ui.active.inverted.dimmer
|
||||||
[:div.ui.text.loader.view-loading-label "Loading"]]]
|
[:div.ui.text.loader.view-loading-label "Loading"]]]
|
||||||
[activity-list @activity-items]))))
|
[:div.ui.container.activity-container
|
||||||
|
{:ref #(reset! container-element %1)}
|
||||||
|
[:div.activity-header "Activities"]
|
||||||
|
[activity-list @activity-page-data container-element]]))))
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
(ns commiteth.bounties
|
(ns commiteth.bounties
|
||||||
(:require [re-frame.core :as rf]
|
(:require [reagent.core :as r]
|
||||||
|
[re-frame.core :as rf]
|
||||||
[commiteth.common :refer [moment-timestamp
|
[commiteth.common :refer [moment-timestamp
|
||||||
issue-url]]))
|
display-data-page
|
||||||
|
items-per-page
|
||||||
|
issue-url]]
|
||||||
|
[commiteth.handlers :as handlers]
|
||||||
|
[commiteth.db :as db]
|
||||||
|
[commiteth.ui-model :as ui-model]
|
||||||
|
[commiteth.subscriptions :as subs]))
|
||||||
|
|
||||||
|
|
||||||
(defn bounty-item [bounty]
|
(defn bounty-item [bounty]
|
||||||
|
@ -42,23 +49,182 @@
|
||||||
[:div.ui.tiny.circular.image
|
[:div.ui.tiny.circular.image
|
||||||
[:img {:src avatar-url}]]]]))
|
[:img {:src avatar-url}]]]]))
|
||||||
|
|
||||||
(defn bounties-list [open-bounties]
|
(defn bounties-filter-tooltip-value-input-view [label tooltip-open? opts]
|
||||||
[:div.ui.container.open-bounties-container
|
[:div.open-bounties-filter-element-tooltip-value-input-container
|
||||||
[:div.open-bounties-header "Bounties"]
|
[:div.:input.open-bounties-filter-element-tooltip-value-input-label
|
||||||
(if (empty? open-bounties)
|
label]
|
||||||
[:div.view-no-data-container
|
[:input.open-bounties-filter-element-tooltip-value-input
|
||||||
[:p "No recent activity yet"]]
|
{:type "range"
|
||||||
(into [:div.ui.items]
|
:min (:min opts)
|
||||||
(for [bounty open-bounties]
|
:max (:max opts)
|
||||||
[bounty-item bounty])))])
|
:step (:step opts)
|
||||||
|
:value (:current-val opts)
|
||||||
|
:on-change (when-let [f (:on-change-val opts)]
|
||||||
|
#(-> % .-target .-value int f))
|
||||||
|
:on-focus #(reset! tooltip-open? true)}]])
|
||||||
|
|
||||||
|
(defmulti bounties-filter-tooltip-view #(-> %2 ::ui-model/bounty-filter-type.category))
|
||||||
|
|
||||||
|
(defmethod bounties-filter-tooltip-view ::ui-model/bounty-filter-type-category|range
|
||||||
|
[filter-type filter-type-def current-filter-value tooltip-open?]
|
||||||
|
(let [default-min (::ui-model/bounty-filter-type.min-val filter-type-def)
|
||||||
|
default-max (::ui-model/bounty-filter-type.max-val filter-type-def)
|
||||||
|
common-range-opts {:min default-min :max default-max}
|
||||||
|
current-min (or (first current-filter-value) default-min)
|
||||||
|
current-max (or (second current-filter-value) default-max)
|
||||||
|
on-change-fn (fn [min-val max-val]
|
||||||
|
(rf/dispatch [::handlers/set-open-bounty-filter-type
|
||||||
|
filter-type
|
||||||
|
[(min min-val default-max)
|
||||||
|
(max max-val default-min)]]))
|
||||||
|
on-min-change-fn (fn [new-min]
|
||||||
|
(let [new-max (max current-max (min default-max new-min))]
|
||||||
|
(on-change-fn new-min new-max)))
|
||||||
|
on-max-change-fn (fn [new-max]
|
||||||
|
(let [new-min (min current-min (max default-min new-max))]
|
||||||
|
(on-change-fn new-min new-max)))]
|
||||||
|
[:div.open-bounties-filter-list
|
||||||
|
(::ui-model/bounty-filter.type.header filter-type-def)
|
||||||
|
[bounties-filter-tooltip-value-input-view "Min" tooltip-open? (merge common-range-opts
|
||||||
|
{:current-val current-min
|
||||||
|
:on-change-val on-min-change-fn})]
|
||||||
|
[bounties-filter-tooltip-value-input-view "Max" tooltip-open? (merge common-range-opts
|
||||||
|
{:current-val current-max
|
||||||
|
:on-change-val on-max-change-fn})]]))
|
||||||
|
|
||||||
|
(defmethod bounties-filter-tooltip-view ::ui-model/bounty-filter-type-category|single-static-option
|
||||||
|
[filter-type filter-type-def current-filter-value tooltip-open?]
|
||||||
|
[:div.open-bounties-filter-list
|
||||||
|
(for [[option-type option-text] (::ui-model/bounty-filter-type.options filter-type-def)]
|
||||||
|
^{:key (str option-type)}
|
||||||
|
[:div.open-bounties-filter-list-option
|
||||||
|
(merge {:on-click #(do (rf/dispatch [::handlers/set-open-bounty-filter-type
|
||||||
|
filter-type
|
||||||
|
option-type])
|
||||||
|
(reset! tooltip-open? false))
|
||||||
|
:tab-index 0
|
||||||
|
:on-focus #(reset! tooltip-open? true)}
|
||||||
|
(when (= option-type current-filter-value)
|
||||||
|
{:class "active"}))
|
||||||
|
option-text])])
|
||||||
|
|
||||||
|
(defmethod bounties-filter-tooltip-view ::ui-model/bounty-filter-type-category|multiple-dynamic-options
|
||||||
|
[filter-type filter-type-def current-filter-value tooltip-open?]
|
||||||
|
(let [options (rf/subscribe [(::ui-model/bounty-filter-type.re-frame-subs-key-for-options filter-type-def)])]
|
||||||
|
[:div.open-bounties-filter-list
|
||||||
|
(for [option @options]
|
||||||
|
(let [active? (boolean (and current-filter-value (current-filter-value option)))]
|
||||||
|
^{:key (str option)}
|
||||||
|
[:div.open-bounties-filter-list-option-checkbox
|
||||||
|
[:label
|
||||||
|
{:on-click #(rf/dispatch [::handlers/set-open-bounty-filter-type
|
||||||
|
filter-type
|
||||||
|
(cond
|
||||||
|
(and active? (= #{option} current-filter-value)) nil
|
||||||
|
active? (disj current-filter-value option)
|
||||||
|
:else (into #{option} current-filter-value))])
|
||||||
|
:tab-index 0
|
||||||
|
:on-focus #(do (.stopPropagation %) (reset! tooltip-open? true))}
|
||||||
|
[:input
|
||||||
|
{:type "checkbox"
|
||||||
|
:checked active?
|
||||||
|
:on-focus #(reset! tooltip-open? true)}]
|
||||||
|
[:div.text option]]]))]))
|
||||||
|
|
||||||
|
(defn bounty-filter-view [filter-type current-filter-value]
|
||||||
|
(let [tooltip-open? (r/atom false)]
|
||||||
|
(fn [filter-type current-filter-value]
|
||||||
|
[:div.open-bounties-filter-element-container
|
||||||
|
{:tab-index 0
|
||||||
|
:on-focus #(reset! tooltip-open? true)
|
||||||
|
:on-blur #(reset! tooltip-open? false)}
|
||||||
|
[:div.open-bounties-filter-element
|
||||||
|
{:on-mouse-down #(swap! tooltip-open? not)
|
||||||
|
:class (when (or current-filter-value @tooltip-open?)
|
||||||
|
"open-bounties-filter-element-active")}
|
||||||
|
[:div.text
|
||||||
|
(if current-filter-value
|
||||||
|
(ui-model/bounty-filter-value->short-text filter-type current-filter-value)
|
||||||
|
(ui-model/bounty-filter-type->name filter-type))]
|
||||||
|
(when current-filter-value
|
||||||
|
[:div.remove-container
|
||||||
|
{:tab-index 0
|
||||||
|
:on-focus #(.stopPropagation %)}
|
||||||
|
[:img.remove
|
||||||
|
{:src "bounty-filter-remove.svg"
|
||||||
|
:on-mouse-down (fn [e]
|
||||||
|
(.stopPropagation e)
|
||||||
|
(rf/dispatch [::handlers/set-open-bounty-filter-type filter-type nil])
|
||||||
|
(reset! tooltip-open? false))}]])]
|
||||||
|
(when @tooltip-open?
|
||||||
|
[:div.open-bounties-filter-element-tooltip
|
||||||
|
[bounties-filter-tooltip-view
|
||||||
|
filter-type
|
||||||
|
(ui-model/bounty-filter-types-def filter-type)
|
||||||
|
current-filter-value
|
||||||
|
tooltip-open?]])])))
|
||||||
|
|
||||||
|
(defn bounty-filters-view []
|
||||||
|
(let [current-filters (rf/subscribe [::subs/open-bounties-filters])]
|
||||||
|
[:div.open-bounties-filter
|
||||||
|
; doall because derefs are not supported in lazy seqs: https://github.com/reagent-project/reagent/issues/18
|
||||||
|
(doall
|
||||||
|
(for [filter-type ui-model/bounty-filter-types]
|
||||||
|
^{:key (str filter-type)}
|
||||||
|
[bounty-filter-view
|
||||||
|
filter-type
|
||||||
|
(get @current-filters filter-type)]))]))
|
||||||
|
|
||||||
|
(defn bounties-sort-view []
|
||||||
|
(let [open? (r/atom false)]
|
||||||
|
(fn []
|
||||||
|
(let [current-sorting (rf/subscribe [::subs/open-bounties-sorting-type])]
|
||||||
|
[:div.open-bounties-sort
|
||||||
|
{:tab-index 0
|
||||||
|
:on-blur #(reset! open? false)}
|
||||||
|
[:div.open-bounties-sort-element
|
||||||
|
{:on-click #(swap! open? not)}
|
||||||
|
(ui-model/bounty-sorting-type->name @current-sorting)
|
||||||
|
[:div.icon-forward-white-box
|
||||||
|
[:img
|
||||||
|
{:src "icon-forward-white.svg"}]]]
|
||||||
|
(when @open?
|
||||||
|
[:div.open-bounties-sort-element-tooltip
|
||||||
|
(for [sorting-type (keys ui-model/bounty-sorting-types-def)]
|
||||||
|
^{:key (str sorting-type)}
|
||||||
|
[:div.open-bounties-sort-type
|
||||||
|
{:on-click #(do
|
||||||
|
(reset! open? false)
|
||||||
|
(rf/dispatch [::handlers/set-open-bounties-sorting-type sorting-type]))}
|
||||||
|
(ui-model/bounty-sorting-type->name sorting-type)])])]))))
|
||||||
|
|
||||||
|
(defn bounties-list [{:keys [items item-count page-number total-count]
|
||||||
|
:as bounty-page-data}
|
||||||
|
container-element]
|
||||||
|
(if (empty? items)
|
||||||
|
[:div.view-no-data-container
|
||||||
|
[:p "No matching bounties found."]]
|
||||||
|
[:div
|
||||||
|
(let [left (inc (* (dec page-number) items-per-page))
|
||||||
|
right (dec (+ left item-count))]
|
||||||
|
[:div.item-counts-label
|
||||||
|
[:span (str "Showing " left "-" right " of " total-count)]])
|
||||||
|
(display-data-page bounty-page-data bounty-item container-element)]))
|
||||||
|
|
||||||
(defn bounties-page []
|
(defn bounties-page []
|
||||||
(let [open-bounties (rf/subscribe [:open-bounties])
|
(let [bounty-page-data (rf/subscribe [:open-bounties-page])
|
||||||
open-bounties-loading? (rf/subscribe [:get-in [:open-bounties-loading?]])]
|
open-bounties-loading? (rf/subscribe [:get-in [:open-bounties-loading?]])
|
||||||
|
container-element (atom nil)]
|
||||||
(fn []
|
(fn []
|
||||||
(if @open-bounties-loading?
|
(if @open-bounties-loading?
|
||||||
[:div.view-loading-container
|
[:div.view-loading-container
|
||||||
[:div.ui.active.inverted.dimmer
|
[:div.ui.active.inverted.dimmer
|
||||||
[:div.ui.text.loader.view-loading-label "Loading"]]]
|
[:div.ui.text.loader.view-loading-label "Loading"]]]
|
||||||
[bounties-list @open-bounties]))))
|
[:div.ui.container.open-bounties-container
|
||||||
|
{:ref #(reset! container-element %1)}
|
||||||
|
[:div.open-bounties-header "Bounties"]
|
||||||
|
[:div.open-bounties-filter-and-sort
|
||||||
|
[bounty-filters-view]
|
||||||
|
[bounties-sort-view]]
|
||||||
|
[bounties-list @bounty-page-data container-element]]))
|
||||||
|
))
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
(ns commiteth.common
|
(ns commiteth.common
|
||||||
(:require [reagent.core :as r]
|
(:require [reagent.core :as r]
|
||||||
[re-frame.core :as rf]
|
[re-frame.core :as rf]
|
||||||
|
[clojure.string :as str]
|
||||||
[cljsjs.moment]))
|
[cljsjs.moment]))
|
||||||
|
|
||||||
(defn input [val-ratom props]
|
(defn input [val-ratom props]
|
||||||
|
@ -29,3 +30,96 @@
|
||||||
|
|
||||||
(defn issue-url [owner repo number]
|
(defn issue-url [owner repo number]
|
||||||
(str "https://github.com/" owner "/" repo "/issues/" number))
|
(str "https://github.com/" owner "/" repo "/issues/" number))
|
||||||
|
|
||||||
|
(def items-per-page 15)
|
||||||
|
|
||||||
|
(defn draw-page-numbers [page-number page-count container-element]
|
||||||
|
"Draw page numbers for the pagination component.
|
||||||
|
Inserts ellipsis when list is too long, by default
|
||||||
|
max 6 items are allowed"
|
||||||
|
(let [draw-page-num-fn (fn [current? i]
|
||||||
|
^{:key i}
|
||||||
|
[:div.rectangle-rounded
|
||||||
|
(if current?
|
||||||
|
{:class "page-num-active"}
|
||||||
|
{:class "grayed-out-page-num"
|
||||||
|
:on-click #(do
|
||||||
|
(rf/dispatch [:set-page-number i])
|
||||||
|
(when @container-element
|
||||||
|
(.scrollIntoView @container-element)))})
|
||||||
|
i])
|
||||||
|
max-page-nums 6]
|
||||||
|
[:div.page-nums-container
|
||||||
|
(cond (<= page-count max-page-nums)
|
||||||
|
(for [i (map inc (range page-count))]
|
||||||
|
(draw-page-num-fn (= i page-number) i))
|
||||||
|
(<= page-number (- max-page-nums 3))
|
||||||
|
(concat
|
||||||
|
(for [i (map inc (range (- max-page-nums 2)))]
|
||||||
|
(draw-page-num-fn (= i page-number) i))
|
||||||
|
[^{:key (dec max-page-nums)}
|
||||||
|
[:div.page-nav-text [:span "..."]]]
|
||||||
|
[(draw-page-num-fn false page-count)])
|
||||||
|
(>= page-number (- page-count (- max-page-nums 4)))
|
||||||
|
(concat
|
||||||
|
[(draw-page-num-fn false 1)
|
||||||
|
^{:key 2}
|
||||||
|
[:div.page-nav-text [:span "..."]]]
|
||||||
|
(for [i (map inc (range (- page-count 4) page-count))]
|
||||||
|
(draw-page-num-fn (= i page-number) i))
|
||||||
|
)
|
||||||
|
:else
|
||||||
|
(concat
|
||||||
|
[(draw-page-num-fn false 1)
|
||||||
|
^{:key 2} [:div.page-nav-text [:span "..."]]]
|
||||||
|
(for [i [(dec page-number) page-number (inc page-number)]]
|
||||||
|
(draw-page-num-fn (= i page-number) i))
|
||||||
|
[^{:key (dec page-count)} [:div.page-nav-text [:span "..."]]
|
||||||
|
(draw-page-num-fn false page-count)]))]))
|
||||||
|
|
||||||
|
(defn display-data-page [{:keys [items
|
||||||
|
item-count
|
||||||
|
total-count
|
||||||
|
page-number
|
||||||
|
page-count]}
|
||||||
|
draw-item-fn
|
||||||
|
container-element]
|
||||||
|
"Draw data items along with pagination controls"
|
||||||
|
(let [draw-items (fn []
|
||||||
|
(into [:div.ui.items]
|
||||||
|
(for [item items]
|
||||||
|
^{:key item} [draw-item-fn item])))
|
||||||
|
on-direction-click (fn [forward?]
|
||||||
|
#(when (or (and (< page-number page-count)
|
||||||
|
forward?)
|
||||||
|
(and (< 1 page-number)
|
||||||
|
(not forward?)))
|
||||||
|
(rf/dispatch [:set-page-number
|
||||||
|
(if forward?
|
||||||
|
(inc page-number)
|
||||||
|
(dec page-number))])
|
||||||
|
(when @container-element
|
||||||
|
(.scrollIntoView @container-element))))
|
||||||
|
draw-rect (fn [direction]
|
||||||
|
(let [forward? (= direction :forward)
|
||||||
|
gray-out? (or (and forward? (= page-number page-count))
|
||||||
|
(and (not forward?) (= page-number 1)))]
|
||||||
|
[:div.rectangle-rounded
|
||||||
|
(cond-> {:on-click (on-direction-click forward?)}
|
||||||
|
gray-out? (assoc :class "grayed-out-direction"))
|
||||||
|
[:img.icon-forward-gray
|
||||||
|
(cond-> {:src "icon-forward-gray.svg"}
|
||||||
|
forward? (assoc :class "flip-horizontal"))]]))]
|
||||||
|
(cond (<= total-count items-per-page)
|
||||||
|
[draw-items]
|
||||||
|
:else
|
||||||
|
[:div
|
||||||
|
[draw-items]
|
||||||
|
[:div.page-nav-container
|
||||||
|
[:div.page-direction-container
|
||||||
|
[draw-rect :backward]
|
||||||
|
[draw-rect :forward]]
|
||||||
|
[:div.page-nav-text [:span (str "Page " page-number " of " page-count)]]
|
||||||
|
[draw-page-numbers page-number page-count container-element]]])))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
(ns commiteth.db)
|
(ns commiteth.db
|
||||||
|
(:require [commiteth.ui-model :as ui-model]))
|
||||||
|
|
||||||
(def default-db
|
(def default-db
|
||||||
{:page :bounties
|
{:page :bounties
|
||||||
|
@ -8,6 +9,14 @@
|
||||||
:activity-feed-loading? false
|
:activity-feed-loading? false
|
||||||
:open-bounties-loading? false
|
:open-bounties-loading? false
|
||||||
:open-bounties []
|
:open-bounties []
|
||||||
|
:page-number 1
|
||||||
|
:bounty-page-number 1
|
||||||
|
:activity-page-number 1
|
||||||
|
::open-bounties-sorting-type ::ui-model/bounty-sorting-type|most-recent
|
||||||
|
::open-bounties-filters {::ui-model/bounty-filter-type|value nil
|
||||||
|
::ui-model/bounty-filter-type|currency nil
|
||||||
|
::ui-model/bounty-filter-type|date nil
|
||||||
|
::ui-model/bounty-filter-type|owner nil}
|
||||||
:owner-bounties {}
|
:owner-bounties {}
|
||||||
:top-hunters []
|
:top-hunters []
|
||||||
:activity-feed []})
|
:activity-feed []})
|
||||||
|
|
|
@ -11,7 +11,8 @@
|
||||||
[cljs-web3.eth :as web3-eth]
|
[cljs-web3.eth :as web3-eth]
|
||||||
[akiroz.re-frame.storage
|
[akiroz.re-frame.storage
|
||||||
:as rf-storage
|
:as rf-storage
|
||||||
:refer [reg-co-fx!]]))
|
:refer [reg-co-fx!]]
|
||||||
|
[commiteth.ui-model :as ui-model]))
|
||||||
|
|
||||||
|
|
||||||
(rf-storage/reg-co-fx! :commiteth-sob {:fx :store
|
(rf-storage/reg-co-fx! :commiteth-sob {:fx :store
|
||||||
|
@ -38,7 +39,6 @@
|
||||||
(println "redirecting to" path)
|
(println "redirecting to" path)
|
||||||
(set! (.-pathname js/location) path)))
|
(set! (.-pathname js/location) path)))
|
||||||
|
|
||||||
|
|
||||||
(reg-event-fx
|
(reg-event-fx
|
||||||
:initialize-db
|
:initialize-db
|
||||||
[(inject-cofx :store)]
|
[(inject-cofx :store)]
|
||||||
|
@ -70,7 +70,15 @@
|
||||||
(reg-event-db
|
(reg-event-db
|
||||||
:set-active-page
|
:set-active-page
|
||||||
(fn [db [_ page]]
|
(fn [db [_ page]]
|
||||||
(assoc db :page page)))
|
(assoc db :page page
|
||||||
|
:page-number 1
|
||||||
|
::db/open-bounties-filters {}
|
||||||
|
::db/open-bounties-sorting-type ::ui-model/bounty-sorting-type|most-recent)))
|
||||||
|
|
||||||
|
(reg-event-db
|
||||||
|
:set-page-number
|
||||||
|
(fn [db [_ page]]
|
||||||
|
(assoc db :page-number page)))
|
||||||
|
|
||||||
(reg-event-fx
|
(reg-event-fx
|
||||||
:set-flash-message
|
:set-flash-message
|
||||||
|
@ -85,7 +93,6 @@
|
||||||
(fn [db _]
|
(fn [db _]
|
||||||
(dissoc db :flash-message)))
|
(dissoc db :flash-message)))
|
||||||
|
|
||||||
|
|
||||||
(defn assoc-in-if-not-empty [m path val]
|
(defn assoc-in-if-not-empty [m path val]
|
||||||
(if (seq val)
|
(if (seq val)
|
||||||
(assoc-in m path val)
|
(assoc-in m path val)
|
||||||
|
@ -459,3 +466,16 @@
|
||||||
(fn [db [_]]
|
(fn [db [_]]
|
||||||
(.removeEventListener js/window "click" close-dropdown)
|
(.removeEventListener js/window "click" close-dropdown)
|
||||||
(assoc db :user-dropdown-open? false)))
|
(assoc db :user-dropdown-open? false)))
|
||||||
|
|
||||||
|
(reg-event-db
|
||||||
|
::set-open-bounties-sorting-type
|
||||||
|
(fn [db [_ sorting-type]]
|
||||||
|
(merge db {::db/open-bounties-sorting-type sorting-type
|
||||||
|
:page-number 1})))
|
||||||
|
|
||||||
|
(reg-event-db
|
||||||
|
::set-open-bounty-filter-type
|
||||||
|
(fn [db [_ filter-type filter-value]]
|
||||||
|
(-> db
|
||||||
|
(assoc-in [::db/open-bounties-filters filter-type] filter-value)
|
||||||
|
(assoc :page-number 1))))
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
(ns commiteth.subscriptions
|
(ns commiteth.subscriptions
|
||||||
(:require [re-frame.core :refer [reg-sub]]))
|
(:require [re-frame.core :refer [reg-sub]]
|
||||||
|
[commiteth.db :as db]
|
||||||
|
[commiteth.ui-model :as ui-model]
|
||||||
|
[commiteth.common :refer [items-per-page]]
|
||||||
|
[clojure.string :as string]))
|
||||||
|
|
||||||
(reg-sub
|
(reg-sub
|
||||||
:db
|
:db
|
||||||
|
@ -33,7 +37,28 @@
|
||||||
(reg-sub
|
(reg-sub
|
||||||
:open-bounties
|
:open-bounties
|
||||||
(fn [db _]
|
(fn [db _]
|
||||||
(:open-bounties db)))
|
(vec (:open-bounties db))))
|
||||||
|
|
||||||
|
(reg-sub
|
||||||
|
:page-number
|
||||||
|
(fn [db _]
|
||||||
|
(:page-number db)))
|
||||||
|
|
||||||
|
(reg-sub
|
||||||
|
:open-bounties-page
|
||||||
|
:<- [::filtered-and-sorted-open-bounties]
|
||||||
|
:<- [:page-number]
|
||||||
|
(fn [[open-bounties page-number] _]
|
||||||
|
(let [total-count (count open-bounties)
|
||||||
|
start (* (dec page-number) items-per-page)
|
||||||
|
end (min total-count (+ items-per-page start))
|
||||||
|
items (subvec open-bounties start end)]
|
||||||
|
{:items items
|
||||||
|
:item-count (count items)
|
||||||
|
:total-count total-count
|
||||||
|
:page-number page-number
|
||||||
|
:page-count (Math/ceil (/ total-count items-per-page))})))
|
||||||
|
|
||||||
|
|
||||||
(reg-sub
|
(reg-sub
|
||||||
:owner-bounties
|
:owner-bounties
|
||||||
|
@ -53,7 +78,23 @@
|
||||||
(reg-sub
|
(reg-sub
|
||||||
:activity-feed
|
:activity-feed
|
||||||
(fn [db _]
|
(fn [db _]
|
||||||
(:activity-feed db)))
|
(vec (:activity-feed db))))
|
||||||
|
|
||||||
|
(reg-sub
|
||||||
|
:activities-page
|
||||||
|
:<- [:activity-feed]
|
||||||
|
:<- [:page-number]
|
||||||
|
(fn [[activities page-number] _]
|
||||||
|
(let [total-count (count activities)
|
||||||
|
start (* (dec page-number) items-per-page)
|
||||||
|
end (min total-count (+ items-per-page start))
|
||||||
|
items (subvec activities start end)]
|
||||||
|
{:items items
|
||||||
|
:item-count (count items)
|
||||||
|
:total-count total-count
|
||||||
|
:page-number page-number
|
||||||
|
:page-count (Math/ceil (/ total-count items-per-page))})))
|
||||||
|
|
||||||
|
|
||||||
(reg-sub
|
(reg-sub
|
||||||
:gh-admin-token
|
:gh-admin-token
|
||||||
|
@ -80,3 +121,49 @@
|
||||||
:user-dropdown-open?
|
:user-dropdown-open?
|
||||||
(fn [db _]
|
(fn [db _]
|
||||||
(:user-dropdown-open? db)))
|
(:user-dropdown-open? db)))
|
||||||
|
|
||||||
|
(reg-sub
|
||||||
|
::open-bounties-sorting-type
|
||||||
|
(fn [db _]
|
||||||
|
(::db/open-bounties-sorting-type db)))
|
||||||
|
|
||||||
|
(reg-sub
|
||||||
|
::open-bounties-filters
|
||||||
|
(fn [db _]
|
||||||
|
(::db/open-bounties-filters db)))
|
||||||
|
|
||||||
|
(reg-sub
|
||||||
|
::open-bounties-owners
|
||||||
|
:<- [:open-bounties]
|
||||||
|
(fn [open-bounties _]
|
||||||
|
(->> open-bounties
|
||||||
|
(map :repo-owner)
|
||||||
|
set)))
|
||||||
|
|
||||||
|
(reg-sub
|
||||||
|
::open-bounties-owners-sorted
|
||||||
|
:<- [::open-bounties-owners]
|
||||||
|
(fn [owners _]
|
||||||
|
(sort-by string/lower-case owners)))
|
||||||
|
|
||||||
|
(reg-sub
|
||||||
|
::open-bounties-currencies
|
||||||
|
:<- [:open-bounties]
|
||||||
|
(fn [open-bounties _]
|
||||||
|
(let [token-ids (->> open-bounties
|
||||||
|
(map :tokens)
|
||||||
|
(mapcat keys)
|
||||||
|
(filter identity)
|
||||||
|
set)]
|
||||||
|
(into #{"ETH"} token-ids))))
|
||||||
|
|
||||||
|
(reg-sub
|
||||||
|
::filtered-and-sorted-open-bounties
|
||||||
|
:<- [:open-bounties]
|
||||||
|
:<- [::open-bounties-filters]
|
||||||
|
:<- [::open-bounties-sorting-type]
|
||||||
|
(fn [[open-bounties filters sorting-type] _]
|
||||||
|
(cond->> open-bounties
|
||||||
|
true (ui-model/filter-bounties filters)
|
||||||
|
sorting-type (ui-model/sort-bounties-by-sorting-type sorting-type)
|
||||||
|
true vec)))
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
(ns commiteth.ui-model
|
||||||
|
(:require [clojure.set :as set]
|
||||||
|
[cljs-time.core :as t]
|
||||||
|
[cljs-time.coerce :as t-coerce]
|
||||||
|
[cljs-time.format :as t-format]))
|
||||||
|
|
||||||
|
;;;; bounty sorting types
|
||||||
|
|
||||||
|
(def bounty-sorting-types-def
|
||||||
|
{::bounty-sorting-type|most-recent {::bounty-sorting-type.name "Most recent"
|
||||||
|
::bounty-sorting-type.sort-key-fn (fn [bounty]
|
||||||
|
(:updated bounty))
|
||||||
|
::bounty-sorting-type.sort-comparator-fn (comp - compare)}
|
||||||
|
::bounty-sorting-type|lowest-value {::bounty-sorting-type.name "Lowest value"
|
||||||
|
::bounty-sorting-type.sort-key-fn (fn [bounty]
|
||||||
|
(js/parseFloat (:value-usd bounty)))
|
||||||
|
::bounty-sorting-type.sort-comparator-fn compare}
|
||||||
|
::bounty-sorting-type|highest-value {::bounty-sorting-type.name "Highest value"
|
||||||
|
::bounty-sorting-type.sort-key-fn (fn [bounty]
|
||||||
|
(js/parseFloat (:value-usd bounty)))
|
||||||
|
::bounty-sorting-type.sort-comparator-fn (comp - compare)}
|
||||||
|
::bounty-sorting-type|owner {::bounty-sorting-type.name "Owner"
|
||||||
|
::bounty-sorting-type.sort-key-fn (fn [bounty]
|
||||||
|
(:repo-owner bounty))
|
||||||
|
::bounty-sorting-type.sort-comparator-fn compare}})
|
||||||
|
|
||||||
|
(defn bounty-sorting-type->name [sorting-type]
|
||||||
|
(-> bounty-sorting-types-def (get sorting-type) ::bounty-sorting-type.name))
|
||||||
|
|
||||||
|
(defn sort-bounties-by-sorting-type [sorting-type bounties]
|
||||||
|
(let [keyfn (-> bounty-sorting-types-def
|
||||||
|
sorting-type
|
||||||
|
::bounty-sorting-type.sort-key-fn)
|
||||||
|
comparator (-> bounty-sorting-types-def
|
||||||
|
sorting-type
|
||||||
|
::bounty-sorting-type.sort-comparator-fn)]
|
||||||
|
(sort-by keyfn comparator bounties)))
|
||||||
|
|
||||||
|
;;;; bounty filter types
|
||||||
|
|
||||||
|
(def bounty-filter-type-date-options-def {::bounty-filter-type-date-option|last-week "Last week"
|
||||||
|
::bounty-filter-type-date-option|last-month "Last month"
|
||||||
|
::bounty-filter-type-date-option|last-3-months "Last 3 months"})
|
||||||
|
|
||||||
|
(def bounty-filter-type-date-options (keys bounty-filter-type-date-options-def))
|
||||||
|
|
||||||
|
(defn bounty-filter-type-date-option->name [option]
|
||||||
|
(bounty-filter-type-date-options-def option))
|
||||||
|
|
||||||
|
(def bounty-filter-type-date-pre-predicate-value-processor
|
||||||
|
"It converts an option of the filter type date to a cljs-time interval in which
|
||||||
|
that option is valid, so that you can check `cljs-time.core.within?` against that
|
||||||
|
interval and know if a `cljs-time` date is valid for that filter type date option."
|
||||||
|
(fn [filter-value]
|
||||||
|
(let [filter-from (condp = filter-value
|
||||||
|
::bounty-filter-type-date-option|last-week (t/minus (t/now) (t/weeks 1))
|
||||||
|
::bounty-filter-type-date-option|last-month (t/minus (t/now) (t/months 1))
|
||||||
|
::bounty-filter-type-date-option|last-3-months (t/minus (t/now) (t/months 3)))]
|
||||||
|
(t/interval filter-from (t/now)))))
|
||||||
|
(def bounty-filter-type-date-predicate
|
||||||
|
(fn [filter-value-interval bounty]
|
||||||
|
(when-let [date-inst (:updated bounty)]
|
||||||
|
(let [date (-> date-inst inst-ms t-coerce/from-long)]
|
||||||
|
(t/within? filter-value-interval date)))))
|
||||||
|
|
||||||
|
(def bounty-filter-type-claims-options-def {::bounty-filter-type-claims-option|no-claims "Not claimed yet"})
|
||||||
|
|
||||||
|
(def bounty-filter-type-claims-options (keys bounty-filter-type-claims-options-def))
|
||||||
|
|
||||||
|
(defn bounty-filter-type-claims-option->name [option]
|
||||||
|
(bounty-filter-type-claims-options-def option))
|
||||||
|
|
||||||
|
(def bounty-filter-types-def
|
||||||
|
{::bounty-filter-type|value
|
||||||
|
{::bounty-filter-type.name "Value"
|
||||||
|
::bounty-filter-type.category ::bounty-filter-type-category|range
|
||||||
|
::bounty-filter-type.min-val 0
|
||||||
|
::bounty-filter-type.max-val 10000
|
||||||
|
::bounty-filter.type.header "$0 - $10000+"
|
||||||
|
::bounty-filter-type.predicate (fn [filter-value bounty]
|
||||||
|
(let [min-val (first filter-value)
|
||||||
|
max-val (second filter-value)]
|
||||||
|
(<= min-val (:value-usd bounty) max-val)))}
|
||||||
|
|
||||||
|
::bounty-filter-type|currency
|
||||||
|
{::bounty-filter-type.name "Currency"
|
||||||
|
::bounty-filter-type.category ::bounty-filter-type-category|multiple-dynamic-options
|
||||||
|
::bounty-filter-type.re-frame-subs-key-for-options :commiteth.subscriptions/open-bounties-currencies
|
||||||
|
::bounty-filter-type.predicate (fn [filter-value bounty]
|
||||||
|
(and (or (not-any? #{"ETH"} filter-value)
|
||||||
|
(< 0 (:balance-eth bounty)))
|
||||||
|
(set/subset? (->> filter-value (remove #{"ETH"}) set)
|
||||||
|
(-> bounty :tokens keys set))))}
|
||||||
|
|
||||||
|
::bounty-filter-type|date
|
||||||
|
{::bounty-filter-type.name "Date"
|
||||||
|
::bounty-filter-type.category ::bounty-filter-type-category|single-static-option
|
||||||
|
::bounty-filter-type.options bounty-filter-type-date-options-def
|
||||||
|
::bounty-filter-type.pre-predicate-value-processor bounty-filter-type-date-pre-predicate-value-processor
|
||||||
|
::bounty-filter-type.predicate bounty-filter-type-date-predicate}
|
||||||
|
|
||||||
|
::bounty-filter-type|owner
|
||||||
|
{::bounty-filter-type.name "Owner"
|
||||||
|
::bounty-filter-type.category ::bounty-filter-type-category|multiple-dynamic-options
|
||||||
|
::bounty-filter-type.re-frame-subs-key-for-options :commiteth.subscriptions/open-bounties-owners-sorted
|
||||||
|
::bounty-filter-type.predicate (fn [filter-value bounty]
|
||||||
|
(->> filter-value
|
||||||
|
(some #{(:repo-owner bounty)})
|
||||||
|
boolean))}
|
||||||
|
|
||||||
|
::bounty-filter-type|claims
|
||||||
|
{::bounty-filter-type.name "Claims"
|
||||||
|
::bounty-filter-type.category ::bounty-filter-type-category|single-static-option
|
||||||
|
::bounty-filter-type.options bounty-filter-type-claims-options-def
|
||||||
|
::bounty-filter-type.predicate (fn [filter-value bounty]
|
||||||
|
(condp = filter-value
|
||||||
|
::bounty-filter-type-claims-option|no-claims
|
||||||
|
(= 0 (:claim-count bounty))))}})
|
||||||
|
|
||||||
|
(def bounty-filter-types (keys bounty-filter-types-def))
|
||||||
|
|
||||||
|
(defn bounty-filter-type->name [filter-type]
|
||||||
|
(-> bounty-filter-types-def (get filter-type) ::bounty-filter-type.name))
|
||||||
|
|
||||||
|
(defn bounty-filter-value->short-text [filter-type filter-value]
|
||||||
|
(cond
|
||||||
|
(= filter-type ::bounty-filter-type|date)
|
||||||
|
(bounty-filter-type-date-option->name filter-value)
|
||||||
|
|
||||||
|
(#{::bounty-filter-type|owner
|
||||||
|
::bounty-filter-type|currency} filter-type)
|
||||||
|
(str (bounty-filter-type->name filter-type) " (" (count filter-value) ")")
|
||||||
|
|
||||||
|
(= filter-type ::bounty-filter-type|value)
|
||||||
|
(str "$" (first filter-value) "-$" (second filter-value))
|
||||||
|
|
||||||
|
(= filter-type ::bounty-filter-type|claims)
|
||||||
|
(bounty-filter-type-claims-option->name filter-value)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(str filter-type " with val " filter-value)))
|
||||||
|
|
||||||
|
(defn- bounty-filter-values-by-type->predicates [filters-by-type]
|
||||||
|
"It receives a map with filter types as keys and filter values as values and
|
||||||
|
returns a lazy seq of predicates, one for each pair of filter type and value.
|
||||||
|
Those predicate can receive a bounty and tell whether that bounty passes
|
||||||
|
the filter type with that filter value. It removes filter types with a `nil`
|
||||||
|
filter value."
|
||||||
|
(->> filters-by-type
|
||||||
|
; used `nil?` because a valid filter value can be `false`
|
||||||
|
(remove #(nil? (val %)))
|
||||||
|
(map (fn [[filter-type filter-value]]
|
||||||
|
(let [filter-type-def (bounty-filter-types-def filter-type)
|
||||||
|
pred (::bounty-filter-type.predicate filter-type-def)
|
||||||
|
pre-pred-processor (::bounty-filter-type.pre-predicate-value-processor filter-type-def)
|
||||||
|
filter-value (cond-> filter-value
|
||||||
|
pre-pred-processor pre-pred-processor)]
|
||||||
|
(partial pred filter-value))))))
|
||||||
|
|
||||||
|
(defn filter-bounties [filters-by-type bounties]
|
||||||
|
(let [filter-preds (bounty-filter-values-by-type->predicates filters-by-type)
|
||||||
|
filters-pred (fn [bounty]
|
||||||
|
(every? #(% bounty) filter-preds))]
|
||||||
|
(cond->> bounties
|
||||||
|
(not-empty filter-preds) (filter filters-pred))))
|
|
@ -421,6 +421,304 @@ label[for="input-hidden"] {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #42505c;
|
color: #42505c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter-and-sort {
|
||||||
|
margin-top: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.open-bounties-filter-and-sort {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter-element {
|
||||||
|
font-family: "PostGrotesk-Book";
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.0;
|
||||||
|
color: #8d99a4;
|
||||||
|
//padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: solid 1px rgba(151, 151, 151, 0.2);
|
||||||
|
margin-right: 10px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
margin-left: -6px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter-element:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter-element-active
|
||||||
|
{
|
||||||
|
background-color: #57a7ed;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter-element-container:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter-element-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
min-width: 227px;
|
||||||
|
margin-top: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
box-shadow: 0 15px 12px 0 rgba(161, 174, 182, 0.53), 0 0 38px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
font-family: "PostGrotesk-Book";
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
z-index: 999;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
|
.open-bounties-filter-element-tooltip-value-input-container {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter-element-tooltip-value-input-label {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter-element-tooltip-value-input {
|
||||||
|
margin-top: 24px;
|
||||||
|
width: 288.5px;
|
||||||
|
height: 4px;
|
||||||
|
background-color: #55a5ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
// generated with http://danielstern.ca/range.css/#/
|
||||||
|
input[type=range] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
//width: 100%;
|
||||||
|
margin: 14.5px 0;
|
||||||
|
}
|
||||||
|
input[type=range]:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
input[type=range]::-webkit-slider-runnable-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
|
||||||
|
background: #55a5ea;
|
||||||
|
border-radius: 0px;
|
||||||
|
border: 0px solid #010101;
|
||||||
|
}
|
||||||
|
input[type=range]::-webkit-slider-thumb {
|
||||||
|
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
|
||||||
|
border: 0px solid #000000;
|
||||||
|
height: 33px;
|
||||||
|
width: 33px;
|
||||||
|
border-radius: 50px;
|
||||||
|
background: #55a5ea;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin-top: -14.5px;
|
||||||
|
}
|
||||||
|
input[type=range]:focus::-webkit-slider-runnable-track {
|
||||||
|
background: #5aa7eb;
|
||||||
|
}
|
||||||
|
input[type=range]::-moz-range-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
|
||||||
|
background: #55a5ea;
|
||||||
|
border-radius: 0px;
|
||||||
|
border: 0px solid #010101;
|
||||||
|
}
|
||||||
|
input[type=range]::-moz-range-thumb {
|
||||||
|
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
|
||||||
|
border: 0px solid #000000;
|
||||||
|
height: 33px;
|
||||||
|
width: 33px;
|
||||||
|
border-radius: 50px;
|
||||||
|
background: #55a5ea;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
input[type=range]::-ms-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
input[type=range]::-ms-fill-lower {
|
||||||
|
background: #50a3e9;
|
||||||
|
border: 0px solid #010101;
|
||||||
|
border-radius: 0px;
|
||||||
|
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
|
||||||
|
}
|
||||||
|
input[type=range]::-ms-fill-upper {
|
||||||
|
background: #55a5ea;
|
||||||
|
border: 0px solid #010101;
|
||||||
|
border-radius: 0px;
|
||||||
|
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
|
||||||
|
}
|
||||||
|
input[type=range]::-ms-thumb {
|
||||||
|
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
|
||||||
|
border: 0px solid #000000;
|
||||||
|
height: 33px;
|
||||||
|
width: 33px;
|
||||||
|
border-radius: 50px;
|
||||||
|
background: #55a5ea;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
input[type=range]:focus::-ms-fill-lower {
|
||||||
|
background: #55a5ea;
|
||||||
|
}
|
||||||
|
input[type=range]:focus::-ms-fill-upper {
|
||||||
|
background: #5aa7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter-list-option {
|
||||||
|
font-family: "PostGrotesk-Book";
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.0;
|
||||||
|
color: #42505c;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: solid 1px rgba(151, 151, 151, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter-list-option.active {
|
||||||
|
background-color: #57a7ed;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter-list-option:not(:first-child) {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter-list-option:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #57a7ed;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter-list-option-checkbox {
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
label:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
label:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
background: green;
|
||||||
|
transform: scale(1.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.0;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-filter-list-option-checkbox:not(:first-child) {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-sort {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-sort:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-sort-element {
|
||||||
|
display: flex;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.0;
|
||||||
|
color: #8d99a4;
|
||||||
|
padding: 8px 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-sort-element:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-sort-element-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
//padding: 16px 0;
|
||||||
|
min-width: 200px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #1e3751;
|
||||||
|
font-family: "PostGrotesk-Book";
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.0;
|
||||||
|
text-align: left;
|
||||||
|
color: #ffffff;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-sort-type {
|
||||||
|
padding: 19px 24px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-bounties-sort-type:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-forward-white-box {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.open-bounty-item {
|
.open-bounty-item {
|
||||||
|
@ -504,7 +802,7 @@ label[for="input-hidden"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.open-claims-label {
|
.open-claims-label {
|
||||||
padding-left: 5px;
|
padding-left: 15px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: #57a7ed;
|
color: #57a7ed;
|
||||||
}
|
}
|
||||||
|
@ -531,7 +829,7 @@ label[for="input-hidden"] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
border: #e7e7e7 solid 0.1em!important;
|
border-bottom: #eaecee 1px solid !important;
|
||||||
box-shadow: none!important;
|
box-shadow: none!important;
|
||||||
border-radius: 0.3em!important;
|
border-radius: 0.3em!important;
|
||||||
padding: 0.8em 1em 1.1em!important;
|
padding: 0.8em 1em 1.1em!important;
|
||||||
|
@ -596,9 +894,16 @@ label[for="input-hidden"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-container {
|
.activity-container {
|
||||||
|
background-color: #fff;
|
||||||
transform: translate(0, -45px);
|
transform: translate(0, -45px);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
padding: 24px;
|
||||||
|
.activity-header {
|
||||||
|
font-family: "PostGrotesk-Medium";
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #42505c;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-row {
|
.footer-row {
|
||||||
|
@ -878,3 +1183,106 @@ body {
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rectangle-rounded {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
color: #57a7ed;
|
||||||
|
/*opacity: 0.2;*/
|
||||||
|
border-radius: 22.5px;
|
||||||
|
/*background-color: #57a7ed;*/
|
||||||
|
background-color: rgba(87,167,237,.2);
|
||||||
|
font-family: "PostGrotesk-Medium";
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
margin: 0 6px;
|
||||||
|
flex: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-num-active {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grayed-out-page-num {
|
||||||
|
color: #8d99a4;
|
||||||
|
background-color: #f2f5f8;
|
||||||
|
opacity: 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grayed-out-direction {
|
||||||
|
color: #8d99a4;
|
||||||
|
background-color: #f2f5f8;
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: auto
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-horizontal {
|
||||||
|
-moz-transform: scaleX(-1);
|
||||||
|
-webkit-transform: scaleX(-1);
|
||||||
|
-o-transform: scaleX(-1);
|
||||||
|
transform: scaleX(-1);
|
||||||
|
-ms-filter: fliph; /*IE*/
|
||||||
|
filter: fliph;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-forward-gray {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-nav-container {
|
||||||
|
display: flex;
|
||||||
|
margin: 0 -6px;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-direction-container {
|
||||||
|
display: flex;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-nums-container {
|
||||||
|
display: flex;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
margin-left: auto;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-nav-text {
|
||||||
|
font-family: PostGrotesk-Book;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #8d99a4;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
margin: 0 6px;
|
||||||
|
flex: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-counts-label {
|
||||||
|
margin: auto;
|
||||||
|
font-family: "PostGrotesk-Book";
|
||||||
|
font-size: 15px;
|
||||||
|
color: #8d99a4;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>icon_sl</title><g fill="#000"><path d="M12.632 3.316H7.893a2.365 2.365 0 0 0-2.368 2.369v11.821a2.365 2.365 0 0 0 2.368 2.369 2.366 2.366 0 0 0 2.369-2.369v-2.369h2.37c3.254 0 5.91-2.656 5.91-5.911 0-3.254-2.656-5.91-5.91-5.91z"/><path d="M7.905 19.277a1.771 1.771 0 0 1-1.771-1.771V5.673a1.77 1.77 0 0 1 1.759-1.772h4.739a5.336 5.336 0 0 1 5.325 5.325 5.334 5.334 0 0 1-5.325 5.324H9.676v2.956c0 .967-.794 1.771-1.771 1.771z"/><path d="M7.905 18.68a1.185 1.185 0 0 1-1.185-1.186V5.673c0-.645.517-1.173 1.161-1.185h4.751a4.734 4.734 0 0 1 4.726 4.727 4.74 4.74 0 0 1-4.726 4.726H9.09v3.541c0 .666-.529 1.198-1.185 1.198z"/><path d="M7.905 18.094a.596.596 0 0 1-.598-.6V5.673c0-.322.252-.587.574-.598h4.751a4.144 4.144 0 0 1 4.14 4.14 4.145 4.145 0 0 1-4.14 4.14h-4.13v4.139c0 .334-.264.6-.597.6z"/><path d="M7.905 12.768h4.727a3.556 3.556 0 0 0 3.554-3.552 3.55 3.55 0 0 0-3.554-3.554H7.905v7.106z"/><path d="M8.502 12.182V6.26h4.14a2.953 2.953 0 0 1 2.955 2.956 2.954 2.954 0 0 1-2.955 2.956h-4.14v.01z"/><path d="M9.09 11.595V6.857h3.542a2.366 2.366 0 0 1 2.369 2.369 2.366 2.366 0 0 1-2.369 2.369H9.09z"/><path d="M9.676 11.01V7.444h2.956a1.783 1.783 0 0 1 0 3.566H9.676z"/><path d="M10.262 10.411v-2.38h2.37c.654 0 1.195.53 1.195 1.195 0 .668-.528 1.196-1.195 1.196h-2.37v-.011z"/><path d="M10.861 9.825V8.617h1.771c.333 0 .599.265.599.599a.595.595 0 0 1-.599.598h-1.771v.011z"/><ellipse cx="7.827" cy="5.678" rx="2.369" ry="2.369"/><path d="M18.036 16.137a2.361 2.361 0 1 1-3.863 2.716l-3.542-5.062a2.362 2.362 0 0 1 3.865-2.713l3.54 5.059z"/></g></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>icon_sl</title><g fill="#FFF"><path d="M12.632 3.316H7.893a2.365 2.365 0 0 0-2.368 2.369v11.821a2.365 2.365 0 0 0 2.368 2.369 2.366 2.366 0 0 0 2.369-2.369v-2.369h2.37c3.254 0 5.91-2.656 5.91-5.911 0-3.254-2.656-5.91-5.91-5.91z"/><path d="M7.905 19.277a1.771 1.771 0 0 1-1.771-1.771V5.673a1.77 1.77 0 0 1 1.759-1.772h4.739a5.336 5.336 0 0 1 5.325 5.325 5.334 5.334 0 0 1-5.325 5.324H9.676v2.956c0 .967-.794 1.771-1.771 1.771z"/><path d="M7.905 18.68a1.185 1.185 0 0 1-1.185-1.186V5.673c0-.645.517-1.173 1.161-1.185h4.751a4.734 4.734 0 0 1 4.726 4.727 4.74 4.74 0 0 1-4.726 4.726H9.09v3.541c0 .666-.529 1.198-1.185 1.198z"/><path d="M7.905 18.094a.596.596 0 0 1-.598-.6V5.673c0-.322.252-.587.574-.598h4.751a4.144 4.144 0 0 1 4.14 4.14 4.145 4.145 0 0 1-4.14 4.14h-4.13v4.139c0 .334-.264.6-.597.6z"/><path d="M7.905 12.768h4.727a3.556 3.556 0 0 0 3.554-3.552 3.55 3.55 0 0 0-3.554-3.554H7.905v7.106z"/><path d="M8.502 12.182V6.26h4.14a2.953 2.953 0 0 1 2.955 2.956 2.954 2.954 0 0 1-2.955 2.956h-4.14v.01z"/><path d="M9.09 11.595V6.857h3.542a2.366 2.366 0 0 1 2.369 2.369 2.366 2.366 0 0 1-2.369 2.369H9.09z"/><path d="M9.676 11.01V7.444h2.956a1.783 1.783 0 0 1 0 3.566H9.676z"/><path d="M10.262 10.411v-2.38h2.37c.654 0 1.195.53 1.195 1.195 0 .668-.528 1.196-1.195 1.196h-2.37v-.011z"/><path d="M10.861 9.825V8.617h1.771c.333 0 .599.265.599.599a.595.595 0 0 1-.599.598h-1.771v.011z"/><ellipse cx="7.827" cy="5.678" rx="2.369" ry="2.369"/><path d="M18.036 16.137a2.361 2.361 0 1 1-3.863 2.716l-3.542-5.062a2.362 2.362 0 0 1 3.865-2.713l3.54 5.059z"/></g></svg>
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -290,6 +290,25 @@
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
</script>
|
</script>
|
||||||
<!-- /Google Analytics -->
|
<!-- /Google Analytics -->
|
||||||
|
|
||||||
|
<!-- Facebook Pixel Code -->
|
||||||
|
<script>
|
||||||
|
!function(f,b,e,v,n,t,s)
|
||||||
|
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||||
|
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
||||||
|
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
||||||
|
n.queue=[];t=b.createElement(e);t.async=!0;
|
||||||
|
t.src=v;s=b.getElementsByTagName(e)[0];
|
||||||
|
s.parentNode.insertBefore(t,s)}(window, document,'script',
|
||||||
|
'https://connect.facebook.net/en_US/fbevents.js');
|
||||||
|
fbq('init', '293089407869419');
|
||||||
|
fbq('track', 'PageView');
|
||||||
|
</script>
|
||||||
|
<noscript><img height="1" width="1" style="display:none"
|
||||||
|
src="https://www.facebook.com/tr?id=293089407869419&ev=PageView&noscript=1"
|
||||||
|
/></noscript>
|
||||||
|
<!-- End Facebook Pixel Code -->
|
||||||
|
|
||||||
<!-- Start of HubSpot Embed Code -->
|
<!-- Start of HubSpot Embed Code -->
|
||||||
<script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script>
|
<script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script>
|
||||||
<!-- End of HubSpot Embed Code -->
|
<!-- End of HubSpot Embed Code -->
|
||||||
|
|
|
@ -168,6 +168,10 @@
|
||||||
src="https://www.facebook.com/tr?id=293089407869419&ev=PageView&noscript=1"
|
src="https://www.facebook.com/tr?id=293089407869419&ev=PageView&noscript=1"
|
||||||
/></noscript>
|
/></noscript>
|
||||||
<!-- End Facebook Pixel Code -->
|
<!-- End Facebook Pixel Code -->
|
||||||
|
|
||||||
|
<!-- Start of HubSpot Embed Code -->
|
||||||
|
<script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script>
|
||||||
|
<!-- End of HubSpot Embed Code -->
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
node ('linux1') {sauce('1be1b688-e0e7-4314-92a0-db11f52d3c00') {
|
||||||
|
checkout([$class: 'GitSCM', branches: [[name: '*/develop']], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'CleanBeforeCheckout']], submoduleCfg: [], userRemoteConfigs: [[url: 'https://github.com/status-im/open-bounty.git']]])
|
||||||
|
configFileProvider([configFile(fileId: 'sob_automation_test_config', targetLocation: 'test/end-to-end/tests')]) {
|
||||||
|
try {sh 'cd test/end-to-end/tests && python3 -m pytest -m sanity --build=$BUILD_NAME -v -n 1'
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
saucePublisher()
|
||||||
|
junit testDataPublishers: [[$class: 'SauceOnDemandReportPublisher', jobVisibility: 'public']], testResults: 'test/end-to-end/tests/*.xml' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
from pages.base_page import BasePageObject
|
from pages.base_page import BasePageObject
|
||||||
from pages.base_element import *
|
from pages.base_element import *
|
||||||
|
from tests import test_data
|
||||||
|
|
||||||
|
|
||||||
class BountiesHeader(BaseText):
|
class BountiesHeader(BaseText):
|
||||||
|
@ -15,11 +16,38 @@ class TopHuntersHeader(BaseText):
|
||||||
super(TopHuntersHeader, self).__init__(driver)
|
super(TopHuntersHeader, self).__init__(driver)
|
||||||
self.locator = self.Locator.css_selector('.top-hunters-header')
|
self.locator = self.Locator.css_selector('.top-hunters-header')
|
||||||
|
|
||||||
|
class BountyTitles(BaseText):
|
||||||
|
|
||||||
|
def __init__(self, driver):
|
||||||
|
super(BountyTitles, self).__init__(driver)
|
||||||
|
self.locator = self.Locator.css_selector('.open-bounty-item-content .header')
|
||||||
|
|
||||||
|
|
||||||
|
class BountyItemRows(BaseText):
|
||||||
|
|
||||||
|
def __init__(self, driver):
|
||||||
|
super(BountyItemRows, self).__init__(driver)
|
||||||
|
self.locator = self.Locator.css_selector('.open-bounty-item-content .bounty-item-row')
|
||||||
|
|
||||||
|
class BountyFooters(BaseText):
|
||||||
|
|
||||||
|
def __init__(self, driver):
|
||||||
|
super(BountyFooters, self).__init__(driver)
|
||||||
|
self.locator = self.Locator.css_selector('.open-bounty-item-content .footer-row')
|
||||||
|
|
||||||
|
|
||||||
class BountiesPage(BasePageObject):
|
class BountiesPage(BasePageObject):
|
||||||
def __init__(self, driver):
|
def __init__(self, driver):
|
||||||
super(BountiesPage, self).__init__(driver)
|
super(BountiesPage, self).__init__(driver)
|
||||||
|
|
||||||
self.driver = driver
|
self.driver = driver
|
||||||
|
|
||||||
self.bounties_header = BountiesHeader(self.driver)
|
self.bounties_header = BountiesHeader(self.driver)
|
||||||
self.top_hunters_header = TopHuntersHeader(self.driver)
|
self.top_hunters_header = TopHuntersHeader(self.driver)
|
||||||
|
self.bounty_titles = BountyTitles(self.driver)
|
||||||
|
self.bounty_item_rows = BountyItemRows(self.driver)
|
||||||
|
self.bounty_footers = BountyFooters(self.driver)
|
||||||
|
|
||||||
|
def get_bounties_page(self):
|
||||||
|
self.driver.get(test_data.config['Common']['url'] + 'app')
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from pages.base_page import BasePageObject
|
from pages.base_page import BasePageObject
|
||||||
from pages.base_element import *
|
from pages.base_element import *
|
||||||
|
from tests import test_data
|
||||||
|
|
||||||
|
|
||||||
class LoginButton(BaseButton):
|
class LoginButton(BaseButton):
|
||||||
|
@ -20,4 +21,4 @@ class LandingPage(BasePageObject):
|
||||||
self.login_button = LoginButton(self.driver)
|
self.login_button = LoginButton(self.driver)
|
||||||
|
|
||||||
def get_landing_page(self):
|
def get_landing_page(self):
|
||||||
self.driver.get('https://openbounty.status.im:444/')
|
self.driver.get(test_data.config['Common']['url'])
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import time, pytest
|
import time, pytest
|
||||||
from pages.base_element import *
|
from pages.base_element import *
|
||||||
from pages.base_page import BasePageObject
|
from pages.base_page import BasePageObject
|
||||||
|
from tests import test_data
|
||||||
|
|
||||||
|
|
||||||
class EmailEditbox(BaseEditBox):
|
class EmailEditbox(BaseEditBox):
|
||||||
|
@ -84,7 +85,7 @@ class LabelsButton(BaseButton):
|
||||||
class BountyLabel(BaseButton):
|
class BountyLabel(BaseButton):
|
||||||
def __init__(self, driver):
|
def __init__(self, driver):
|
||||||
super(LabelsButton.BountyLabel, self).__init__(driver)
|
super(LabelsButton.BountyLabel, self).__init__(driver)
|
||||||
self.locator = self.Locator.css_selector("[data-name='bounty']")
|
self.locator = self.Locator.css_selector("[data-name='748942015']")
|
||||||
|
|
||||||
class CrossButton(BaseButton):
|
class CrossButton(BaseButton):
|
||||||
def __init__(self, driver):
|
def __init__(self, driver):
|
||||||
|
@ -156,11 +157,14 @@ class GithubPage(BasePageObject):
|
||||||
def create_new_bounty(self):
|
def create_new_bounty(self):
|
||||||
self.get_issues_page()
|
self.get_issues_page()
|
||||||
self.new_issue_button.click()
|
self.new_issue_button.click()
|
||||||
self.issue_title_input.send_keys('auto_test_bounty_%s' % self.time_now)
|
test_data.issue = dict()
|
||||||
|
test_data.issue['title'] = 'auto_test_bounty_%s' % self.time_now
|
||||||
|
self.issue_title_input.send_keys(test_data.issue['title'])
|
||||||
self.labels_button.click()
|
self.labels_button.click()
|
||||||
self.bounty_label.click()
|
self.bounty_label.click()
|
||||||
self.cross_button.click()
|
self.cross_button.click()
|
||||||
self.submit_new_issue_button.click()
|
self.submit_new_issue_button.click()
|
||||||
|
return test_data.issue['title']
|
||||||
|
|
||||||
def get_deployed_contract(self, wait=120):
|
def get_deployed_contract(self, wait=120):
|
||||||
for i in range(wait):
|
for i in range(wait):
|
||||||
|
|
|
@ -1,8 +1,17 @@
|
||||||
|
import configparser
|
||||||
|
|
||||||
class TestData(object):
|
class TestData(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.test_name = None
|
self.test_name = None
|
||||||
|
self.config = configparser.ConfigParser()
|
||||||
|
|
||||||
|
# define here path to your config.ini file
|
||||||
|
#example - config_example.ini
|
||||||
|
|
||||||
|
self.config.read('config.ini')
|
||||||
|
self.base_case_issue = dict()
|
||||||
|
self.base_case_issue['title'] = 'Very first auto_test_bounty'
|
||||||
|
|
||||||
|
|
||||||
test_data = TestData()
|
test_data = TestData()
|
||||||
|
|
|
@ -39,18 +39,29 @@ class BaseTestCase:
|
||||||
desired_caps['captureHtml'] = False
|
desired_caps['captureHtml'] = False
|
||||||
return desired_caps
|
return desired_caps
|
||||||
|
|
||||||
|
@property
|
||||||
|
def environment(self):
|
||||||
|
return pytest.config.getoption('env')
|
||||||
|
|
||||||
def setup_method(self):
|
def setup_method(self):
|
||||||
|
|
||||||
self.errors = []
|
self.errors = []
|
||||||
|
self.cleanup = None
|
||||||
|
|
||||||
# options = webdriver.ChromeOptions()
|
if self.environment == 'local':
|
||||||
# options.add_argument('--start-fullscreen')
|
options = webdriver.ChromeOptions()
|
||||||
# options.add_extension(path.abspath('/resources/metamask3_12_0.crx'))
|
options.add_argument('--start-fullscreen')
|
||||||
|
options.add_extension(
|
||||||
|
path.abspath(test_data.config['Paths']['tests_absolute'] + 'resources/metamask3_12_0.crx'))
|
||||||
|
# for chromedriver 2.35
|
||||||
|
self.driver = webdriver.Chrome(chrome_options=options)
|
||||||
|
if self.environment == 'sauce':
|
||||||
self.driver = webdriver.Remote(self.executor_sauce_lab,
|
self.driver = webdriver.Remote(self.executor_sauce_lab,
|
||||||
desired_capabilities=self.capabilities_sauce_lab)
|
desired_capabilities=self.capabilities_sauce_lab)
|
||||||
self.driver.implicitly_wait(5)
|
self.driver.implicitly_wait(5)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def verify_no_errors(self):
|
def verify_no_errors(self):
|
||||||
if self.errors:
|
if self.errors:
|
||||||
msg = ''
|
msg = ''
|
||||||
|
@ -59,7 +70,7 @@ class BaseTestCase:
|
||||||
pytest.fail(msg, pytrace=False)
|
pytest.fail(msg, pytrace=False)
|
||||||
|
|
||||||
def teardown_method(self):
|
def teardown_method(self):
|
||||||
|
if self.cleanup:
|
||||||
remove_application(self.driver)
|
remove_application(self.driver)
|
||||||
remove_installation(self.driver)
|
remove_installation(self.driver)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
[Common]
|
||||||
|
;app URL
|
||||||
|
url = https://openbounty.status.im:444/
|
||||||
|
[Paths]
|
||||||
|
;AbsolutePath to 'tests' folder
|
||||||
|
tests_absolute = /usr/dir/open-bounty/test/end-to-end/tests/
|
||||||
|
[ORG]
|
||||||
|
;GitHub credentials for organization
|
||||||
|
gh_login = login
|
||||||
|
gh_password = password
|
||||||
|
;MetaMask password for organization
|
||||||
|
mm_password = password
|
||||||
|
|
|
@ -11,6 +11,10 @@ def pytest_addoption(parser):
|
||||||
action='store',
|
action='store',
|
||||||
default=True,
|
default=True,
|
||||||
help='Display each test step in terminal as plain text: True/False')
|
help='Display each test step in terminal as plain text: True/False')
|
||||||
|
parser.addoption('--env',
|
||||||
|
action='store',
|
||||||
|
default='sauce',
|
||||||
|
help='Specify environment, sauce or local')
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
|
|
|
@ -1,22 +1,42 @@
|
||||||
import pytest
|
import pytest
|
||||||
from os import environ
|
from os import environ
|
||||||
from pages.openbounty.landing import LandingPage
|
from pages.openbounty.landing import LandingPage
|
||||||
|
from pages.openbounty.bounties import BountiesPage
|
||||||
from tests.basetestcase import BaseTestCase
|
from tests.basetestcase import BaseTestCase
|
||||||
|
from tests import test_data
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.sanity
|
@pytest.mark.sanity
|
||||||
class TestLogin(BaseTestCase):
|
class TestLogin(BaseTestCase):
|
||||||
|
|
||||||
def test_deploy_new_contract(self):
|
def test_deploy_new_contract(self):
|
||||||
|
self.cleanup = True
|
||||||
landing = LandingPage(self.driver)
|
landing = LandingPage(self.driver)
|
||||||
landing.get_landing_page()
|
landing.get_landing_page()
|
||||||
|
|
||||||
|
# Sign Up to SOB
|
||||||
github = landing.login_button.click()
|
github = landing.login_button.click()
|
||||||
github.sign_in('anna04test',
|
github.sign_in(test_data.config['ORG']['gh_login'],
|
||||||
'f@E23D3H15Rd')
|
test_data.config['ORG']['gh_password'])
|
||||||
assert github.permission_type.text == 'Personal user data'
|
assert github.permission_type.text == 'Personal user data'
|
||||||
bounties_page = github.authorize_sob.click()
|
bounties_page = github.authorize_sob.click()
|
||||||
|
|
||||||
|
# SOB Plugin installation and navigate to "Open bounties"
|
||||||
github.install_sob_plugin()
|
github.install_sob_plugin()
|
||||||
assert bounties_page.bounties_header.text == 'Bounties'
|
assert bounties_page.bounties_header.text == 'Bounties'
|
||||||
assert bounties_page.top_hunters_header.text == 'Top hunters'
|
assert bounties_page.top_hunters_header.text == 'Top 5 hunters'
|
||||||
|
|
||||||
|
# Waiting for deployed contract; test_data.issue created here
|
||||||
github.create_new_bounty()
|
github.create_new_bounty()
|
||||||
github.get_deployed_contract()
|
github.get_deployed_contract()
|
||||||
|
|
||||||
|
# Navigate and check top bounty in "Open bounties"
|
||||||
|
bounties_page = BountiesPage(self.driver)
|
||||||
|
bounties_page.get_bounties_page()
|
||||||
|
titles = bounties_page.bounty_titles.find_elements()
|
||||||
|
assert titles[0].text == test_data.issue['title']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|