@ -0,0 +1,25 @@ |
|||
node_modules |
|||
.idea |
|||
|
|||
.git |
|||
|
|||
docker/ |
|||
!docker/concordia-contracts/migrate.sh |
|||
!docker/concordia-contracts/test-contracts.sh |
|||
!docker/concordia-app/test-app.sh |
|||
!docker/concordia-app/nginx.conf |
|||
!docker/concordia-app/create-environment.sh |
|||
!docker/concordia-app/run.sh |
|||
!docker/ganache/start-blockchain.sh |
|||
|
|||
packages/*/node_modules |
|||
packages/*/dist |
|||
packages/*/coverage |
|||
packages/*/*.env* |
|||
# TO-NEVER-DO: exclude the build folder of the contracts package, it's needed for building the application image. |
|||
packages/concordia-app/build |
|||
|
|||
Jenkinsfile |
|||
|
|||
README.md |
|||
packages/*/README.md |
@ -1 +1,10 @@ |
|||
# Set the default behavior, in case people don't have core.autocrlf set. |
|||
* text=auto eol=lf |
|||
|
|||
# Denote all files that are truly binary and should not be modified. |
|||
*.png binary |
|||
*.jpg binary |
|||
*.ico binary |
|||
|
|||
# Solidity |
|||
*.sol linguist-language=Solidity |
|||
|
@ -1,26 +1,38 @@ |
|||
# See https://help.github.com/ignore-files/ for more about ignoring files. |
|||
|
|||
# Node |
|||
/node_modules |
|||
package-lock.json |
|||
packages/*/node_modules |
|||
packages/concordia-contracts/build |
|||
|
|||
# Testing |
|||
/coverage |
|||
# IDE |
|||
.DS_Store |
|||
.idea |
|||
|
|||
# Production |
|||
# Build Directories |
|||
/build |
|||
/src/build |
|||
/packages/concordia-app/build |
|||
/packages/concordia-contracts/build |
|||
|
|||
# Logs |
|||
/log |
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
|
|||
# Docker volumes |
|||
docker/volumes |
|||
docker/ganache/volumes |
|||
docker/concordia-contracts-provider/volumes |
|||
docker/concordia-pinner/volumes |
|||
docker/reports |
|||
|
|||
# Misc |
|||
.DS_Store |
|||
.env.local |
|||
.env.development.local |
|||
.env.test.local |
|||
.env.production.local |
|||
|
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
# Lerna |
|||
*.lerna_backup |
|||
|
|||
# Jetbrains |
|||
.idea |
|||
yarn-clean.sh |
|||
|
@ -0,0 +1,21 @@ |
|||
MIT License |
|||
|
|||
Copyright (c) 2020 ECEntrics |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
@ -1,3 +1,18 @@ |
|||
# Apella |
|||
# Concordia |
|||
> A distributed forum using Blockchain, supporting direct democratic voting |
|||
|
|||
*Note: This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).* |
|||
## Setup |
|||
|
|||
You can find detailed instructions on how to get started with each service in the respective package directories. The |
|||
overall setup pipeline **for development** is this though: |
|||
|
|||
- start rendezvous server and ganache |
|||
- migrate contracts |
|||
- start application |
|||
|
|||
## Using Docker images |
|||
|
|||
This project provides docker images for a number of services required to set up Concordia, as well as for Concordia |
|||
itself. |
|||
|
|||
Check out the README.md in the `./docker` directory |
|||
|
@ -1,88 +0,0 @@ |
|||
pragma solidity ^0.4.17; |
|||
|
|||
contract Forum { |
|||
|
|||
//----------------------------------------USER---------------------------------------- |
|||
struct User { |
|||
string userName; // TODO: set an upper bound instead of arbitrary string |
|||
// TODO: orbitDBAddress; |
|||
uint[] topicIDs; // IDs of the topics the user created |
|||
uint[] postIDs; // IDs of the posts the user created |
|||
} |
|||
|
|||
mapping (address => User) users; |
|||
mapping (string => address) userAddresses; |
|||
|
|||
function signUp(string userName) public returns (bool) { // Also allows user to update his name - TODO: his previous name will appear as taken |
|||
require(!isUserNameTaken(userName)); |
|||
users[msg.sender] = User(userName, new uint[](0), new uint[](0)); |
|||
userAddresses[userName] = msg.sender; |
|||
return true; |
|||
} |
|||
|
|||
function login() public view returns (string) { |
|||
require (hasUserSignedUp(msg.sender)); |
|||
return users[msg.sender].userName; |
|||
} |
|||
|
|||
function getUsername(address userAddress) public view returns (string) { |
|||
return users[userAddress].userName; |
|||
} |
|||
|
|||
function getUserAddress(string userName) public view returns (address) { |
|||
return userAddresses[userName]; |
|||
} |
|||
|
|||
function hasUserSignedUp(address userAddress) public view returns (bool) { |
|||
if (bytes(getUsername(userAddress)).length!=0) |
|||
return true; |
|||
return false; |
|||
} |
|||
|
|||
function isUserNameTaken(string userName) public view returns (bool) { |
|||
if (getUserAddress(userName)!=0) |
|||
return true; |
|||
return false; |
|||
} |
|||
|
|||
//----------------------------------------TOPIC---------------------------------------- |
|||
struct Topic { |
|||
uint topicID; |
|||
address author; |
|||
uint timestamp; |
|||
uint[] postIDs; |
|||
} |
|||
|
|||
struct Post { |
|||
uint postID; |
|||
address author; |
|||
uint timestamp; |
|||
} |
|||
|
|||
uint numTopics; // Total number of topics |
|||
uint numPosts; // Total number of posts |
|||
|
|||
mapping (uint => Topic) topics; |
|||
mapping (uint => Post) posts; |
|||
|
|||
function createTopic() public returns (uint topicID) { |
|||
require(hasUserSignedUp(msg.sender)); // Only registered users can create topics |
|||
topicID = numTopics++; |
|||
topics[topicID] = Topic(topicID, msg.sender, block.timestamp, new uint[](0)); |
|||
users[msg.sender].topicIDs.push(topicID); |
|||
} |
|||
|
|||
function createPost(uint topicID) public returns (uint postID) { |
|||
require(hasUserSignedUp(msg.sender)); // Only registered users can create posts |
|||
require(topicID<numTopics); // Only allow posting to a topic that exists |
|||
postID = numPosts++; |
|||
posts[postID] = Post(postID, msg.sender, block.timestamp); |
|||
topics[topicID].postIDs.push(postID); |
|||
users[msg.sender].postIDs.push(postID); |
|||
} |
|||
|
|||
function getTopicPosts (uint topicID) public view returns (uint[]) { |
|||
require(topicID<numTopics); // Topic should exist |
|||
return topics[topicID].postIDs; |
|||
} |
|||
} |
@ -1,23 +0,0 @@ |
|||
pragma solidity ^0.4.19; |
|||
|
|||
contract Migrations { |
|||
address public owner; |
|||
uint public last_completed_migration; |
|||
|
|||
modifier restricted() { |
|||
if (msg.sender == owner) _; |
|||
} |
|||
|
|||
function Migrations() public { |
|||
owner = msg.sender; |
|||
} |
|||
|
|||
function setCompleted(uint completed) public restricted { |
|||
last_completed_migration = completed; |
|||
} |
|||
|
|||
function upgrade(address new_address) public restricted { |
|||
Migrations upgraded = Migrations(new_address); |
|||
upgraded.setCompleted(last_completed_migration); |
|||
} |
|||
} |
@ -0,0 +1,95 @@ |
|||
.EXPORT_ALL_VARIABLES: |
|||
PACKAGES := $(abspath ${CURDIR}/../packages) |
|||
REPORTS := $(abspath ${CURDIR}/reports) |
|||
GANACHE_VOLUMES := $(abspath ${CURDIR}/ganache/volumes) |
|||
CONTRACTS_PROVIDER_VOLUMES := $(abspath ${CURDIR}/concordia-contracts-provider/volumes) |
|||
PINNER_VOLUMES := $(abspath ${CURDIR}/concordia-pinner/volumes) |
|||
DOCKER_BUILDKIT = 1 |
|||
|
|||
run: compose-run build-contracts-provider run-contracts-provider build-contracts-migrate run-contracts-migrate build-pinner run-pinner build-app run-app |
|||
@echo "Concordia is up and running, head over to http://localhost:7777." |
|||
run-staging: compose-run build-contracts-provider run-contracts-provider-staging build-contracts-migrate run-contracts-migrate build-pinner run-pinner-staging build-app-staging run-app-staging |
|||
@echo "Concordia is up and running, head over to http://localhost:7000." |
|||
|
|||
prepare-network: |
|||
@docker network create --driver bridge concordia_concordia_network || true |
|||
|
|||
# Targets for building/running/stopping the blockchain and rendezvous server (using the docker-compose file)
|
|||
compose-build: |
|||
@docker-compose -f ./docker-compose.yml -p concordia build |
|||
compose-run: |
|||
@docker-compose -f ./docker-compose.yml -p concordia up -d |
|||
compose-stop: |
|||
@docker-compose -f ./docker-compose.yml -p concordia down |
|||
compose-stop-clean-data: |
|||
@docker-compose -f ./docker-compose.yml -p concordia down -v |
|||
|
|||
# Ganache targets
|
|||
build-ganache: |
|||
@docker build ../ -f ./ganache/Dockerfile -t ecentrics/concordia-ganache |
|||
run-ganache: |
|||
@docker run -d -v ${GANACHE_VOLUMES}/ganache_keys:/mnt/concordia/ganache_keys -p 8545:8545 --env-file=./env/ganache.env --name concordia-ganache --net=concordia_concordia_network ecentrics/concordia-ganache:latest |
|||
run-ganache-test: |
|||
@docker run --rm -d -p 8546:8546 --env-file=./env/ganache.test.env --name concordia-ganache-test --net=concordia_concordia_network ecentrics/concordia-ganache:latest |
|||
|
|||
# Rendezvous targets
|
|||
run-rendezvous: |
|||
@docker run -d -p 9090:9090 --name concordia-rendezvous --net=concordia_concordia_network libp2p/js-libp2p-webrtc-star:version-0.21.1 |
|||
|
|||
# Contracts targets
|
|||
build-contracts: |
|||
@docker build ../ -f ./concordia-contracts/Dockerfile --target compile -t ecentrics/concordia-contracts --build-arg TZ=Europe/Athens |
|||
build-contracts-migrate: |
|||
@docker build ../ -f ./concordia-contracts/Dockerfile -t ecentrics/concordia-contracts-migrate --build-arg TZ=Europe/Athens |
|||
build-contracts-tests: |
|||
@docker build ../ -f ./concordia-contracts/Dockerfile --target test -t ecentrics/concordia-contracts-tests --build-arg TZ=Europe/Athens |
|||
run-contracts-tests: |
|||
@docker run --rm -v ${REPORTS}/contracts/:/mnt/concordia/test-reports/ --env-file=./env/contracts-test.env --net=concordia_concordia_network ecentrics/concordia-contracts-tests:latest |
|||
run-contracts-tests-host-chain: |
|||
@docker run --rm -v ${REPORTS}/contracts/:/mnt/concordia/test-reports/ --env-file=./env/contracts-test.env --net=host ecentrics/concordia-contracts-tests:latest |
|||
run-contracts-migrate: |
|||
@docker run --rm -v ${PACKAGES}/concordia-contracts/build/:/usr/src/concordia/packages/concordia-contracts/build/ --env-file=./env/contracts.env --net=concordia_concordia_network ecentrics/concordia-contracts-migrate:latest |
|||
run-contracts-migrate-host-chain: |
|||
@docker run --rm -v ${PACKAGES}/concordia-contracts/build/:/usr/src/concordia/packages/concordia-contracts/build/ --env-file=./env/contracts.env --net=host ecentrics/concordia-contracts-migrate:latest |
|||
get-contracts: |
|||
@docker run --rm -v ${PACKAGES}/concordia-contracts/build/:/mnt/concordia/build --entrypoint=sh ecentrics/concordia-contracts:latest -c 'cp /usr/src/concordia/packages/concordia-contracts/build/* /mnt/concordia/build' |
|||
|
|||
# App targets
|
|||
build-app: |
|||
@docker build ../ -f ./concordia-app/Dockerfile -t ecentrics/concordia-app --build-arg TZ=Europe/Athens |
|||
build-app-staging: |
|||
@docker build ../ -f ./concordia-app/Dockerfile --target staging -t ecentrics/concordia-app-staging --build-arg TZ=Europe/Athens |
|||
build-app-tests: |
|||
@docker build ../ -f ./concordia-app/Dockerfile --target test -t ecentrics/concordia-app-tests --build-arg TZ=Europe/Athens |
|||
run-app-tests: |
|||
@docker run --rm -v ${REPORTS}/app/:/mnt/concordia/test-reports/ --env-file=./env/concordia.env ecentrics/concordia-app-tests:latest |
|||
run-app: |
|||
@docker run -d --env-file=./env/concordia.env -p 7777:80 --name concordia-app ecentrics/concordia-app:latest |
|||
run-app-staging: |
|||
@docker run -itd --env-file=./env/concordia.env -p 7000:3000 --name concordia-app-staging ecentrics/concordia-app-staging:latest |
|||
run-app-host-chain: |
|||
@docker run -d --env-file=./env/concordia.env --name concordia-app --net=host ecentrics/concordia-app:latest |
|||
|
|||
# Contracts provider targets
|
|||
build-contracts-provider: |
|||
@docker build ../ -f ./concordia-contracts-provider/Dockerfile -t ecentrics/concordia-contracts-provider --build-arg TZ=Europe/Athens |
|||
run-contracts-provider-staging: |
|||
@docker run -d -v ${CONTRACTS_PROVIDER_VOLUMES}:/mnt/concordia --env-file=./env/contracts-provider.env -p 8400:8400 --name concordia-contracts-provider --net=concordia_concordia_network ecentrics/concordia-contracts-provider:latest |
|||
run-contracts-provider: |
|||
@docker run -d -v ${CONTRACTS_PROVIDER_VOLUMES}:/mnt/concordia --env-file=./env/contracts-provider.env -e NODE_ENV=production -p 8400:8400 --name concordia-contracts-provider --net=concordia_concordia_network ecentrics/concordia-contracts-provider:latest |
|||
|
|||
# Pinner targets
|
|||
build-pinner: |
|||
@docker build ../ -f ./concordia-pinner/Dockerfile -t ecentrics/concordia-pinner --build-arg TZ=Europe/Athens |
|||
run-pinner-staging: |
|||
@docker run -d -v ${PINNER_VOLUMES}:/mnt/concordia --env-file=./env/pinner.env -p 4444:4444 --name concordia-pinner --net=concordia_concordia_network ecentrics/concordia-pinner:latest |
|||
run-pinner: |
|||
@docker run -d -v ${PINNER_VOLUMES}:/mnt/concordia --env-file=./env/pinner.env -e NODE_ENV=production -p 4444:4444 --name concordia-pinner --net=concordia_concordia_network ecentrics/concordia-pinner:latest |
|||
run-pinner-staging-host: |
|||
@docker run -d -v ${PINNER_VOLUMES}:/mnt/concordia --env-file=./env/pinner.env --net=host --name concordia-pinner ecentrics/concordia-pinner:latest |
|||
run-pinner-host: |
|||
@docker run -d -v ${PINNER_VOLUMES}:/mnt/concordia --env-file=./env/pinner.env -e NODE_ENV=production --net=host --name concordia-pinner ecentrics/concordia-pinner:latest |
|||
|
|||
# Other
|
|||
clean-images: |
|||
@docker rmi `docker images -q -f "dangling=true"` |
@ -0,0 +1,262 @@ |
|||
# Concordia Dockerized |
|||
|
|||
This document gives information about the provided docker images, their configuration and supported deployment |
|||
strategies. |
|||
|
|||
TLDR: head down to [Putting it all together/Scripts](#piat-mkfile-targets) for a quick setup. |
|||
|
|||
## Services |
|||
|
|||
Concordia requires at the minimum two services to work, a blockchain and a rendezvous server. |
|||
|
|||
Additionally, the Concordia application code must be provided to the user. Currently, the only way of distributing the |
|||
application code is via a webserver as a web application. |
|||
|
|||
### Ganache |
|||
|
|||
Ganache is a personal blockchain software used during development. It is a very convenient way of developing and testing |
|||
dApps. More information can be found in the project's [website](https://www.trufflesuite.com/ganache). |
|||
|
|||
Note that any other Ethereum compliant blockchain can be used. |
|||
|
|||
### Rendezvous |
|||
|
|||
Concordia uses a distributed database to store forum data. A rendezvous server is needed in order for users to discover |
|||
peers in the network and get access to the data. |
|||
|
|||
### Contracts Provider |
|||
|
|||
Contracts provider is a **very** simple resource provider server that handles saving the contract artifacts produced |
|||
during contracts migration and serving them to the users (and pinner). |
|||
|
|||
### Pinner |
|||
|
|||
Pinner is a bot node of the network that pins all IPFS content, so the forum data may be available even if the users |
|||
that create it are not. |
|||
|
|||
### Application |
|||
|
|||
The Concordia application is a React app that handles interactions with the contracts and the distributed database used. |
|||
|
|||
## Docker images |
|||
|
|||
This repository provides docker images to easily setup (and destroy) instances of all required services Concordia. |
|||
Furthermore, we provide an image that builds the contracts and handles their migration to the blockchain in use. |
|||
|
|||
### Ganache |
|||
|
|||
The Dockerfile is provided in the path `./ganache`. The image makes use of the environment variables described |
|||
below. |
|||
|
|||
| Environment variable | Default value | Usage | |
|||
| --- | --- | --- | |
|||
| ACCOUNTS_NUMBER | `10` | Set the number of accounts generated | |
|||
| ACCOUNTS_ETHER | `100` | Set the amount of ETH assigned to each account | |
|||
| MNEMONIC | `NaN` | The mnemonic phrase sued as a seed for deterministic account generation | |
|||
| HOST | `0.0.0.0` | The hostname to listen on | |
|||
| PORT | `8545` | The port to listen on | |
|||
| NETWORK_ID | `5778` | The network id used to identify ganache | |
|||
|
|||
Note that the Ganache instance running inside the container will save the generated blockchain keys in the path |
|||
`/mnt/concordia/ganache_keys/keys.json`. If you need to access the keys (e.g. for getting a private key and importing in |
|||
Metamask) you can mount a volume to this path to have easier access. |
|||
|
|||
Also, the database used by Ganache for storing blockchain information is placed in the path |
|||
`/mnt/concordia/ganache_db/`. You can maintain the blockchain state between runs by mounting a volume to the database |
|||
path. To do that, add the docker flag `-v host/absolute/path/to/ganache_db:/mnt/concordia/ganache_db`. |
|||
|
|||
### Rendezvous |
|||
|
|||
The rendezvous server used here is `js-libp2p-webrtc-star`. The server listens on port 9090. More information can be |
|||
found on the github page of the project [here](https://github.com/libp2p/js-libp2p-webrtc-star). |
|||
|
|||
### Contracts provider |
|||
|
|||
A Dockerfile for the contracts provider service is provided in the path `./concordia-contracts-provider`. The Dockerfile |
|||
contains only one stage, that is the runtime. |
|||
|
|||
The image makes use of the environment variables described below. |
|||
|
|||
| Environment variable | Default value | Usage | |
|||
| --- | --- | --- | |
|||
| CONTRACTS_PROVIDER_PORT | `8400` | Set the port of the contracts provider application | |
|||
| UPLOAD_CONTRACTS_DIRECTORY | `concordia/packages/concordia-contracts-provider/contracts-uploads` | Set the directory where the uploaded contracts are saved | |
|||
| LOGS_PATH | `concordia/packages/concordia-contracts-provider/logs` | Set the directory where the application logs are saved | |
|||
| CORS_ALLOWED_ORIGINS | `http://127.0.0.1:7000`, `http://localhost:7000`, `https://127.0.0.1:7000`, `https://localhost:7000`, `http://127.0.0.1:4444`, `http://localhost:4444`, `https://127.0.0.1:4444`, `https://localhost:4444` | Set the list of addresses allowed by CORS* | |
|||
|
|||
### Contracts |
|||
|
|||
This is a provision system that compiles and deploys the contracts to any Ethereum blockchain. It also uploads the |
|||
deployed contract artifacts to the contracts-provider service. |
|||
|
|||
**Attention**: the contracts-provider instance targeted by the environment variables MUST be up and running before |
|||
attempting to migrate the contracts. |
|||
|
|||
A Dockerfile is provided in the path `./concordia-contracts` that will build the contracts used by Concordia during |
|||
image build and handle their deployment to any Ethereum network defined using env-vars upon container run. Dockerfile |
|||
contains three useful stages, described in the table below. |
|||
|
|||
| Stage name | Entrypoint | Usage | |
|||
| --- | --- | --- | |
|||
| compile | Exits immediately | Compiles the contracts | |
|||
| test | Runs contract tests | Compiles contracts and runs tests using blockchain defined by env vars | |
|||
| runtime | Migrates contracts | Compiles contracts and migrates to the blockchain defined by env vars. Does **not** run tests | |
|||
|
|||
The image makes use of the environment variables described below. |
|||
|
|||
| Environment variable | Default value | Usage | |
|||
| --- | --- | --- | |
|||
| MIGRATE_NETWORK | `develop` | Set the network where the contracts will be deployed/tested (set this to "env" unless you know what you're doing) | |
|||
| WEB3_HOST | `127.0.0.1` | Set the hostname of the blockchain network that will be used for deployment (requires network to be "env") | |
|||
| WEB3_PORT | `8545` | `NaN` | Set the port of the blockchain network that will be used for deployment (requires network to be "env") | |
|||
| CONTRACTS_PROVIDER_HOST | `http://127.0.0.1` | Set the hostname of the contracts provider | |
|||
| CONTRACTS_PROVIDER_PORT | `8400` | Set the port of the contracts provider | |
|||
| CONTRACTS_VERSION_TAG | `NaN` | Set the tag of the contracts uploaded to provided | |
|||
|
|||
You can find the contract artifacts in the directory `/usr/src/concordia/packages/concordia-contracts/build/` inside |
|||
the image. |
|||
|
|||
**Attention**: make sure the targeted blockchain is up and running before trying to migrate the contracts. |
|||
|
|||
### Pinner |
|||
|
|||
A Dockerfile for the pinner service is provided in the path `./concordia-pinner`. The Dockerfile contains only one |
|||
stage, that is the runtime. |
|||
|
|||
The image makes use of the environment variables described below. |
|||
|
|||
| Environment variable | Default value | Usage | |
|||
| --- | --- | --- | |
|||
| PINNER_API_HOST | `127.0.0.1` | Set the hostname of the pinner application | |
|||
| PINNER_API_PORT | `4444` | Set the port of the pinner application | |
|||
| USE_EXTERNAL_CONTRACTS_PROVIDER | `false` | Enable/Disable use of external contracts provider | |
|||
| IPFS_DIRECTORY | `concordia/packages/concordia-pinner/ipfs` | Set the directory where the ipfs blocks are saved | |
|||
| ORBIT_DIRECTORY | `concordia/packages/concordia-pinner/orbitdb` | Set the directory where the orbitdb data are saved | |
|||
| LOGS_PATH | `concordia/packages/concordia-pinner/logs` | Set the directory where the application logs are saved | |
|||
| WEB3_HOST | `127.0.0.1` | Set the hostname of the blockchain | |
|||
| WEB3_PORT | `8545` | Set the port of the blockchain | |
|||
| RENDEZVOUS_HOST | `/ip4/127.0.0.1` | Set the hostname of the rendezvous server | |
|||
| RENDEZVOUS_PORT | `9090` | Set the port of the rendezvous server | |
|||
| CONTRACTS_PROVIDER_HOST | `http://127.0.0.1` | Set the hostname of the contracts provider service | |
|||
| CONTRACTS_PROVIDER_PORT | `8400` | Set the port of the contracts provider service | |
|||
| CONTRACTS_VERSION_HASH | `latest` | Set the contracts tag that will be pulled | |
|||
|
|||
### Application |
|||
|
|||
The Dockerfile provided in the path `./concordia-application` either builds the application for production and serves |
|||
the resulting build using an nginx server or simply runs the node development server. Dockerfile contains four stages, |
|||
described in the table below. |
|||
|
|||
| Stage name | Entrypoint | Usage | |
|||
| --- | --- | --- | |
|||
| test | Runs tests | Fetches npm packages and runs tests | |
|||
| staging | Serves application for development | Starts the node development server | |
|||
| runtime | Serves application for production | Builds for production and serves it through nginx | |
|||
|
|||
The image makes use of the environment variables described below. |
|||
|
|||
| Environment variable | Default value | Usage | |
|||
| --- | --- | --- | |
|||
| REACT_APP_CONCORDIA_HOST | `127.0.0.1` | Set the hostname of the concordia application | |
|||
| REACT_APP_CONCORDIA_PORT | `7000` | Set the port of the concordia application | |
|||
| REACT_APP_RENDEZVOUS_HOST | `/ip4/127.0.0.1` | Set the hostname of the rendezvous server | |
|||
| REACT_APP_RENDEZVOUS_PORT | `9090` | Set the port of the rendezvous server | |
|||
| REACT_APP_USE_EXTERNAL_CONTRACTS_PROVIDER | `false` | Enable/Disable use of external contracts provider | |
|||
| REACT_APP_CONTRACTS_PROVIDER_HOST | `http://127.0.0.1` | Set the hostname of the contracts provider service | |
|||
| REACT_APP_CONTRACTS_PROVIDER_PORT | `8400` | Set the port of the contracts provider service | |
|||
| REACT_APP_CONTRACTS_VERSION_HASH | `latest` | Set the contracts tag that will be pulled | |
|||
|
|||
**Attention**: this image will copy the contract artifacts from the directory `/packages/concordia-contracts/build` if |
|||
available. The image can then use these artifacts after build or pull new artifacts from a contracts provider. |
|||
|
|||
**Attention**: if you plan to use the imported contract artifacts instead of a provider, make sure the contracts have |
|||
been deployed before **building** this image. Also, make sure the rendezvous server is up and running. |
|||
|
|||
## Docker Compose |
|||
|
|||
A docker-compose file also is provided. The docker-compose handles the lifecycle of the Ganache and Rendezvous server |
|||
containers. |
|||
|
|||
## Putting it all together |
|||
|
|||
You can find some ready to use scripts for common scenarios like dev deploys and testing in the `./docker` directory. |
|||
These scripts are documented in the following chapters. |
|||
|
|||
### <a name="piat-mkfile-targets"></a> Makefile targets |
|||
|
|||
Concordia uses blockchain and other distributed technologies. There are a number of ways to set up a running instance of |
|||
this application. |
|||
|
|||
This chapter will guide you through simple setups for testing and production that depend on local blockchain (ganache) |
|||
instances which do not require real ETH to work or have any other charges. |
|||
|
|||
#### Testing the contracts |
|||
|
|||
Build the ganache image and spin up a blockchain for testing: |
|||
|
|||
```shell |
|||
make build-ganache run-ganache-test |
|||
``` |
|||
|
|||
Build the testing stage of the contracts image: |
|||
|
|||
```shell |
|||
make build-contracts-tests |
|||
``` |
|||
|
|||
Run the tests: |
|||
|
|||
```shell |
|||
make run-contracts-tests |
|||
``` |
|||
|
|||
The results should be printed in the terminal, but are also available in the directory `./reports/contracts`. |
|||
|
|||
#### Testing the application |
|||
|
|||
Build the testing stage of the application image: |
|||
|
|||
```shell |
|||
make build-app-tests |
|||
``` |
|||
|
|||
Run the test: |
|||
|
|||
```shell |
|||
make run-app-tests |
|||
``` |
|||
|
|||
The results should be printed in the terminal, but are also available in the directory `./reports/app`. |
|||
|
|||
#### Production |
|||
|
|||
Just run the target: |
|||
|
|||
```shell |
|||
make run |
|||
``` |
|||
|
|||
And you' re done! Head to [localhost:7777](localhost:7777) and voilà, a working Concordia instance appears! The |
|||
blockchain is exposed in the address `localhost:8545`. |
|||
|
|||
**Tip**: the accounts (private keys) generated by Ganache are available in the file `./volumes/ganache_keys/keys.json`. |
|||
|
|||
Note that the `make run` command might take several minutes to execute (depending on your system). What happens under |
|||
the hood is that: |
|||
|
|||
- the ganache image is built |
|||
- blockchain and rendezvous server containers are started |
|||
- migration stage of the contracts image is built |
|||
- the contracts are deployed to the blockchain: |
|||
- the application image is built and then deployed |
|||
|
|||
### Env Files |
|||
|
|||
Targets in the Makefile make use of env files located in the directory `./env`. Using this environment variables, you |
|||
can change various configuration options of the testing/production deploys. |
|||
|
|||
Makefile targets suffixed with `host-chain` will try to use a blockchain and rendezvous server running in the host |
|||
machine. They use the `--net=host` docker option. In order to work with these targets you need to create and use your |
|||
own env files (or modify the existing ones). The environment variables values will largely depend on how you choose to |
|||
run the services in your system (which ports you use etc.), so be sure to create them before running any `host-chain` |
|||
target. |
@ -0,0 +1,97 @@ |
|||
# -------------------------------------------------- |
|||
# Stage 1 (Init application build base) |
|||
# -------------------------------------------------- |
|||
FROM node:14-alpine@sha256:51e341881c2b77e52778921c685e711a186a71b8c6f62ff2edfc6b6950225a2f as base |
|||
LABEL maintainers.1="Apostolos Fanakis <apostolof@auth.gr>" |
|||
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com>" |
|||
LABEL gr.thmmy.ecentrics.concordia-image.name="app" |
|||
|
|||
WORKDIR /usr/src/concordia |
|||
|
|||
# Copy the root package.json and yarn.lock |
|||
COPY ./package.json . |
|||
COPY ./yarn.lock . |
|||
|
|||
# Copy package.json files from contracts, shared and app, then install modules |
|||
COPY ./packages/concordia-contracts/package.json ./packages/concordia-contracts/package.json |
|||
COPY ./packages/concordia-shared/package.json ./packages/concordia-shared/package.json |
|||
COPY ./packages/concordia-app/package.json ./packages/concordia-app/ |
|||
|
|||
# Install required packages |
|||
RUN apk update && apk --no-cache add g++ make python |
|||
|
|||
RUN yarn install --frozen-lockfile |
|||
|
|||
# Gets the rest of the source code |
|||
COPY ./packages/concordia-contracts ./packages/concordia-contracts |
|||
COPY ./packages/concordia-shared ./packages/concordia-shared |
|||
COPY ./packages/concordia-app ./packages/concordia-app |
|||
|
|||
# Fix timezome |
|||
ARG TZ |
|||
ENV TZ=${TZ} |
|||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone |
|||
|
|||
# -------------------------------------------------- |
|||
# Stage 2 (Test) |
|||
# -------------------------------------------------- |
|||
FROM base as test |
|||
|
|||
WORKDIR /opt/concordia-app |
|||
|
|||
COPY ./docker/concordia-app/test-app.sh . |
|||
|
|||
WORKDIR /usr/src/concordia/packages/concordia-app |
|||
|
|||
RUN ["chmod", "+x", "/opt/concordia-app/test-app.sh"] |
|||
|
|||
ENTRYPOINT ["/opt/concordia-app/test-app.sh"] |
|||
|
|||
# -------------------------------------------------- |
|||
# Stage 3 (Build) |
|||
# -------------------------------------------------- |
|||
FROM base as build |
|||
|
|||
WORKDIR /usr/src/concordia/packages/concordia-app |
|||
|
|||
RUN yarn build |
|||
|
|||
# -------------------------------------------------- |
|||
# Stage 4 (Staging runtime) |
|||
# -------------------------------------------------- |
|||
FROM base as staging |
|||
|
|||
WORKDIR /usr/src/concordia/packages/concordia-app |
|||
|
|||
ENTRYPOINT ["yarn", "start"] |
|||
|
|||
# -------------------------------------------------- |
|||
# Stage 4 (Production runtime) |
|||
# -------------------------------------------------- |
|||
FROM nginx:1.17-alpine@sha256:01747306a7247dbe928db991eab42e4002118bf636dd85b4ffea05dd907e5b66 as production |
|||
LABEL maintainers.1="Apostolos Fanakis <apostolof@auth.gr>" |
|||
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com" |
|||
LABEL gr.thmmy.ecentrics.concordia-image.name="app" |
|||
|
|||
# Fix timezome |
|||
ARG TZ |
|||
|
|||
RUN apk add -U tzdata \ |
|||
&& cp /usr/share/zoneinfo/$TZ /etc/localtime \ |
|||
&& echo $TZ > /etc/timezone \ |
|||
&& apk del tzdata \ |
|||
&& rm -rf /var/cache/apk/* |
|||
|
|||
COPY ./docker/concordia-app/create-environment.sh /opt/concordia/create-environment.sh |
|||
COPY ./docker/concordia-app/run.sh /opt/concordia/run.sh |
|||
|
|||
RUN ["chmod", "+x", "/opt/concordia/create-environment.sh"] |
|||
RUN ["chmod", "+x", "/opt/concordia/run.sh"] |
|||
|
|||
COPY ./docker/concordia-app/nginx.conf /etc/nginx/conf.d/default.conf |
|||
|
|||
WORKDIR "/var/www/concordia-app" |
|||
|
|||
COPY --chown=nginx:nginx --from=build /usr/src/concordia/packages/concordia-app/build . |
|||
|
|||
CMD ["/opt/concordia/run.sh"] |
@ -0,0 +1,14 @@ |
|||
#!/bin/sh |
|||
|
|||
echo "window.runtimeEnv = { \ |
|||
REACT_APP_CONCORDIA_HOST: \"${REACT_APP_CONCORDIA_HOST}\", \ |
|||
REACT_APP_CONCORDIA_PORT: \"${REACT_APP_CONCORDIA_PORT}\", \ |
|||
REACT_APP_WEB3_HOST: \"${REACT_APP_WEB3_HOST}\", \ |
|||
REACT_APP_WEB3_PORT: \"${REACT_APP_WEB3_PORT}\", \ |
|||
REACT_APP_RENDEZVOUS_HOST: \"${REACT_APP_RENDEZVOUS_HOST}\", \ |
|||
REACT_APP_RENDEZVOUS_PORT: \"${REACT_APP_RENDEZVOUS_PORT}\", \ |
|||
REACT_APP_USE_EXTERNAL_CONTRACTS_PROVIDER: \"${REACT_APP_USE_EXTERNAL_CONTRACTS_PROVIDER}\", \ |
|||
REACT_APP_CONTRACTS_PROVIDER_HOST: \"${REACT_APP_CONTRACTS_PROVIDER_HOST}\", \ |
|||
REACT_APP_CONTRACTS_PROVIDER_PORT: \"${REACT_APP_CONTRACTS_PROVIDER_PORT}\", \ |
|||
REACT_APP_CONTRACTS_VERSION_HASH: \"${REACT_APP_CONTRACTS_VERSION_HASH}\", \ |
|||
}" >/var/www/concordia-app/environment.js |
@ -0,0 +1,22 @@ |
|||
server { |
|||
listen 80; |
|||
server_name localhost; |
|||
|
|||
#charset koi8-r; |
|||
#access_log /var/log/nginx/host.access.log main; |
|||
|
|||
location / { |
|||
root /var/www/concordia-app; |
|||
index index.html index.htm; |
|||
try_files "$uri" "$uri/" /index.html; |
|||
} |
|||
|
|||
#error_page 404 /404.html; |
|||
|
|||
# redirect server error pages to the static page /50x.html |
|||
# |
|||
error_page 500 502 503 504 /50x.html; |
|||
location = /50x.html { |
|||
root /usr/share/nginx/html; |
|||
} |
|||
} |
@ -0,0 +1,3 @@ |
|||
sh /opt/concordia/create-environment.sh |
|||
|
|||
exec "$(which nginx)" -g "daemon off;" |
@ -0,0 +1,11 @@ |
|||
#!/bin/sh |
|||
|
|||
yarn lint -f html -o /mnt/concordia/test-reports/concordia-app-eslint.html --no-color |
|||
|
|||
if [ $? -eq 0 ]; then |
|||
echo "TESTS RAN SUCCESSFULLY!" |
|||
exit 0 |
|||
else |
|||
echo "SOME TESTS FAILED!" |
|||
exit 1 |
|||
fi |
@ -0,0 +1,36 @@ |
|||
# -------------------------------------------------- |
|||
# Stage 1 (Runtime) |
|||
# -------------------------------------------------- |
|||
FROM node:14-alpine@sha256:51e341881c2b77e52778921c685e711a186a71b8c6f62ff2edfc6b6950225a2f as runtime |
|||
LABEL maintainers.1="Apostolos Fanakis <apostolof@auth.gr>" |
|||
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com>" |
|||
LABEL gr.thmmy.ecentrics.concordia-image.name="contracts-provider" |
|||
|
|||
# Fix timezome (needed for timestamps on report files) |
|||
ARG TZ |
|||
|
|||
RUN apk add -U tzdata \ |
|||
&& cp /usr/share/zoneinfo/$TZ /etc/localtime \ |
|||
&& echo $TZ > /etc/timezone \ |
|||
&& apk del tzdata \ |
|||
&& rm -rf /var/cache/apk/* |
|||
|
|||
WORKDIR /usr/src/concordia |
|||
|
|||
# Copy the root package.json and yarn.lock |
|||
COPY ./package.json . |
|||
COPY ./yarn.lock . |
|||
|
|||
# Copy package.json files from shared and contracts provider, then install modules |
|||
COPY ./packages/concordia-shared/package.json ./packages/concordia-shared/ |
|||
COPY ./packages/concordia-contracts-provider/package.json ./packages/concordia-contracts-provider/ |
|||
|
|||
RUN yarn install --frozen-lockfile --network-timeout 100000 |
|||
|
|||
# Gets the rest of the source code |
|||
COPY ./packages/concordia-shared ./packages/concordia-shared |
|||
COPY ./packages/concordia-contracts-provider ./packages/concordia-contracts-provider |
|||
|
|||
WORKDIR /usr/src/concordia/packages/concordia-contracts-provider |
|||
|
|||
ENTRYPOINT ["yarn", "start"] |
@ -0,0 +1,70 @@ |
|||
# -------------------------------------------------- |
|||
# Stage 1 (Init contracts build base) |
|||
# -------------------------------------------------- |
|||
FROM node:14-alpine@sha256:51e341881c2b77e52778921c685e711a186a71b8c6f62ff2edfc6b6950225a2f as base |
|||
LABEL maintainers.1="Apostolos Fanakis <apostolof@auth.gr>" |
|||
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com>" |
|||
LABEL gr.thmmy.ecentrics.concordia-image.name="contracts" |
|||
|
|||
# Fix timezome (needed for timestamps on report files) |
|||
ARG TZ |
|||
|
|||
RUN apk add -U tzdata \ |
|||
&& cp /usr/share/zoneinfo/$TZ /etc/localtime \ |
|||
&& echo $TZ > /etc/timezone \ |
|||
&& apk del tzdata \ |
|||
&& rm -rf /var/cache/apk/* |
|||
|
|||
WORKDIR /usr/src/concordia |
|||
|
|||
# Copy the root package.json and yarn.lock |
|||
COPY ./package.json . |
|||
COPY ./yarn.lock . |
|||
|
|||
# Copy package.json files from shared and contracts, then install modules |
|||
COPY ./packages/concordia-shared/package.json ./packages/concordia-shared/ |
|||
COPY ./packages/concordia-contracts/package.json ./packages/concordia-contracts/ |
|||
|
|||
RUN yarn install --frozen-lockfile --network-timeout 100000 |
|||
|
|||
# Gets the rest of the source code |
|||
COPY ./packages/concordia-shared ./packages/concordia-shared |
|||
COPY ./packages/concordia-contracts ./packages/concordia-contracts |
|||
|
|||
# -------------------------------------------------- |
|||
# Stage 2 (Compile) |
|||
# -------------------------------------------------- |
|||
FROM base as compile |
|||
|
|||
WORKDIR /usr/src/concordia/packages/concordia-contracts |
|||
RUN yarn compile |
|||
|
|||
# -------------------------------------------------- |
|||
# Stage 3 (Test) |
|||
# -------------------------------------------------- |
|||
FROM compile as test |
|||
|
|||
WORKDIR /opt/concordia-contracts |
|||
|
|||
COPY ./docker/concordia-contracts/test-contracts.sh . |
|||
|
|||
WORKDIR /usr/src/concordia/packages/concordia-contracts |
|||
|
|||
RUN ["chmod", "+x", "/opt/concordia-contracts/test-contracts.sh"] |
|||
|
|||
ENTRYPOINT ["/opt/concordia-contracts/test-contracts.sh"] |
|||
|
|||
# -------------------------------------------------- |
|||
# Stage 4 (Runtime) |
|||
# -------------------------------------------------- |
|||
FROM compile as runtime |
|||
LABEL maintainers.1="Apostolos Fanakis <apostolof@auth.gr>" |
|||
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com>" |
|||
LABEL gr.thmmy.ecentrics.concordia-image.name="contracts" |
|||
|
|||
WORKDIR /opt/concordia-contracts |
|||
|
|||
COPY ./docker/concordia-contracts/migrate.sh . |
|||
RUN ["chmod", "+x", "/opt/concordia-contracts/migrate.sh"] |
|||
|
|||
ENTRYPOINT ["/opt/concordia-contracts/migrate.sh"] |
@ -0,0 +1,8 @@ |
|||
#!/bin/sh |
|||
|
|||
export CHAIN_HOST="$DEPLOY_CHAIN_HOST" |
|||
export CHAIN_PORT="$DEPLOY_CHAIN_PORT" |
|||
|
|||
cd /usr/src/concordia/packages/concordia-contracts && |
|||
yarn _migrate --network "${MIGRATE_NETWORK}" --reset && |
|||
yarn _upload ${CONTRACTS_VERSION_HASH} ${CONTRACTS_VERSION_TAG} |
@ -0,0 +1,22 @@ |
|||
#!/bin/sh |
|||
|
|||
export CHAIN_HOST="$TEST_CHAIN_HOST" |
|||
export CHAIN_PORT="$TEST_CHAIN_PORT" |
|||
|
|||
yarn _eslint -f html -o /mnt/concordia/test-reports/concordia-contracts-eslint.html --no-color |
|||
ESLINT_EXIT_STATUS=$? |
|||
|
|||
yarn _solhint >/mnt/concordia/test-reports/concordia-contracts-solhint.report |
|||
SOLHINT_EXIT_STATUS=$? |
|||
|
|||
yarn test --network env >/mnt/concordia/test-reports/concordia-contracts-truffle-tests.report |
|||
grep -qE failing /mnt/concordia/test-reports/concordia-contracts-truffle-tests.report |
|||
TRUFFLE_TEST_FAILING=$? |
|||
|
|||
if [ $ESLINT_EXIT_STATUS -eq 0 ] && [ "$SOLHINT_EXIT_STATUS" -eq 0 ] && [ "$TRUFFLE_TEST_FAILING" -eq 1 ]; then |
|||
echo "TESTS RAN SUCCESSFULLY!" |
|||
exit 0 |
|||
else |
|||
echo "SOME TESTS FAILED!" |
|||
exit 1 |
|||
fi |
@ -0,0 +1,34 @@ |
|||
# -------------------------------------------------- |
|||
# Stage 1 (Runtime) |
|||
# -------------------------------------------------- |
|||
FROM node:14-buster@sha256:32362e2ea89c62d77c86c8f26ad936dbbdc170cd6c06c4d7ff7a809012bb0c32 as runtime |
|||
LABEL maintainers.1="Apostolos Fanakis <apostolof@auth.gr>" |
|||
LABEL maintainers.2="Panagiotis Nikolaidis <ezerous@gmail.com>" |
|||
LABEL gr.thmmy.ecentrics.concordia-image.name="pinner" |
|||
|
|||
# Fix timezome (needed for timestamps on report files) |
|||
ARG TZ |
|||
ENV TZ=${TZ} |
|||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone |
|||
|
|||
WORKDIR /usr/src/concordia |
|||
|
|||
# Copy the root package.json and yarn.lock |
|||
COPY ./package.json . |
|||
COPY ./yarn.lock . |
|||
|
|||
# Copy package.json files from contracts, shared and pinner, then install modules |
|||
COPY ./packages/concordia-pinner/package.json ./packages/concordia-pinner/ |
|||
COPY ./packages/concordia-contracts/package.json ./packages/concordia-contracts/ |
|||
COPY ./packages/concordia-shared/package.json ./packages/concordia-shared/ |
|||
|
|||
RUN yarn install --frozen-lockfile --network-timeout 100000 |
|||
|
|||
# Gets the rest of the source code |
|||
COPY ./packages/concordia-shared ./packages/concordia-shared |
|||
COPY ./packages/concordia-contracts ./packages/concordia-contracts |
|||
COPY ./packages/concordia-pinner ./packages/concordia-pinner |
|||
|
|||
WORKDIR /usr/src/concordia/packages/concordia-pinner |
|||
|
|||
ENTRYPOINT ["yarn", "start"] |
@ -0,0 +1,33 @@ |
|||
version: '3.8' |
|||
|
|||
services: |
|||
ganache: |
|||
build: |
|||
context: ../ |
|||
dockerfile: ./docker/ganache/Dockerfile |
|||
image: ecentrics/concordia-ganache |
|||
container_name: concordia-ganache |
|||
env_file: |
|||
- env/ganache.env |
|||
expose: |
|||
- 8545 |
|||
ports: |
|||
- 8545:8545 |
|||
user: root |
|||
volumes: |
|||
- ./ganache/volumes/ganache_keys:/mnt/concordia/ganache_keys |
|||
networks: |
|||
concordia_network: |
|||
restart: always |
|||
|
|||
rendezvous: |
|||
image: libp2p/js-libp2p-webrtc-star:version-0.21.1 |
|||
container_name: concordia-rendezvous |
|||
networks: |
|||
concordia_network: |
|||
ports: |
|||
- 9090:9090 |
|||
restart: always |
|||
|
|||
networks: |
|||
concordia_network: |
@ -0,0 +1,5 @@ |
|||
# Variables needed in runtime (in browser) |
|||
REACT_APP_RENDEZVOUS_HOST=/ip4/127.0.0.1 |
|||
REACT_APP_RENDEZVOUS_PORT=9090 |
|||
REACT_APP_USE_EXTERNAL_CONTRACTS_PROVIDER=true |
|||
REACT_APP_CONTRACTS_VERSION_HASH=docker |
@ -0,0 +1,3 @@ |
|||
UPLOAD_CONTRACTS_DIRECTORY=/mnt/concordia/contracts/ |
|||
LOGS_PATH=/mnt/concordia/logs/ |
|||
#CORS_ALLOWED_ORIGINS="http://127.0.0.1:7000;http://localhost:7000;http://127.0.0.1:7777;http://localhost:7777;http://127.0.0.1:4444;127.0.0.1:4444" |
@ -0,0 +1,4 @@ |
|||
# Variables needed in runtime |
|||
MIGRATE_NETWORK=env |
|||
WEB3_HOST=concordia-ganache-test |
|||
WEB3_PORT=8546 |
@ -0,0 +1,8 @@ |
|||
# Variables needed in runtime |
|||
MIGRATE_NETWORK=env |
|||
WEB3_HOST=concordia-ganache |
|||
WEB3_PORT=8545 |
|||
|
|||
CONTRACTS_PROVIDER_HOST=http://concordia-contracts-provider |
|||
CONTRACTS_PROVIDER_PORT=8400 |
|||
CONTRACTS_VERSION_HASH=docker |
@ -0,0 +1,5 @@ |
|||
ACCOUNTS_NUMBER=10 |
|||
ACCOUNTS_ETHER=100 |
|||
HOST=0.0.0.0 |
|||
PORT=8545 |
|||
NETWORK_ID=5778 |
@ -0,0 +1,6 @@ |
|||
ACCOUNTS_NUMBER=5 |
|||
ACCOUNTS_ETHER=100 |
|||
MNEMONIC="myth like bonus scare over problem client lizard pioneer submit female collect" |
|||
HOST=0.0.0.0 |
|||
PORT=8546 |
|||
NETWORK_ID=5778 |
@ -0,0 +1,17 @@ |
|||
USE_EXTERNAL_CONTRACTS_PROVIDER=true |
|||
IPFS_DIRECTORY=/mnt/concordia/ipfs |
|||
ORBIT_DIRECTORY=/mnt/concordia/orbitdb |
|||
LOGS_PATH=/mnt/concordia/logs/ |
|||
|
|||
CONTRACTS_PROVIDER_HOST=http://concordia-contracts-provider |
|||
CONTRACTS_PROVIDER_PORT=8400 |
|||
CONTRACTS_VERSION_HASH=docker |
|||
|
|||
PINNER_API_HOST=127.0.0.1 |
|||
PINNER_API_PORT=4444 |
|||
|
|||
RENDEZVOUS_HOST=/docker/concordia-rendezvous |
|||
RENDEZVOUS_PORT=9090 |
|||
|
|||
WEB3_HOST=concordia-ganache |
|||
WEB3_PORT=8545 |
@ -0,0 +1,10 @@ |
|||
FROM trufflesuite/ganache-cli:v6.12.2@sha256:c062707f17f355872d703cde3de6a12fc45a027ed42857c72514171a5f466ab7 |
|||
|
|||
RUN mkdir -p /mnt/concordia/ganache_db /mnt/concordia/ganache_keys |
|||
|
|||
WORKDIR /opt/concordia-ganache |
|||
|
|||
COPY ./docker/ganache/start-blockchain.sh . |
|||
RUN ["chmod", "+x", "/opt/concordia-ganache/start-blockchain.sh"] |
|||
|
|||
ENTRYPOINT ["/opt/concordia-ganache/start-blockchain.sh"] |
@ -0,0 +1,37 @@ |
|||
#!/bin/sh |
|||
|
|||
N_ACCOUNTS="${ACCOUNTS_NUMBER:-10}" |
|||
ETHER="${ACCOUNTS_ETHER:-10}" |
|||
HOST="${HOST:-"0.0.0.0"}" |
|||
PORT="${PORT:-8545}" |
|||
ID="${NETWORK_ID:-5778}" |
|||
|
|||
if [ -z "${MNEMONIC}" ]; then |
|||
echo "Starting Ganache with non deterministic address generation" |
|||
node /app/ganache-core.docker.cli.js \ |
|||
--accounts "$N_ACCOUNTS" \ |
|||
--defaultBalanceEther "$ETHER" \ |
|||
--host "$HOST" \ |
|||
--port "$PORT" \ |
|||
--networkId "$ID" \ |
|||
--account_keys_path "/mnt/concordia/ganache_keys/keys.json" \ |
|||
--db "/mnt/concordia/ganache_db/" \ |
|||
--allowUnlimitedContractSize \ |
|||
--noVMErrorsOnRPCResponse \ |
|||
--verbose |
|||
else |
|||
echo "Starting Ganache with deterministic address generation" |
|||
node /app/ganache-core.docker.cli.js \ |
|||
--accounts "$N_ACCOUNTS" \ |
|||
--defaultBalanceEther "$ETHER" \ |
|||
--mnemonic "$MNEMONIC" \ |
|||
--host "$HOST" \ |
|||
--port "$PORT" \ |
|||
--networkId "$ID" \ |
|||
--account_keys_path "/mnt/concordia/ganache_keys/keys.json" \ |
|||
--db "/mnt/concordia/ganache_db/" \ |
|||
--allowUnlimitedContractSize \ |
|||
--noVMErrorsOnRPCResponse \ |
|||
--deterministic \ |
|||
--verbose |
|||
fi |
@ -0,0 +1,781 @@ |
|||
#!groovy |
|||
|
|||
def cleanSlateEnabled |
|||
def sanitizedBranchName |
|||
|
|||
// Package change state |
|||
def appPackageChanged |
|||
def contractsPackageChanged |
|||
def contractsProviderPackageChanged |
|||
def pinnerPackageChanged |
|||
def sharedPackageChanged |
|||
|
|||
// Package versions |
|||
def appPackageVersion |
|||
def contractsPackageVersion |
|||
def contractsProviderPackageVersion |
|||
def pinnerPackageVersion |
|||
def sharedPackageVersion |
|||
|
|||
// Docker images |
|||
def appImage |
|||
def contractsImage |
|||
def contractsProviderImage |
|||
def pinnerImage |
|||
|
|||
def freshGanacheStagingRunning = false |
|||
def freshGanacheProductionRunning = false |
|||
|
|||
def successResultGif = "https://media.giphy.com/media/o75ajIFH0QnQC3nCeD/giphy.gif" |
|||
def failResultGif = "https://media.giphy.com/media/ljtfkyTD3PIUZaKWRi/giphy.gif" |
|||
def abortResultGif = "https://media.giphy.com/media/IzXmRTmKd0if6/giphy.gif" |
|||
|
|||
pipeline { |
|||
agent any |
|||
|
|||
post { |
|||
failure { |
|||
updateGitlabCommitStatus name: 'build', state: 'failed' |
|||
|
|||
discordSend footer: "Visit Jenkins for more information", result: currentBuild.currentResult, link: env.BUILD_URL, description: """Jenkins Pipeline Build |
|||
Last commit included is [${GIT_COMMIT[0..7]}](https://gitlab.com/ecentrics/concordia/-/commit/$GIT_COMMIT) |
|||
Build status: ${currentBuild.currentResult} |
|||
""", image: failResultGif, thumbnail: "$CONCORDIA_LOGO_URL", title: JOB_NAME, webhookURL: "${DISCORD_WEBHOOK_URL}" |
|||
} |
|||
success { |
|||
updateGitlabCommitStatus name: 'build', state: 'success' |
|||
|
|||
discordSend footer: "Visit Jenkins for more information", result: currentBuild.currentResult, link: env.BUILD_URL, description: """Jenkins Pipeline Build |
|||
Last commit included is [${GIT_COMMIT[0..7]}](https://gitlab.com/ecentrics/concordia/-/commit/$GIT_COMMIT) |
|||
Build status: ${currentBuild.currentResult} |
|||
""", image: successResultGif, thumbnail: "$CONCORDIA_LOGO_URL", title: JOB_NAME, webhookURL: "${DISCORD_WEBHOOK_URL}" |
|||
} |
|||
aborted { |
|||
discordSend footer: "Visit Jenkins for more information", result: currentBuild.currentResult, link: env.BUILD_URL, description: """Jenkins Pipeline Build |
|||
Last commit included is [${GIT_COMMIT[0..7]}](https://gitlab.com/ecentrics/concordia/-/commit/$GIT_COMMIT) |
|||
Build status: ${currentBuild.currentResult} |
|||
""", image: abortResultGif, thumbnail: "$CONCORDIA_LOGO_URL", title: JOB_NAME, webhookURL: "${DISCORD_WEBHOOK_URL}" |
|||
} |
|||
always { |
|||
archiveArtifacts artifacts: "reports/${BUILD_NUMBER}/**/* , build/**/*, ganache/*", fingerprint: true, allowEmptyArchive: true |
|||
sleep 2 |
|||
sh 'docker images | grep -E "ecentrics/concordia.+tests" | tr -s \' \' | cut -d \' \' -f 3 | xargs --no-run-if-empty docker rmi -f || true' |
|||
sh "docker images | tr -s \' \' | grep -E \'ecentrics/concordia.+staging-b\' | sed -r \'s/(v.*-staging-b)([0-9]*)/\\1\\2 \\2/g\' | awk \'{ if (\$3 < ($BUILD_NUMBER - 2)) print \$1\":\"\$2}\' | xargs --no-run-if-empty docker rmi -f || true" |
|||
sh "docker images | tr -s \' \' | grep -E \'ecentrics/concordia.+production-b\' | sed -r \'s/(v.*-production-b)([0-9]*)/\\1\\2 \\2/g\' | awk \'{ if (\$3 < ($BUILD_NUMBER - 2)) print \$1\":\"\$2}\' | xargs --no-run-if-empty docker rmi -f || true" |
|||
sh 'docker system prune -f' |
|||
sh 'rm -rf reports' |
|||
sh 'rm -rf build' |
|||
} |
|||
} |
|||
options { |
|||
gitLabConnection('apella') |
|||
} |
|||
triggers { |
|||
gitlab(triggerOnPush: true, triggerOnMergeRequest: true, branchFilterType: 'All') |
|||
} |
|||
environment { |
|||
DOCKER_BUILDKIT='1' |
|||
DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/810180975580938290/HYYeK8Nqwt0h8Arx3qPpF-szjgLkPDTqbVVKLkzcmqY7ourTpKJCAc6IuCXHd_cxowuK" |
|||
CONCORDIA_LOGO_URL="https://i.postimg.cc/MGvgy9Lp/app-logo-circle.png" |
|||
} |
|||
|
|||
stages { |
|||
stage ('VERSION') { |
|||
steps { |
|||
script { |
|||
cleanSlateEnabled = sh (script: "git log -1 | grep -qE 'ci: force'", returnStatus: true) |
|||
sanitizedBranchName = sh(script: 'echo $GIT_BRANCH | sed -e "s:.*/::g"', returnStdout: true).trim() |
|||
|
|||
appPackageChanged = sh(script: 'bash ./jenkins/check_package_changed.sh app "$GIT_COMMIT" "$GIT_PREVIOUS_COMMIT"', returnStdout: true).trim() |
|||
contractsPackageChanged = sh(script: 'bash ./jenkins/check_package_changed.sh contracts "$GIT_COMMIT" "$GIT_PREVIOUS_COMMIT"', returnStdout: true).trim() |
|||
contractsProviderPackageChanged = sh(script: 'bash ./jenkins/check_package_changed.sh concordia-contracts-provider "$GIT_COMMIT" "$GIT_PREVIOUS_COMMIT"', returnStdout: true).trim() |
|||
pinnerPackageChanged = sh(script: 'bash ./jenkins/check_package_changed.sh pinner "$GIT_COMMIT" "$GIT_PREVIOUS_COMMIT"', returnStdout: true).trim() |
|||
sharedPackageChanged = sh(script: 'bash ./jenkins/check_package_changed.sh shared "$GIT_COMMIT" "$GIT_PREVIOUS_COMMIT"', returnStdout: true).trim() |
|||
|
|||
appPackageVersion = sh(script: 'grep "\\"version\\":" ./packages/concordia-app/package.json | head -1 | awk -F: \'{ print $2 }\' | sed \'s/[",]//g\' | tr -d \'[[:space:]]\'', returnStdout: true).trim() |
|||
contractsPackageVersion = sh(script: 'grep "\\"version\\":" ./packages/concordia-contracts/package.json | head -1 | awk -F: \'{ print $2 }\' | sed \'s/[",]//g\' | tr -d \'[[:space:]]\'', returnStdout: true).trim() |
|||
contractsProviderPackageVersion = sh(script: 'grep "\\"version\\":" ./packages/concordia-contracts-provider/package.json | head -1 | awk -F: \'{ print $2 }\' | sed \'s/[",]//g\' | tr -d \'[[:space:]]\'', returnStdout: true).trim() |
|||
pinnerPackageVersion = sh(script: 'grep "\\"version\\":" ./packages/concordia-pinner/package.json | head -1 | awk -F: \'{ print $2 }\' | sed \'s/[",]//g\' | tr -d \'[[:space:]]\'', returnStdout: true).trim() |
|||
sharedPackageVersion = sh(script: 'grep "\\"version\\":" ./packages/concordia-shared/package.json | head -1 | awk -F: \'{ print $2 }\' | sed \'s/[",]//g\' | tr -d \'[[:space:]]\'', returnStdout: true).trim() |
|||
|
|||
echo "Package: app, Version: ${appPackageVersion}, Changed: ${appPackageChanged}" |
|||
echo "Package: contracts, Version: ${contractsPackageVersion}, Changed: ${contractsPackageChanged}" |
|||
echo "Package: contracts-provider, Version: ${contractsProviderPackageVersion}, Changed: ${contractsProviderPackageChanged}" |
|||
echo "Package: pinner, Version: ${pinnerPackageVersion}, Changed: ${pinnerPackageChanged}" |
|||
echo "Package: shared, Version: ${sharedPackageVersion}, Changed: ${sharedPackageChanged}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('TEST') { |
|||
parallel { |
|||
stage('TEST CONTRACTS') { |
|||
steps { |
|||
script { |
|||
def ganacheTestPort = sh(script: "bash ./jenkins/hash_build_properties.sh ${BRANCH_NAME} ${BUILD_NUMBER} | xargs bash ./jenkins/map_to_thousand.sh", returnStdout: true).trim() |
|||
|
|||
def ganacheTestImage = docker.build( |
|||
"ecentrics/concordia-ganache", |
|||
"-f docker/ganache/Dockerfile \ |
|||
./" |
|||
) |
|||
|
|||
docker.build( |
|||
"ecentrics/concordia-contracts-tests:${sanitizedBranchName}-v${contractsPackageVersion}-b${BUILD_NUMBER}-tests", |
|||
"-f docker/concordia-contracts/Dockerfile \ |
|||
./ \ |
|||
--target test \ |
|||
--build-arg TZ=Europe/Athens" |
|||
) |
|||
|
|||
sh 'docker network create --driver bridge concordia_ganache_test_network || true' |
|||
|
|||
ganacheTestImage.withRun("""-d -p 6${ganacheTestPort}:8546 \ |
|||
--env-file=./jenkins/env/ganache.test.jenkins.env \ |
|||
--name concordia-ganache-test-6${ganacheTestPort} \ |
|||
--net=concordia_ganache_test_network""") { concordiaGanacheTest -> |
|||
|
|||
try { |
|||
sh """docker run \ |
|||
--rm \ |
|||
-v ecentrics_janus_common:/mnt/concordia/test-reports/ \ |
|||
--env-file=./jenkins/env/contracts.test.jenkins.env \ |
|||
-e WEB3_HOST=concordia-ganache-test-6${ganacheTestPort} \ |
|||
-e WEB3_PORT=6${ganacheTestPort} \ |
|||
--net=concordia_ganache_test_network \ |
|||
ecentrics/concordia-contracts-tests:${sanitizedBranchName}-v${contractsPackageVersion}-b${BUILD_NUMBER}-tests""" |
|||
} catch (e) { |
|||
error('Some tests failed!') |
|||
error('Aborting the build.') |
|||
throw e |
|||
} finally { |
|||
sh 'mkdir -p ./reports/${BUILD_NUMBER}/contracts' |
|||
sh 'find /mnt/janus/common/ -name "concordia-contracts-*" -exec cp \'{}\' ./reports/${BUILD_NUMBER}/contracts/ \\;' |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('TEST APP') { |
|||
steps { |
|||
script { |
|||
docker.build( |
|||
"ecentrics/concordia-app:${sanitizedBranchName}-v${appPackageVersion}-b${BUILD_NUMBER}-tests", |
|||
"-f docker/concordia-app/Dockerfile \ |
|||
./ \ |
|||
--target test \ |
|||
--build-arg TZ=Europe/Athens" |
|||
) |
|||
|
|||
try { |
|||
sh """docker run \ |
|||
--rm \ |
|||
-v ecentrics_janus_common:/mnt/concordia/test-reports/ \ |
|||
ecentrics/concordia-app:${sanitizedBranchName}-v${appPackageVersion}-b${BUILD_NUMBER}-tests""" |
|||
} catch (e) { |
|||
error('Some tests failed!') |
|||
error('Aborting the build.') |
|||
throw e |
|||
} finally { |
|||
sh 'mkdir -p ./reports/${BUILD_NUMBER}/app' |
|||
sh 'find /mnt/janus/common/ -name "concordia-app-*" -exec cp \'{}\' ./reports/${BUILD_NUMBER}/app/ \\;' |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('BUILD FOR PRODUCTION') { |
|||
when { |
|||
branch 'master' |
|||
} |
|||
parallel { |
|||
stage('BUILD CONTRACTS') { |
|||
when { |
|||
expression { |
|||
return "${contractsPackageChanged}" == '0' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
contractsImage = docker.build( |
|||
"ecentrics/concordia-contracts-migrate:v${contractsPackageVersion}", |
|||
"-f docker/concordia-contracts/Dockerfile \ |
|||
./ \ |
|||
-t ecentrics/concordia-contracts-migrate:latest \ |
|||
--build-arg TZ=Europe/Athens" |
|||
) |
|||
|
|||
contractsImage.run('--rm \ |
|||
-v ecentrics_janus_common:/mnt/concordia/build \ |
|||
--entrypoint=sh', |
|||
"-c 'mkdir -p /mnt/concordia/build/contract-artifacts && cp /usr/src/concordia/packages/concordia-contracts/build/* /mnt/concordia/build/contract-artifacts'") |
|||
|
|||
sh 'mkdir -p ./build/${BUILD_NUMBER}/contracts' |
|||
sh 'cp /mnt/janus/common/contract-artifacts/* ./build/${BUILD_NUMBER}/contracts' |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('BUILD APP') { |
|||
when { |
|||
expression { |
|||
return "${appPackageChanged}" == '0' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
appImage = docker.build( |
|||
"ecentrics/concordia-app:v${appPackageVersion}", |
|||
"-f docker/concordia-app/Dockerfile \ |
|||
./ \ |
|||
-t ecentrics/concordia-app:latest \ |
|||
--build-arg TZ=Europe/Athens" |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('BUILD CONTRACTS PROVIDER') { |
|||
when { |
|||
expression { |
|||
return "${contractsProviderPackageChanged}" == '0' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
contractsProviderImage = docker.build( |
|||
"ecentrics/concordia-contracts-provider:v${contractsProviderPackageVersion}", |
|||
"-f docker/concordia-contracts-provider/Dockerfile \ |
|||
./ \ |
|||
-t ecentrics/concordia-contracts-provider:latest \ |
|||
--build-arg TZ=Europe/Athens" |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('BUILD PINNER') { |
|||
when { |
|||
expression { |
|||
return "${pinnerPackageChanged}" == '0' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
pinnerImage = docker.build( |
|||
"ecentrics/concordia-pinner:v${pinnerPackageVersion}", |
|||
"-f docker/concordia-pinner/Dockerfile \ |
|||
./ \ |
|||
-t ecentrics/concordia-pinner:latest \ |
|||
--build-arg TZ=Europe/Athens" |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('BUILD FOR STAGING') { |
|||
when { |
|||
branch 'develop' |
|||
} |
|||
parallel { |
|||
stage('BUILD CONTRACTS') { |
|||
when { |
|||
expression { |
|||
return "${contractsPackageChanged}" == '0' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
contractsImage = docker.build( |
|||
"ecentrics/concordia-contracts-migrate:v${contractsPackageVersion}-staging-b${BUILD_NUMBER}", |
|||
"-f docker/concordia-contracts/Dockerfile \ |
|||
./ \ |
|||
-t ecentrics/concordia-contracts-migrate:staging-latest \ |
|||
--build-arg TZ=Europe/Athens" |
|||
) |
|||
|
|||
// Get contract artifacts |
|||
contractsImage.run('--rm \ |
|||
-v ecentrics_janus_common:/mnt/concordia/build \ |
|||
--entrypoint=sh', |
|||
"-c 'mkdir -p /mnt/concordia/build/contract-artifacts && cp /usr/src/concordia/packages/concordia-contracts/build/* /mnt/concordia/build/contract-artifacts'") |
|||
|
|||
sh 'mkdir -p ./build/${BUILD_NUMBER}/contracts' |
|||
sh 'cp /mnt/janus/common/contract-artifacts/* ./build/${BUILD_NUMBER}/contracts' |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('BUILD APP') { |
|||
when { |
|||
expression { |
|||
return "${appPackageChanged}" == '0' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
appImage = docker.build( |
|||
"ecentrics/concordia-app:v${appPackageVersion}-staging-b${BUILD_NUMBER}", |
|||
"-f docker/concordia-app/Dockerfile \ |
|||
./ \ |
|||
-t ecentrics/concordia-app:staging-latest \ |
|||
--target staging \ |
|||
--build-arg TZ=Europe/Athens" |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('BUILD CONTRACTS PROVIDER') { |
|||
when { |
|||
expression { |
|||
return "${contractsProviderPackageChanged}" == '0' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
contractsProviderImage = docker.build( |
|||
"ecentrics/concordia-contracts-provider:v${contractsProviderPackageVersion}-staging-b${BUILD_NUMBER}", |
|||
"-f docker/concordia-contracts-provider/Dockerfile \ |
|||
./ \ |
|||
-t ecentrics/concordia-contracts-provider:staging-latest \ |
|||
--build-arg TZ=Europe/Athens" |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('BUILD PINNER') { |
|||
when { |
|||
expression { |
|||
return "${pinnerPackageChanged}" == '0' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
pinnerImage = docker.build( |
|||
"ecentrics/concordia-pinner:v${pinnerPackageVersion}-staging-b${BUILD_NUMBER}", |
|||
"-f docker/concordia-pinner/Dockerfile \ |
|||
./ \ |
|||
-t ecentrics/concordia-pinner:staging-latest \ |
|||
--build-arg TZ=Europe/Athens" |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('PUBLISH') { |
|||
when { |
|||
branch 'master' |
|||
} |
|||
parallel { |
|||
stage('PUBLISH CONTRACTS') { |
|||
when { |
|||
expression { |
|||
return "${contractsPackageChanged}" == '0' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
docker.withRegistry('https://registry.hub.docker.com/', 'docker-hub-concordia') { |
|||
contractsImage.push() |
|||
contractsImage.push('latest') |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('PUBLISH APP') { |
|||
when { |
|||
expression { |
|||
return "${appPackageChanged}" == '0' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
docker.withRegistry('https://registry.hub.docker.com/', 'docker-hub-concordia') { |
|||
appImage.push() |
|||
appImage.push('latest') |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('PUBLISH CONTRACTS PROVIDER') { |
|||
when { |
|||
expression { |
|||
return "${contractsProviderPackageChanged}" == '0' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
docker.withRegistry('https://registry.hub.docker.com/', 'docker-hub-concordia') { |
|||
contractsProviderImage.push() |
|||
contractsProviderImage.push('latest') |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('PUBLISH PINNER') { |
|||
when { |
|||
expression { |
|||
return "${pinnerPackageChanged}" == '0' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
docker.withRegistry('https://registry.hub.docker.com/', 'docker-hub-concordia') { |
|||
pinnerImage.push() |
|||
pinnerImage.push('latest') |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('DEPLOY STAGING') { |
|||
when { |
|||
branch 'develop' |
|||
} |
|||
stages { |
|||
stage('STAGING DEPLOY PREPARATION') { |
|||
steps { |
|||
script { |
|||
sh 'docker network create --driver bridge ecentrics_concordia_staging_network || true' |
|||
|
|||
def rendezvousServerRunning = sh (script: 'docker ps -f name=concordia-rendezvous | \ |
|||
grep -qE concordia-rendezvous', returnStatus: true) |
|||
|
|||
if ("$rendezvousServerRunning" == '1' || "$cleanSlateEnabled" == '0') { |
|||
sh 'docker stop concordia-rendezvous || true \ |
|||
&& docker rm concordia-rendezvous || true' |
|||
|
|||
sh 'docker run \ |
|||
-d \ |
|||
--env-file=./jenkins/env/rendezvous.jenkins.env \ |
|||
-p 9090:9090 \ |
|||
--name concordia-rendezvous \ |
|||
--net=ecentrics_concordia_staging_network \ |
|||
libp2p/js-libp2p-webrtc-star:version-0.21.1' |
|||
} else { |
|||
sh 'docker network connect ecentrics_concordia_staging_network concordia-rendezvous || true' |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('DEPLOY CONTRACTS PROVIDER') { |
|||
when { |
|||
expression { |
|||
def contractsProviderStagingRunning = sh (script: 'docker ps -f name=concordia-contracts-provider-staging | \ |
|||
grep -qE concordia-contracts-provider-staging', returnStatus: true) |
|||
return "${contractsProviderPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' || "$contractsProviderStagingRunning" == '1' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
sh 'docker stop concordia-contracts-provider-staging || true \ |
|||
&& docker rm concordia-contracts-provider-staging || true' |
|||
|
|||
sh 'if [ "$cleanSlateEnabled" -eq "0" ]; then \ |
|||
docker volume rm concordia-contracts-provider-staging || true; \ |
|||
fi' |
|||
|
|||
sh (script: """docker run \ |
|||
-d \ |
|||
-v concordia-contracts-provider-staging:/mnt/concordia \ |
|||
--env-file=./jenkins/env/contracts.provider.staging.env \ |
|||
-p 8450:8450 \ |
|||
--name concordia-contracts-provider-staging \ |
|||
--net=ecentrics_concordia_staging_network \ |
|||
ecentrics/concordia-contracts-provider:staging-latest""") |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('RECREATE GANACHE') { |
|||
when { |
|||
expression { |
|||
def ganacheStagingRunning = sh (script: 'docker ps -f name=concordia-ganache-staging | \ |
|||
grep -qE concordia-ganache-staging', returnStatus: true) |
|||
return "$cleanSlateEnabled" == '0' || "$ganacheStagingRunning" == '1'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
sh 'docker stop concordia-ganache-staging || true \ |
|||
&& docker rm concordia-ganache-staging || true' |
|||
|
|||
sh 'docker volume rm concordia-ganache-staging || true' |
|||
|
|||
sh (script: 'docker run \ |
|||
-d \ |
|||
-v concordia-ganache-staging:/mnt/concordia \ |
|||
-p 8555:8555 \ |
|||
--env-file=./jenkins/env/ganache.staging.jenkins.env \ |
|||
--name concordia-ganache-staging \ |
|||
--net=ecentrics_concordia_staging_network \ |
|||
ecentrics/concordia-ganache:latest') |
|||
|
|||
// Ganache image might take a while to come alive |
|||
sleep 10 |
|||
|
|||
sh 'mkdir -p ./ganache/ && docker cp concordia-ganache-staging:/mnt/concordia/ganache_keys/keys.json ./ganache/' |
|||
freshGanacheStagingRunning = true |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('DEPLOY CONTRACTS') { |
|||
when { |
|||
expression { |
|||
return "${contractsPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' || "$freshGanacheStagingRunning" || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
sh """docker run \ |
|||
--rm \ |
|||
--env-file=./jenkins/env/contracts.staging.jenkins.env \ |
|||
-e CONTRACTS_VERSION_HASH=${contractsPackageVersion}-dev \ |
|||
--net=ecentrics_concordia_staging_network \ |
|||
ecentrics/concordia-contracts-migrate:staging-latest""" |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('DEPLOY PINNER') { |
|||
when { |
|||
expression { |
|||
def pinnerStagingRunning = sh (script: 'docker ps -f name=concordia-pinner-staging | \ |
|||
grep -qE concordia-pinner-staging', returnStatus: true) |
|||
return "${pinnerPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' || "${contractsPackageChanged}" == '0' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
sh 'docker stop concordia-pinner-staging || true \ |
|||
&& docker rm concordia-pinner-staging || true' |
|||
|
|||
sh 'if [ "$cleanSlateEnabled" -eq "0" ]; then \ |
|||
docker volume rm concordia-pinner-staging || true; \ |
|||
fi' |
|||
|
|||
sh """docker run \ |
|||
-d \ |
|||
-v concordia-pinner-staging:/mnt/concordia/ \ |
|||
-p 5555:5555 \ |
|||
--env-file=./jenkins/env/pinner.staging.jenkins.env \ |
|||
--name concordia-pinner-staging \ |
|||
--net=ecentrics_concordia_staging_network \ |
|||
ecentrics/concordia-pinner:staging-latest""" |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('DEPLOY APP') { |
|||
when { |
|||
expression { |
|||
def pinnerStagingRunning = sh (script: 'docker ps -f name=concordia-app-staging | \ |
|||
grep -qE concordia-app-staging', returnStatus: true) |
|||
return "${appPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
|
|||
steps { |
|||
script { |
|||
sh 'docker stop concordia-app-staging || true \ |
|||
&& docker rm concordia-app-staging || true' |
|||
|
|||
sh """docker run \ |
|||
-itd \ |
|||
-p 7000:3000 \ |
|||
--env-file=./jenkins/env/concordia.staging.jenkins.env \ |
|||
--name concordia-app-staging \ |
|||
--net=ecentrics_concordia_staging_network \ |
|||
ecentrics/concordia-app:staging-latest""" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('DEPLOY PRODUCTION') { |
|||
when { |
|||
branch 'master' |
|||
} |
|||
stages { |
|||
stage('PRODUCTION DEPLOY PREPARATION') { |
|||
steps { |
|||
script { |
|||
sh 'docker network create --driver bridge ecentrics_concordia_production_network || true' |
|||
|
|||
def rendezvousServerRunning = sh (script: 'docker ps -f name=concordia-rendezvous | \ |
|||
grep -qE concordia-rendezvous', returnStatus: true) |
|||
|
|||
if ("$rendezvousServerRunning" == '1' || "$cleanSlateEnabled" == '0') { |
|||
sh 'docker stop concordia-rendezvous || true \ |
|||
&& docker rm concordia-rendezvous || true' |
|||
|
|||
sh 'docker run \ |
|||
-d \ |
|||
--env-file=./jenkins/env/rendezvous.jenkins.env \ |
|||
-p 9090:9090 \ |
|||
--name concordia-rendezvous \ |
|||
--net=ecentrics_concordia_production_network \ |
|||
libp2p/js-libp2p-webrtc-star:version-0.21.1' |
|||
} else { |
|||
sh 'docker network connect ecentrics_concordia_production_network concordia-rendezvous || true' |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('DEPLOY CONTRACTS PROVIDER') { |
|||
when { |
|||
expression { |
|||
def contractsProviderProductionRunning = sh (script: 'docker ps -f name=concordia-contracts-provider-production | \ |
|||
grep -qE concordia-contracts-provider-production', returnStatus: true) |
|||
return "${contractsProviderPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' || "$contractsProviderProductionRunning" == '1' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
sh 'docker stop concordia-contracts-provider-production || true \ |
|||
&& docker rm concordia-contracts-provider-production || true' |
|||
|
|||
sh 'if [ "$cleanSlateEnabled" -eq "0" ]; then \ |
|||
docker volume rm concordia-contracts-provider-production || true; \ |
|||
fi' |
|||
|
|||
sh (script: """docker run \ |
|||
-d \ |
|||
-v concordia-contracts-provider-production:/mnt/concordia \ |
|||
--env-file=./jenkins/env/contracts.provider.production.env \ |
|||
-e NODE_ENV=production \ |
|||
-p 8400:8400 \ |
|||
--name concordia-contracts-provider-production \ |
|||
--net=ecentrics_concordia_production_network \ |
|||
ecentrics/concordia-contracts-provider:latest""") |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('RECREATE GANACHE') { |
|||
when { |
|||
expression { |
|||
def ganacheProductionRunning = sh (script: 'docker ps -f name=concordia-ganache-production | \ |
|||
grep -qE concordia-ganache-production', returnStatus: true) |
|||
return "$cleanSlateEnabled" == '0' || "$ganacheProductionRunning" == '1'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
sh 'docker stop concordia-ganache-production || true \ |
|||
&& docker rm concordia-ganache-production || true' |
|||
|
|||
sh 'docker volume rm concordia-ganache-production || true' |
|||
|
|||
sh (script: 'docker run \ |
|||
-d \ |
|||
-v concordia-ganache-production:/mnt/concordia \ |
|||
-p 8545:8545 \ |
|||
--env-file=./jenkins/env/ganache.production.jenkins.env \ |
|||
--name concordia-ganache-production \ |
|||
--net=ecentrics_concordia_production_network \ |
|||
ecentrics/concordia-ganache:latest') |
|||
|
|||
// Ganache image might take a while to come alive |
|||
sleep 10 |
|||
|
|||
sh 'mkdir -p ./ganache/ && docker cp concordia-ganache-production:/mnt/concordia/ganache_keys/keys.json ./ganache/' |
|||
freshGanacheProductionRunning = true |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('DEPLOY CONTRACTS') { |
|||
when { |
|||
expression { |
|||
return "${contractsPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' || "$freshGanacheProductionRunning" || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
sh """docker run \ |
|||
--rm \ |
|||
--env-file=./jenkins/env/contracts.production.jenkins.env \ |
|||
-e CONTRACTS_VERSION_HASH=${contractsPackageVersion} \ |
|||
--net=ecentrics_concordia_production_network \ |
|||
ecentrics/concordia-contracts-migrate:latest""" |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('DEPLOY PINNER') { |
|||
when { |
|||
expression { |
|||
def pinnerProductionRunning = sh (script: 'docker ps -f name=concordia-pinner-production | \ |
|||
grep -qE concordia-pinner-production', returnStatus: true) |
|||
return "${pinnerPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' || "${pinnerProductionRunning}" == '1' || "${contractsPackageChanged}" == '0' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
steps { |
|||
script { |
|||
sh 'docker stop concordia-pinner-production || true \ |
|||
&& docker rm concordia-pinner-production || true' |
|||
|
|||
sh 'if [ "$cleanSlateEnabled" -eq "0" ]; then \ |
|||
docker volume rm concordia-pinner-production || true; \ |
|||
fi' |
|||
|
|||
sh """docker run \ |
|||
-d \ |
|||
-v concordia-pinner-production:/mnt/concordia \ |
|||
-p 4444:4444 \ |
|||
-e NODE_ENV=production \ |
|||
--env-file=./jenkins/env/pinner.production.jenkins.env \ |
|||
--name concordia-pinner-production \ |
|||
--net=ecentrics_concordia_production_network \ |
|||
ecentrics/concordia-pinner:latest""" |
|||
} |
|||
} |
|||
} |
|||
|
|||
stage('DEPLOY APP') { |
|||
when { |
|||
expression { |
|||
def appProductionRunning = sh (script: 'docker ps -f name=concordia-app-production | \ |
|||
grep -qE concordia-app-production', returnStatus: true) |
|||
return "${appPackageChanged}" == '0' || "$cleanSlateEnabled" == '0' ||"${appProductionRunning}" == '1' || "${sharedPackageChanged}" == '0'; |
|||
} |
|||
} |
|||
|
|||
steps { |
|||
script { |
|||
sh 'docker stop concordia-app-production || true \ |
|||
&& docker rm concordia-app-production || true' |
|||
|
|||
sh """docker run \ |
|||
-d \ |
|||
-p 7777:80 \ |
|||
--env-file=./jenkins/env/concordia.production.jenkins.env \ |
|||
--name concordia-app-production \ |
|||
--net=ecentrics_concordia_production_network \ |
|||
ecentrics/concordia-app:latest""" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,36 @@ |
|||
#!/bin/bash |
|||
|
|||
# Based on this post: |
|||
# https://engineering.brigad.co/only-deploy-services-impacted-by-changes-in-a-mono-repository-18f54b8ac109 |
|||
|
|||
APP=$1 |
|||
# Jenkins should provide these in the environment by default |
|||
GIT_COMMIT=$2 |
|||
GIT_PREVIOUS_COMMIT=$3 |
|||
ROOT_FILES_AND_FOLDERS=${4:-"package.json" "yarn.lock" ".dockerignore" "docker" "jenkins"} |
|||
|
|||
function join_by() { |
|||
local IFS="$1" |
|||
shift |
|||
echo "$*" |
|||
} |
|||
|
|||
function package_changed() { |
|||
git diff --name-only "$COMMIT_RANGE" | grep -qE "^packages/concordia-$1/" && echo true || echo false |
|||
} |
|||
|
|||
if [ "$GIT_COMMIT" == "$GIT_PREVIOUS_COMMIT" ]; then |
|||
# Probably a manual re-run, set the range to just the last commit |
|||
COMMIT_RANGE="$GIT_COMMIT" |
|||
else |
|||
COMMIT_RANGE="$GIT_PREVIOUS_COMMIT...$GIT_COMMIT" |
|||
fi |
|||
|
|||
ROOT_FILES_AND_FOLDERS_ARRAY=($ROOT_FILES_AND_FOLDERS) |
|||
ROOT_FILES_AND_FOLDERS_JOINED=$(join_by "|" ${ROOT_FILES_AND_FOLDERS_ARRAY[*]}) |
|||
|
|||
ROOT_FILES_CHANGED=$(git diff --name-only "$COMMIT_RANGE" | grep -qE "^($ROOT_FILES_AND_FOLDERS_JOINED)" && echo true || echo false) |
|||
IS_FORCE_BUILD=$(git log --oneline "$COMMIT_RANGE" | grep -qE "ci: force" && echo true || echo false) |
|||
APP_FILES_CHANGED=$(package_changed ${APP}) |
|||
|
|||
($IS_FORCE_BUILD || $ROOT_FILES_CHANGED || $APP_FILES_CHANGED) && echo 0 || echo 1 |
@ -0,0 +1,16 @@ |
|||
VIRTUAL_HOST=concordia.ecentrics.net,www.concordia.ecentrics.net |
|||
VIRTUAL_PORT=7777 |
|||
LETSENCRYPT_HOST=concordia.ecentrics.net,www.concordia.ecentrics.net |
|||
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com |
|||
|
|||
# Variables needed in runtime (in browser) |
|||
REACT_APP_CONCORDIA_HOST=https://concordia.ecentrics.net |
|||
REACT_APP_CONCORDIA_PORT=443 |
|||
|
|||
REACT_APP_RENDEZVOUS_HOST=/dns4/rendezvous.ecentrics.net |
|||
REACT_APP_RENDEZVOUS_PORT=443 |
|||
|
|||
REACT_APP_USE_EXTERNAL_CONTRACTS_PROVIDER=true |
|||
REACT_APP_CONTRACTS_PROVIDER_HOST=https://contracts.concordia.ecentrics.net |
|||
REACT_APP_CONTRACTS_PROVIDER_PORT=443 |
|||
REACT_APP_CONTRACTS_VERSION_HASH=stable |
@ -0,0 +1,16 @@ |
|||
VIRTUAL_HOST=staging.concordia.ecentrics.net,www.staging.concordia.ecentrics.net |
|||
VIRTUAL_PORT=7000 |
|||
LETSENCRYPT_HOST=staging.concordia.ecentrics.net,www.staging.concordia.ecentrics.net |
|||
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com |
|||
|
|||
# Variables needed in runtime (in browser) |
|||
REACT_APP_CONCORDIA_HOST=https://staging.concordia.ecentrics.net |
|||
REACT_APP_CONCORDIA_PORT=443 |
|||
|
|||
REACT_APP_RENDEZVOUS_HOST=/dns4/rendezvous.ecentrics.net |
|||
REACT_APP_RENDEZVOUS_PORT=443 |
|||
|
|||
REACT_APP_USE_EXTERNAL_CONTRACTS_PROVIDER=true |
|||
REACT_APP_CONTRACTS_PROVIDER_HOST=https://staging.contracts.concordia.ecentrics.net |
|||
REACT_APP_CONTRACTS_PROVIDER_PORT=443 |
|||
REACT_APP_CONTRACTS_VERSION_HASH=latest |
@ -0,0 +1,8 @@ |
|||
# Variables needed in runtime |
|||
MIGRATE_NETWORK=env |
|||
WEB3_HOST=concordia-ganache-production |
|||
WEB3_PORT=8545 |
|||
|
|||
CONTRACTS_PROVIDER_HOST=https://contracts.concordia.ecentrics.net |
|||
CONTRACTS_PROVIDER_PORT=443 |
|||
CONTRACTS_VERSION_TAG=stable |
@ -0,0 +1,9 @@ |
|||
VIRTUAL_HOST=contracts.concordia.ecentrics.net,www.contracts.concordia.ecentrics.net |
|||
VIRTUAL_PORT=8400 |
|||
LETSENCRYPT_HOST=contracts.concordia.ecentrics.net,www.contracts.concordia.ecentrics.net |
|||
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com |
|||
|
|||
CONTRACTS_PROVIDER_PORT=8400 |
|||
UPLOAD_CONTRACTS_DIRECTORY=/mnt/concordia/contracts/ |
|||
LOGS_PATH=/mnt/concordia/logs/ |
|||
CORS_ALLOWED_ORIGINS="https://concordia.ecentrics.net:443;https://concordia.ecentrics.net;http://127.0.0.1:4444;127.0.0.1:4444" |
@ -0,0 +1,9 @@ |
|||
VIRTUAL_HOST=staging.contracts.concordia.ecentrics.net,www.staging.contracts.concordia.ecentrics.net |
|||
VIRTUAL_PORT=8450 |
|||
LETSENCRYPT_HOST=staging.contracts.concordia.ecentrics.net,www.staging.contracts.concordia.ecentrics.net |
|||
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com |
|||
|
|||
CONTRACTS_PROVIDER_PORT=8450 |
|||
UPLOAD_CONTRACTS_DIRECTORY=/mnt/concordia/contracts/ |
|||
LOGS_PATH=/mnt/concordia/logs/ |
|||
CORS_ALLOWED_ORIGINS="https://staging.concordia.ecentrics.net:443;https://staging.concordia.ecentrics.net;http://127.0.0.1:5555;127.0.0.1:5555;https://www.staging.concordia.ecentrics.net:443;https://www.staging.concordia.ecentrics.net;http://www.127.0.0.1:5555;www.127.0.0.1:5555" |
@ -0,0 +1,8 @@ |
|||
# Variables needed in runtime |
|||
MIGRATE_NETWORK=env |
|||
WEB3_HOST=concordia-ganache-staging |
|||
WEB3_PORT=8555 |
|||
|
|||
CONTRACTS_PROVIDER_HOST=https://staging.contracts.concordia.ecentrics.net |
|||
CONTRACTS_PROVIDER_PORT=443 |
|||
CONTRACTS_VERSION_TAG=latest |
@ -0,0 +1,4 @@ |
|||
# Variables needed in runtime |
|||
MIGRATE_NETWORK=env |
|||
WEB3_HOST=concordia-ganache-test |
|||
WEB3_PORT=8546 |
@ -0,0 +1,10 @@ |
|||
VIRTUAL_HOST=ganache.ecentrics.net,www.ganache.ecentrics.net |
|||
VIRTUAL_PORT=8545 |
|||
LETSENCRYPT_HOST=ganache.ecentrics.net,www.LETSENCRYPT_HOST |
|||
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com |
|||
|
|||
ACCOUNTS_NUMBER=1000 |
|||
ACCOUNTS_ETHER=100000 |
|||
HOST=0.0.0.0 |
|||
PORT=8545 |
|||
NETWORK_ID=5778 |
@ -0,0 +1,10 @@ |
|||
VIRTUAL_HOST=staging.ganache.ecentrics.net,www.staging.ganache.ecentrics.net |
|||
VIRTUAL_PORT=8555 |
|||
LETSENCRYPT_HOST=staging.ganache.ecentrics.net,www.staging.ganache.ecentrics.net |
|||
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com |
|||
|
|||
ACCOUNTS_NUMBER=100 |
|||
ACCOUNTS_ETHER=1000 |
|||
HOST=0.0.0.0 |
|||
PORT=8555 |
|||
NETWORK_ID=5778 |
@ -0,0 +1,6 @@ |
|||
ACCOUNTS_NUMBER=5 |
|||
ACCOUNTS_ETHER=1 |
|||
MNEMONIC="myth like bonus scare over problem client lizard pioneer submit female collect" |
|||
HOST=0.0.0.0 |
|||
PORT=8546 |
|||
NETWORK_ID=5778 |
@ -0,0 +1,22 @@ |
|||
VIRTUAL_HOST=pinner.concordia.ecentrics.net,www.pinner.concordia.ecentrics.net |
|||
VIRTUAL_PORT=4444 |
|||
LETSENCRYPT_HOST=pinner.concordia.ecentrics.net,www.pinner.concordia.ecentrics.net |
|||
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com |
|||
|
|||
USE_EXTERNAL_CONTRACTS_PROVIDER=true |
|||
ORBIT_DIRECTORY=/mnt/concordia/orbitdb |
|||
IPFS_DIRECTORY=/mnt/concordia/ipfs |
|||
LOGS_PATH=/mnt/concordia/logs/ |
|||
|
|||
CONTRACTS_PROVIDER_HOST=https://contracts.concordia.ecentrics.net |
|||
CONTRACTS_PROVIDER_PORT=443 |
|||
CONTRACTS_VERSION_HASH=stable |
|||
|
|||
PINNER_API_HOST=https://pinner.concordia.ecentrics.net |
|||
PINNER_API_PORT=443 |
|||
|
|||
RENDEZVOUS_HOST=/dns4/rendezvous.ecentrics.net |
|||
RENDEZVOUS_PORT=443 |
|||
|
|||
WEB3_HOST=ganache.ecentrics.net |
|||
WEB3_PORT=8545 |
@ -0,0 +1,22 @@ |
|||
VIRTUAL_HOST=staging.pinner.concordia.ecentrics.net,www.staging.pinner.concordia.ecentrics.net |
|||
VIRTUAL_PORT=5555 |
|||
LETSENCRYPT_HOST=staging.pinner.concordia.ecentrics.net,www.staging.pinner.concordia.ecentrics.net |
|||
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com |
|||
|
|||
USE_EXTERNAL_CONTRACTS_PROVIDER=true |
|||
ORBIT_DIRECTORY=/mnt/concordia/orbitdb |
|||
IPFS_DIRECTORY=/mnt/concordia/ipfs |
|||
LOGS_PATH=/mnt/concordia/logs/ |
|||
|
|||
CONTRACTS_PROVIDER_HOST=https://staging.contracts.concordia.ecentrics.net |
|||
CONTRACTS_PROVIDER_PORT=443 |
|||
CONTRACTS_VERSION_HASH=latest |
|||
|
|||
PINNER_API_HOST=https://staging.pinner.concordia.ecentrics.net |
|||
PINNER_API_PORT=443 |
|||
|
|||
RENDEZVOUS_HOST=/dns4/rendezvous.ecentrics.net |
|||
RENDEZVOUS_PORT=443 |
|||
|
|||
WEB3_HOST=staging.ganache.ecentrics.net |
|||
WEB3_PORT=8555 |
@ -0,0 +1,5 @@ |
|||
VIRTUAL_HOST=rendezvous.ecentrics.net,www.rendezvous.ecentrics.net |
|||
VIRTUAL_PORT=9090 |
|||
LETSENCRYPT_HOST=rendezvous.ecentrics.net,www.rendezvous.ecentrics.net |
|||
LETSENCRYPT_EMAIL=ecentricsgr@gmail.com |
|||
|
@ -0,0 +1,18 @@ |
|||
#!/bin/bash |
|||
|
|||
# Outputs to the stdout a deterministically generated integer in the range 0-4095. The integer is generated using the |
|||
# input strings and SHA1. |
|||
# Usage: hash_build_properties.sh <branch> <build_number> |
|||
# Inputs: |
|||
# - branch: the branch being build |
|||
# - build_number the incrementing number of the build |
|||
|
|||
BRANCH=$1 |
|||
BUILD_NUMBER=$2 |
|||
|
|||
STRING_TO_HASH="$BRANCH-$BUILD_NUMBER" |
|||
SHA1_SUM_HEX=$(sha1sum <<<"$STRING_TO_HASH") |
|||
SHA1_TRUNCATED_HEX=$(cut -c1-3 <<<"$SHA1_SUM_HEX") |
|||
HASHED_STRING=$((0x${SHA1_TRUNCATED_HEX})) |
|||
|
|||
echo "$HASHED_STRING" |
@ -0,0 +1,5 @@ |
|||
#!/bin/bash |
|||
|
|||
INTEGER_TO_MAP=$1 |
|||
|
|||
echo $(( INTEGER_TO_MAP * 999 / 4095 )) |
@ -1,5 +0,0 @@ |
|||
var Migrations = artifacts.require("./Migrations.sol"); |
|||
|
|||
module.exports = function(deployer) { |
|||
deployer.deploy(Migrations); |
|||
}; |
@ -1,5 +0,0 @@ |
|||
var Forum = artifacts.require("Forum"); |
|||
|
|||
module.exports = function(deployer) { |
|||
deployer.deploy(Forum); |
|||
}; |
@ -1,29 +1,13 @@ |
|||
{ |
|||
"name": "apella", |
|||
"version": "0.1.0", |
|||
"name": "concordia", |
|||
"private": true, |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "https://gitlab.com/Ezerous/Apella.git" |
|||
}, |
|||
"devDependencies": { |
|||
"truffle-contract": "^3.0.4" |
|||
}, |
|||
"dependencies": { |
|||
"react": "^16.3.0", |
|||
"react-dom": "^16.3.0", |
|||
"react-scripts": "1.1.1", |
|||
"react-redux": "^5.0.7", |
|||
"react-router": "3.2.1", |
|||
"react-router-redux": "^4.0.8", |
|||
"redux": "^3.7.2", |
|||
"redux-auth-wrapper": "1.1.0", |
|||
"redux-thunk": "^2.2.0" |
|||
}, |
|||
"scripts": { |
|||
"start": "react-scripts start", |
|||
"build": "react-scripts build", |
|||
"test": "react-scripts test --env=jsdom", |
|||
"eject": "react-scripts eject" |
|||
"workspaces": { |
|||
"packages": [ |
|||
"packages/*" |
|||
], |
|||
"nohoist": [ |
|||
"**/web3", |
|||
"**/web3/**" |
|||
] |
|||
} |
|||
} |
|||
|
@ -0,0 +1,11 @@ |
|||
# This is an example development configuration for the app |
|||
# To create your own configuration, copy this one and ommit the ".example" from the filename, then change the |
|||
# environment cariables to the prefered values. |
|||
|
|||
# Node dev-server host & port |
|||
HOST=localhost |
|||
PORT=7000 |
|||
|
|||
# Variables needed in runtime (in browser) |
|||
REACT_APP_RENDEZVOUS_HOST=127.0.0.1 |
|||
REACT_APP_RENDEZVOUS_PORT=9090 |
@ -0,0 +1,60 @@ |
|||
module.exports = { |
|||
'env': { |
|||
'browser': true, |
|||
'es6': true, |
|||
'jest': true |
|||
}, |
|||
'extends': [ |
|||
'plugin:react/recommended', |
|||
'airbnb' |
|||
], |
|||
'globals': { |
|||
'Atomics': 'readonly', |
|||
'SharedArrayBuffer': 'readonly' |
|||
}, |
|||
parser: 'babel-eslint', |
|||
'parserOptions': { |
|||
'ecmaFeatures': { |
|||
'jsx': true |
|||
}, |
|||
'ecmaVersion': 2018, |
|||
'sourceType': 'module' |
|||
}, |
|||
'plugins': [ |
|||
'react', |
|||
'react-hooks', |
|||
], |
|||
'rules': { |
|||
'react/jsx-props-no-spreading': 'off', |
|||
'import/extensions': 'off', |
|||
"react/jsx-indent": [ |
|||
'error', |
|||
4, |
|||
{ |
|||
checkAttributes: true, |
|||
indentLogicalExpressions: true |
|||
} |
|||
], |
|||
'react/require-default-props': 'off', |
|||
'react/prop-types': 'off', |
|||
'react-hooks/rules-of-hooks': 'error', |
|||
'react-hooks/exhaustive-deps': 'error', |
|||
'max-len': ['warn', {'code': 120, 'tabWidth': 4}], |
|||
'no-unused-vars': 'warn', |
|||
'no-console': ['warn', {allow: ['warn', 'error']}], |
|||
'no-shadow': 'warn', |
|||
"no-multi-str": "warn", |
|||
"jsx-a11y/label-has-associated-control": [2, { |
|||
"labelAttributes": ["label"], |
|||
"controlComponents": ["Input"], |
|||
"depth": 3, |
|||
}], |
|||
}, |
|||
'settings': { |
|||
'import/resolver': { |
|||
'node': { |
|||
'extensions': ['.js', '.jsx'] |
|||
} |
|||
} |
|||
}, |
|||
}; |
@ -0,0 +1,22 @@ |
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. |
|||
|
|||
# testing |
|||
/coverage |
|||
|
|||
# production |
|||
/build |
|||
|
|||
# misc |
|||
.DS_Store |
|||
.env |
|||
.env.local |
|||
.env.development |
|||
.env.development.local |
|||
.env.test |
|||
.env.test.local |
|||
.env.production |
|||
.env.production.local |
|||
|
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
@ -0,0 +1,36 @@ |
|||
# Concordia |
|||
|
|||
The Concordia application is a React app that handles interactions with the contracts and the distributed database used. |
|||
|
|||
This document provides information about the Concordia React Application and how to get it running for development. |
|||
|
|||
## Prerequisites |
|||
|
|||
Concordia requires at the minimum two services to work, a blockchain and a rendezvous server. |
|||
|
|||
Furthermore, the deployed contract artifacts must be made available. This can be done in two ways, migrate the contracts |
|||
and make sure the artifacts are present in the `concordia/packages/concordia-contracts/build` directory, or migrate the |
|||
contracts and upload the artifacts to a contracts-provider instance. |
|||
|
|||
## Running Concordia |
|||
|
|||
To start the application in development mode simply execute the `start` script: |
|||
```shell |
|||
yarn start |
|||
``` |
|||
|
|||
The application makes use of the environment variables described below. |
|||
|
|||
| Environment variable | Default value | Usage | |
|||
| --- | --- | --- | |
|||
| REACT_APP_CONCORDIA_HOST | `127.0.0.1` | Set the hostname of the concordia application | |
|||
| REACT_APP_CONCORDIA_PORT | `7000` | Set the port of the concordia application | |
|||
| REACT_APP_RENDEZVOUS_HOST | `/ip4/127.0.0.1` | Set the hostname of the rendezvous server | |
|||
| REACT_APP_RENDEZVOUS_PORT | `9090` | Set the port of the rendezvous server | |
|||
| REACT_APP_USE_EXTERNAL_CONTRACTS_PROVIDER | `false` | Enable/Disable use of external contracts provider | |
|||
| REACT_APP_CONTRACTS_PROVIDER_HOST | `http://127.0.0.1` | Set the hostname of the contracts provider service | |
|||
| REACT_APP_CONTRACTS_PROVIDER_PORT | `8400` | Set the port of the contracts provider service | |
|||
| REACT_APP_CONTRACTS_VERSION_HASH | `latest` | Set the contracts tag that will be pulled | |
|||
|
|||
**Attention**: if using a contracts provider, make sure that the provider is set to allow CORS from the host-port combo |
|||
defined by `REACT_APP_CONCORDIA_HOST` and `REACT_APP_CONCORDIA_PORT`. |
@ -0,0 +1,71 @@ |
|||
{ |
|||
"name": "concordia-app", |
|||
"version": "0.1.0", |
|||
"private": true, |
|||
"scripts": { |
|||
"start": "cross-env REACT_APP_VERSION=$npm_package_version REACT_APP_NAME=$npm_package_name react-scripts start", |
|||
"build": "cross-env REACT_APP_VERSION=$npm_package_version REACT_APP_NAME=$npm_package_name react-scripts build", |
|||
"test": "cross-env REACT_APP_VERSION=$npm_package_version REACT_APP_NAME=$npm_package_name react-scripts test", |
|||
"eject": "react-scripts eject", |
|||
"postinstall": "patch-package", |
|||
"analyze": "react-scripts build && source-map-explorer 'build/static/js/*.js' --gzip", |
|||
"lint": "eslint --ext js,jsx . --format table" |
|||
}, |
|||
"browserslist": { |
|||
"production": [ |
|||
">0.2%", |
|||
"not dead", |
|||
"not op_mini all" |
|||
], |
|||
"development": [ |
|||
"last 1 chrome version", |
|||
"last 1 firefox version", |
|||
"last 1 safari version" |
|||
] |
|||
}, |
|||
"dependencies": { |
|||
"@ecentrics/breeze": "~0.7.0", |
|||
"@ecentrics/drizzle": "~0.5.0", |
|||
"@ecentrics/eth-identity-provider": "~0.1.2", |
|||
"@reduxjs/toolkit": "~1.4.0", |
|||
"@welldone-software/why-did-you-render": "~6.0.5", |
|||
"apexcharts": "^3.26.0", |
|||
"concordia-contracts": "~0.1.0", |
|||
"concordia-shared": "~0.1.0", |
|||
"crypto-js": "~4.0.0", |
|||
"i18next": "^19.8.3", |
|||
"i18next-browser-languagedetector": "^6.0.1", |
|||
"i18next-http-backend": "^1.0.21", |
|||
"lodash": "^4.17.20", |
|||
"prop-types": "~15.7.2", |
|||
"react": "~16.13.1", |
|||
"react-apexcharts": "^1.3.7", |
|||
"react-avatar": "~3.9.7", |
|||
"react-copy-to-clipboard": "^5.0.3", |
|||
"react-dom": "~16.13.1", |
|||
"react-i18next": "^11.7.3", |
|||
"react-markdown": "^5.0.3", |
|||
"react-particles-js": "^3.4.0", |
|||
"react-redux": "~7.2.1", |
|||
"react-router": "^5.2.0", |
|||
"react-router-dom": "^5.2.0", |
|||
"react-scripts": "~3.4.3", |
|||
"react-timeago": "~5.2.0", |
|||
"redux-saga": "~1.1.3", |
|||
"semantic-ui-css": "~2.4.1", |
|||
"semantic-ui-react": "~2.0.3", |
|||
"web3": "~1.3.3" |
|||
}, |
|||
"devDependencies": { |
|||
"cross-env": "^7.0.3", |
|||
"eslint": "^6.8.0", |
|||
"eslint-config-airbnb": "^18.1.0", |
|||
"eslint-plugin-import": "^2.20.2", |
|||
"eslint-plugin-jsx-a11y": "^6.2.3", |
|||
"eslint-plugin-react": "^7.19.0", |
|||
"eslint-plugin-react-hooks": "^4.2.0", |
|||
"patch-package": "~6.2.2", |
|||
"postinstall-postinstall": "~2.1.0", |
|||
"source-map-explorer": "~2.5.0" |
|||
} |
|||
} |
@ -0,0 +1,15 @@ |
|||
diff --git a/node_modules/web3-eth/lib/index.js b/node_modules/web3-eth/lib/index.js
|
|||
index da8a65f..06d5f83 100644
|
|||
--- a/node_modules/web3-eth/lib/index.js
|
|||
+++ b/node_modules/web3-eth/lib/index.js
|
|||
@@ -288,8 +288,8 @@ var Eth = function Eth() {
|
|||
this.Iban = Iban; |
|||
// add ABI |
|||
this.abi = abi; |
|||
- // add ENS
|
|||
- this.ens = new ENS(this);
|
|||
+ // add ENS (Removed because of https://github.com/ethereum/web3.js/issues/2665#issuecomment-687164093)
|
|||
+ // this.ens = new ENS(this);
|
|||
var methods = [ |
|||
new Method({ |
|||
name: 'getNodeInfo', |
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,106 @@ |
|||
{ |
|||
"board.header.no.topics.message": "There are no topics yet!", |
|||
"board.sub.header.no.topics.guest": "Sign up and be the first to post.", |
|||
"board.sub.header.no.topics.user": "Be the first to post.", |
|||
"clear.databases.modal.cancel.button": "Cancel, keep databases", |
|||
"clear.databases.modal.clear.button": "Yes, delete databases", |
|||
"clear.databases.modal.clearing.progress.message": "This might take a minute...", |
|||
"clear.databases.modal.clearing.progress.title": "Clearing all Concordia databases", |
|||
"clear.databases.modal.description.body.user": "Although this action is generally recoverable some of your topics and posts may be permanently lost.", |
|||
"clear.databases.modal.description.pre.guest": "You are about to clear the Concordia databases stored locally in your browser.", |
|||
"clear.databases.modal.description.pre.user": "Be careful, {{username}}! You are about to clear the Concordia databases stored locally in your browser.", |
|||
"clear.databases.modal.form.username.label.guest": "Please type Concordia to confirm.", |
|||
"clear.databases.modal.form.username.label.user": "Please type your username to confirm.", |
|||
"clear.databases.modal.title": "Clear all Concordia databases. Are you sure?", |
|||
"custom.loading.tab.pane.default.generic.message": "Magic in the background", |
|||
"edit.information.modal.form.cancel.button": "Cancel", |
|||
"edit.information.modal.form.error.invalid.profile.picture.url.message": "The profile picture URL provided is not valid.", |
|||
"edit.information.modal.form.error.message.header": "Form contains errors", |
|||
"edit.information.modal.form.location.field.label": "Location", |
|||
"edit.information.modal.form.location.field.placeholder": "Location", |
|||
"edit.information.modal.form.profile.picture.field.label": "Profile picture URL", |
|||
"edit.information.modal.form.profile.picture.field.placeholder": "URL", |
|||
"edit.information.modal.form.submit.button": "Submit", |
|||
"edit.information.modal.title": "Edit profile information", |
|||
"poll.create.add.option.button": "Add Option", |
|||
"poll.create.allow.vote.changes.field.label": "Allow vote changes", |
|||
"poll.create.option.field.label": "Option #{{id}}", |
|||
"poll.create.option.field.placeholder": "Option #{{id}}", |
|||
"poll.create.question.field.label": "Poll Question", |
|||
"poll.create.question.field.placeholder": "Question", |
|||
"post.create.form.send.button": "Post", |
|||
"post.form.content.field.placeholder": "Message", |
|||
"post.form.subject.field.placeholder": "Subject", |
|||
"post.list.row.post.id": "#{{id}}", |
|||
"profile.general.tab.address.row.title": "Account address:", |
|||
"profile.general.tab.clear.databases.button.title": "Clear databases", |
|||
"profile.general.tab.edit.info.button.title": "Edit information", |
|||
"profile.general.tab.location.row.not.set": "Not set", |
|||
"profile.general.tab.location.row.title": "Location:", |
|||
"profile.general.tab.number.of.posts.row.title": "Number of posts:", |
|||
"profile.general.tab.number.of.topics.row.title": "Number of topics created:", |
|||
"profile.general.tab.polls.db.address.row.title": "PollsDB:", |
|||
"profile.general.tab.posts.db.address.row.title": "PostsDB:", |
|||
"profile.general.tab.registration.date.row.title": "Member since:", |
|||
"profile.general.tab.save.info.button.title": "Save information", |
|||
"profile.general.tab.title": "General", |
|||
"profile.general.tab.topics.db.address.row.title": "TopicsDB:", |
|||
"profile.general.tab.user.db.address.row.title": "UserDB:", |
|||
"profile.general.tab.username.row.title": "Username:", |
|||
"profile.posts.tab.title": "Posts", |
|||
"profile.topics.tab.title": "Topics", |
|||
"profile.user.has.no.posts.header.message": "{{user}} has not posted yet", |
|||
"profile.user.has.no.topics.header.message": "{{user}} has created no topics yet", |
|||
"register.card.header": "Sign Up", |
|||
"register.form.button.back": "Back", |
|||
"register.form.header.already.member.message": "There is already an account for this address.\nIf you want to create another account please change your address.", |
|||
"register.form.personal.information.step.button.skip": "Skip for now", |
|||
"register.form.personal.information.step.button.submit": "Submit", |
|||
"register.form.personal.information.step.error.invalid.profile.picture.url.message": "The profile picture URL provided is not valid.", |
|||
"register.form.personal.information.step.error.message.header": "Form contains errors", |
|||
"register.form.personal.information.step.location.field.label": "Location", |
|||
"register.form.personal.information.step.location.field.placeholder": "Location", |
|||
"register.form.personal.information.step.profile.picture.field.label": "Profile picture URL", |
|||
"register.form.personal.information.step.profile.picture.field.placeholder": "URL", |
|||
"register.form.profile.information.step.description": "Give a hint about who you are", |
|||
"register.form.profile.information.step.title": "Profile Information", |
|||
"register.form.sign.up.step.button.guest": "Continue as guest", |
|||
"register.form.sign.up.step.button.submit": "Sign Up", |
|||
"register.form.sign.up.step.description": "Create a Concordia account", |
|||
"register.form.sign.up.step.error.message.header": "Form contains errors", |
|||
"register.form.sign.up.step.title": "Sign Up", |
|||
"register.p.account.address": "Account address:", |
|||
"topbar.button.about": "About", |
|||
"topbar.button.clear.databases": "Clear databases", |
|||
"topbar.button.create.topic": "Create topic", |
|||
"topbar.button.profile": "Profile", |
|||
"topbar.button.register": "Sign Up", |
|||
"topic.create.form.add.poll.button": "Add Poll", |
|||
"topic.create.form.content.field.label": "First post content", |
|||
"topic.create.form.content.field.placeholder": "Message", |
|||
"topic.create.form.post.button": "Create Topic", |
|||
"topic.create.form.remove.poll.button": "Remove Poll", |
|||
"topic.create.form.subject.field.label": "Topic subject", |
|||
"topic.create.form.subject.field.placeholder": "Subject", |
|||
"topic.list.row.topic.id": "#{{id}}", |
|||
"topic.poll.guest.header": "Only registered users are able to vote in polls.", |
|||
"topic.poll.guest.sub.header.link": "signup", |
|||
"topic.poll.guest.sub.header.post": " page.", |
|||
"topic.poll.guest.sub.header.pre": "You can register in the ", |
|||
"topic.poll.invalid.data.header": "This topic has a poll but the data are untrusted!", |
|||
"topic.poll.invalid.data.sub.header": "The poll data downloaded from the poster have been tampered with.", |
|||
"topic.poll.tab.graph.title": "Results", |
|||
"topic.poll.tab.results.vote": "Vote", |
|||
"topic.poll.tab.results.votes": "Votes", |
|||
"topic.poll.tab.results.no.votes": "No one has voted yet!", |
|||
"topic.poll.tab.results.user.vote": "You voted: ", |
|||
"topic.poll.tab.vote.no.changes": "This poll does not allow vote changes.", |
|||
"topic.poll.tab.vote.form.button.submit": "Submit", |
|||
"topic.poll.tab.vote.form.button.unvote": "Remove Vote", |
|||
"topic.poll.tab.vote.form.radio.label": "Select one of the options:", |
|||
"topic.poll.tab.vote.title": "Vote", |
|||
"username.selector.error.username.empty.message": "Username is required", |
|||
"username.selector.error.username.taken.message": "The username {{username}} is already taken.", |
|||
"username.selector.username.field.label": "Username", |
|||
"username.selector.username.field.placeholder": "Username" |
|||
} |
@ -0,0 +1,2 @@ |
|||
# https://www.robotstxt.org/robotstxt.html |
|||
User-agent: * |
@ -0,0 +1,25 @@ |
|||
import React from 'react'; |
|||
import { Provider } from 'react-redux'; |
|||
import { BrowserRouter as Router } from 'react-router-dom'; |
|||
import PropTypes from 'prop-types'; |
|||
import InitializationScreen from './components/InitializationScreen'; |
|||
import Routes from './Routes'; |
|||
import './intl/index'; |
|||
import 'semantic-ui-css/semantic.min.css'; |
|||
|
|||
const App = ({ store }) => ( |
|||
<Provider store={store}> |
|||
<InitializationScreen> |
|||
<Router> |
|||
<Routes /> |
|||
</Router> |
|||
</InitializationScreen> |
|||
</Provider> |
|||
); |
|||
|
|||
App.propTypes = { |
|||
// eslint-disable-next-line react/forbid-prop-types |
|||
store: PropTypes.object.isRequired, |
|||
}; |
|||
|
|||
export default App; |
@ -0,0 +1,22 @@ |
|||
import React from 'react'; |
|||
|
|||
class ErrorBoundary extends React.Component { |
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { hasError: false }; |
|||
} |
|||
|
|||
static getDerivedStateFromError() { |
|||
return { hasError: true }; |
|||
} |
|||
|
|||
render() { |
|||
const { props: { children }, state: { hasError } } = this; |
|||
if (hasError) { |
|||
return <h1>Something went wrong.</h1>; // TODO: Make a better "Something went wrong" screen |
|||
} |
|||
return children; |
|||
} |
|||
} |
|||
|
|||
export default ErrorBoundary; |
@ -0,0 +1,95 @@ |
|||
import React, { Fragment, lazy, Suspense } from 'react'; |
|||
import { Redirect, Route, Switch } from 'react-router-dom'; |
|||
import MainLayout from './layouts/MainLayout'; |
|||
import LoadingScreen from './components/LoadingScreen'; |
|||
import RegisterLayout from './layouts/RegisterLayout'; |
|||
|
|||
const routesConfig = [ |
|||
{ |
|||
exact: true, |
|||
path: '/', |
|||
component: () => <Redirect to="/home" />, |
|||
}, |
|||
{ |
|||
exact: true, |
|||
path: '/404', |
|||
layout: MainLayout, |
|||
component: lazy(() => import('./components/NotFound')), |
|||
}, |
|||
{ |
|||
path: '/auth', |
|||
layout: RegisterLayout, |
|||
routes: [ |
|||
{ |
|||
exact: true, |
|||
path: '/auth/register', |
|||
component: lazy(() => import('./views/Register')), |
|||
}, |
|||
{ |
|||
component: () => <Redirect to="/404" />, |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
path: '*', |
|||
layout: MainLayout, |
|||
routes: [ |
|||
{ |
|||
exact: true, |
|||
path: '/home', |
|||
component: lazy(() => import('./views/Home')), |
|||
}, |
|||
{ |
|||
exact: true, |
|||
path: '/about', |
|||
component: lazy(() => import('./views/About')), |
|||
}, |
|||
{ |
|||
exact: true, |
|||
path: '/topics/:id(\\bnew\\b|\\d+)', |
|||
component: lazy(() => import('./views/Topic')), |
|||
}, |
|||
{ |
|||
exact: true, |
|||
path: ['/users/:id', '/profiles/:id', '/profile'], |
|||
component: lazy(() => import('./views/Profile')), |
|||
}, |
|||
{ |
|||
component: () => <Redirect to="/404" />, |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
|
|||
const renderRoutes = (routes) => (routes ? ( |
|||
<Suspense fallback={<LoadingScreen />}> |
|||
<Switch> |
|||
{routes.map((route, i) => { |
|||
const Layout = route.layout || Fragment; |
|||
const Component = route.component; |
|||
|
|||
const key = route.path ? route.path.concat(i) : ''.concat(i); |
|||
return ( |
|||
<Route |
|||
key={key} |
|||
path={route.path} |
|||
exact={route.exact} |
|||
render={(props) => ( |
|||
<Layout> |
|||
{route.routes |
|||
? renderRoutes(route.routes) |
|||
: <Component {...props} />} |
|||
</Layout> |
|||
)} |
|||
/> |
|||
); |
|||
})} |
|||
</Switch> |
|||
</Suspense> |
|||
) : null); |
|||
|
|||
function Routes() { |
|||
return renderRoutes(routesConfig); |
|||
} |
|||
|
|||
export default Routes; |
@ -0,0 +1,35 @@ |
|||
# About Concordia |
|||
|
|||
## What |
|||
|
|||
Concordia is a forum platform (remember forums? 🤩) that focuses on user privacy and direct democratic voting. It is a |
|||
FOSS distributed via its Gitlab [repository][concordia-repository] and Docker [repository][concordia-docker-hub] under |
|||
the [MIT][concordia-license] license. |
|||
|
|||
## Why |
|||
|
|||
The value of privacy, freedom of speech and democracy are diminishing in modern software. Even more so in social media |
|||
platforms. Users are called to select between being the product of companies that sell their personal information and |
|||
being shut out of the modern, digital society. |
|||
|
|||
Concordia, much like other projects of this kind, provides an alternative to this predicament. |
|||
|
|||
## How |
|||
|
|||
Concordia uses decentralized technologies, namely the Ethereum blockchain and its smart contracts, as well as the |
|||
distributed database OrbitDB that's based on the decentralized network IPFS. These technologies make Concordia |
|||
impervious to censorship and guaranty the immutability of user information and anonymity while enabling user |
|||
authentication that makes trusted, direct voting possible. |
|||
|
|||
You can read more about the technological stack in Concordia's [whitepaper][concordia-whitepaper]. |
|||
|
|||
--- |
|||
|
|||
Developed by [apostolof][devs-apostolof-profile], [ezerous][devs-ezerous-profile] |
|||
|
|||
[concordia-repository]: https://gitlab.com/ecentrics/concordia |
|||
[concordia-docker-hub]: https://hub.docker.com/repository/docker/ecentrics/concordia-app |
|||
[concordia-license]: https://gitlab.com/ecentrics/concordia/-/blob/master/LICENSE.md |
|||
[devs-apostolof-profile]: https://gitlab.com/Apostolof |
|||
[devs-ezerous-profile]: https://gitlab.com/Ezerous |
|||
[concordia-whitepaper]: https://whitepaper.concordia.ecentrics.net |
@ -0,0 +1,72 @@ |
|||
:root { |
|||
--primary-color: #EA6954; |
|||
--primary-color-highlighted: #DB5844; |
|||
--secondary-color: #0B2540; |
|||
--secondary-color-highlighted: #061A30; |
|||
} |
|||
|
|||
body { |
|||
overflow: auto; |
|||
margin: 0; |
|||
background: white; |
|||
} |
|||
|
|||
div { |
|||
word-break: break-word; |
|||
} |
|||
|
|||
#root { |
|||
height: 100%; |
|||
} |
|||
|
|||
.i18next-newlines { |
|||
white-space: pre-line !important; |
|||
} |
|||
|
|||
.text-secondary { |
|||
color: gray; |
|||
font-style: italic; |
|||
} |
|||
|
|||
.primary-button{ |
|||
color: white !important; |
|||
background-color: var(--primary-color) !important; |
|||
} |
|||
|
|||
.primary-button:hover { |
|||
background-color: var(--primary-color-highlighted) !important; |
|||
} |
|||
|
|||
.secondary-button{ |
|||
color: white !important; |
|||
background-color: var(--secondary-color) !important; |
|||
} |
|||
|
|||
.secondary-button:hover { |
|||
background-color: var(--secondary-color-highlighted) !important; |
|||
} |
|||
|
|||
.skip-button { |
|||
color: var(--secondary-color) !important; |
|||
background-color: white !important; |
|||
box-shadow: 0 0 0 1px var(--secondary-color) inset !important; |
|||
} |
|||
|
|||
.skip-button:hover { |
|||
color: var(--secondary-color-highlighted) !important; |
|||
box-shadow: 0 0 0 1px var(--secondary-color-highlighted) inset !important; |
|||
} |
|||
|
|||
.unselectable { |
|||
-webkit-user-select: none; |
|||
-khtml-user-select: none; |
|||
-moz-user-select: none; |
|||
-ms-user-select: none; |
|||
-o-user-select: none; |
|||
user-select: none; |
|||
} |
|||
|
|||
textarea, input, button, select { |
|||
font-family: inherit; |
|||
font-size: inherit; |
|||
} |
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 10 KiB |
@ -0,0 +1,32 @@ |
|||
const particlesOptions = { |
|||
particles: { |
|||
number: { |
|||
value: 120, |
|||
density: { |
|||
enable: true, |
|||
value_area: 1500, |
|||
}, |
|||
}, |
|||
line_linked: { |
|||
enable: true, |
|||
opacity: 0.06, |
|||
}, |
|||
move: { |
|||
direction: 'none', |
|||
speed: 0.12, |
|||
}, |
|||
size: { |
|||
value: 1.6, |
|||
}, |
|||
opacity: { |
|||
anim: { |
|||
enable: true, |
|||
speed: 0.6, |
|||
opacity_min: 0.05, |
|||
}, |
|||
}, |
|||
}, |
|||
retina_detect: true, |
|||
}; |
|||
|
|||
export default particlesOptions; |
@ -0,0 +1,81 @@ |
|||
// Modified version of https://github.com/trufflesuite/drizzle/blob/develop/packages/react-plugin/src/DrizzleContext.js |
|||
import React from 'react'; |
|||
|
|||
const Context = React.createContext(); |
|||
|
|||
class Provider extends React.Component { |
|||
constructor(props) { |
|||
super(props); |
|||
|
|||
this.state = { |
|||
drizzleState: null, |
|||
drizzleInitialized: false, |
|||
breezeState: null, |
|||
breezeInitialized: false, |
|||
}; |
|||
} |
|||
|
|||
componentDidMount() { |
|||
const { drizzle, breeze } = this.props; |
|||
// subscribe to changes in the store, keep state up-to-date |
|||
this.unsubscribe = drizzle.store.subscribe(() => { |
|||
const drizzleState = drizzle.store.getState(); |
|||
const breezeState = breeze.store.getState(); |
|||
|
|||
if (drizzleState.drizzleStatus.initialized) { |
|||
this.setState({ |
|||
drizzleState, |
|||
drizzleInitialized: true, |
|||
}); |
|||
} |
|||
if (breezeState.breezeStatus.initialized) { |
|||
this.setState({ |
|||
breezeState, |
|||
breezeInitialized: true, |
|||
}); |
|||
} |
|||
}); |
|||
|
|||
this.unsubscribe = breeze.store.subscribe(() => { |
|||
const breezeState = breeze.store.getState(); |
|||
if (breezeState.breezeStatus.initialized) { |
|||
this.setState({ |
|||
breezeState, |
|||
breezeInitialized: true, |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
componentWillUnmount() { |
|||
this.unsubscribe(); |
|||
} |
|||
|
|||
render() { |
|||
const { |
|||
drizzleState, drizzleInitialized, breezeState, breezeInitialized, |
|||
} = this.state; |
|||
const { drizzle, breeze, children } = this.props; |
|||
|
|||
return ( |
|||
<Context.Provider |
|||
value={{ |
|||
drizzle, |
|||
drizzleState, |
|||
drizzleInitialized, |
|||
breeze, |
|||
breezeState, |
|||
breezeInitialized, |
|||
}} |
|||
> |
|||
{children} |
|||
</Context.Provider> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default { |
|||
Context, |
|||
Consumer: Context.Consumer, |
|||
Provider, |
|||
}; |
@ -0,0 +1,151 @@ |
|||
import React, { |
|||
useCallback, useMemo, useState, |
|||
useEffect, |
|||
} from 'react'; |
|||
import { |
|||
Button, Form, Input, Modal, |
|||
} from 'semantic-ui-react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import { useSelector } from 'react-redux'; |
|||
import purgeIndexedDBs from '../../utils/indexedDB/indexedDBUtils'; |
|||
|
|||
const ClearDatabasesModal = (props) => { |
|||
const { |
|||
open, onDatabasesCleared, onCancel, |
|||
} = props; |
|||
const [confirmationInput, setConfirmationInput] = useState(''); |
|||
const [userConfirmed, setUserConfirmed] = useState(false); |
|||
const [isClearing, setIsClearing] = useState(false); |
|||
const user = useSelector((state) => state.user); |
|||
const { t } = useTranslation(); |
|||
|
|||
useEffect(() => { |
|||
if (user.hasSignedUp && confirmationInput === user.username) { |
|||
setUserConfirmed(true); |
|||
} else if (!user.hasSignedUp && confirmationInput.toLowerCase() === 'concordia') { |
|||
setUserConfirmed(true); |
|||
} else { |
|||
setUserConfirmed(false); |
|||
} |
|||
}, [confirmationInput, user.hasSignedUp, user.username]); |
|||
|
|||
const handleSubmit = useCallback(() => { |
|||
setIsClearing(true); |
|||
|
|||
purgeIndexedDBs() |
|||
.then(() => { |
|||
onDatabasesCleared(); |
|||
}).catch((reason) => console.error(reason)); |
|||
}, [onDatabasesCleared]); |
|||
|
|||
const onCancelTry = useCallback(() => { |
|||
if (!isClearing) { |
|||
setConfirmationInput(''); |
|||
onCancel(); |
|||
} |
|||
}, [isClearing, onCancel]); |
|||
|
|||
const handleInputChange = (event, { value }) => { setConfirmationInput(value); }; |
|||
|
|||
const modalContent = useMemo(() => { |
|||
if (isClearing) { |
|||
return ( |
|||
<> |
|||
<p> |
|||
{t('clear.databases.modal.clearing.progress.message')} |
|||
</p> |
|||
</> |
|||
); |
|||
} |
|||
|
|||
if (user.hasSignedUp) { |
|||
return ( |
|||
<> |
|||
<p> |
|||
{t('clear.databases.modal.description.pre.user', { username: user.username })} |
|||
</p> |
|||
<p> |
|||
{t('clear.databases.modal.description.body.user')} |
|||
</p> |
|||
<Form> |
|||
<Form.Field> |
|||
<label htmlFor="form-clear-databases-field-confirm"> |
|||
{t('clear.databases.modal.form.username.label.user')} |
|||
</label> |
|||
<Input |
|||
id="form-clear-databases-field-confirm" |
|||
name="confirmationInput" |
|||
value={confirmationInput} |
|||
onChange={handleInputChange} |
|||
/> |
|||
</Form.Field> |
|||
</Form> |
|||
</> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<> |
|||
<p> |
|||
{t('clear.databases.modal.description.pre.guest')} |
|||
</p> |
|||
<Form> |
|||
<Form.Field> |
|||
<label htmlFor="form-clear-databases-field-confirm"> |
|||
{t('clear.databases.modal.form.username.label.guest')} |
|||
</label> |
|||
<Input |
|||
id="form-clear-databases-field-confirm" |
|||
name="confirmationInput" |
|||
value={confirmationInput} |
|||
onChange={handleInputChange} |
|||
/> |
|||
</Form.Field> |
|||
</Form> |
|||
</> |
|||
); |
|||
}, [confirmationInput, isClearing, t, user.hasSignedUp, user.username]); |
|||
|
|||
return useMemo(() => ( |
|||
<Modal |
|||
onClose={onCancelTry} |
|||
open={open} |
|||
size="small" |
|||
> |
|||
<Modal.Header> |
|||
{isClearing |
|||
? t('clear.databases.modal.clearing.progress.title') |
|||
: t('clear.databases.modal.title')} |
|||
</Modal.Header> |
|||
<Modal.Content> |
|||
<Modal.Description> |
|||
{modalContent} |
|||
</Modal.Description> |
|||
</Modal.Content> |
|||
|
|||
{!isClearing && ( |
|||
<Modal.Actions> |
|||
<Button className="secondary-button" onClick={onCancelTry} disabled={isClearing}> |
|||
{t('clear.databases.modal.cancel.button')} |
|||
</Button> |
|||
<Button onClick={handleSubmit} className="primary-button" disabled={!userConfirmed}> |
|||
{t('clear.databases.modal.clear.button')} |
|||
</Button> |
|||
</Modal.Actions> |
|||
)} |
|||
</Modal> |
|||
), [handleSubmit, isClearing, modalContent, onCancelTry, open, t, userConfirmed]); |
|||
}; |
|||
|
|||
ClearDatabasesModal.defaultProps = { |
|||
open: false, |
|||
}; |
|||
|
|||
ClearDatabasesModal.propTypes = { |
|||
open: PropTypes.bool, |
|||
onDatabasesCleared: PropTypes.func.isRequired, |
|||
onCancel: PropTypes.func.isRequired, |
|||
}; |
|||
|
|||
export default ClearDatabasesModal; |
@ -0,0 +1,46 @@ |
|||
import React, { useMemo } from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { |
|||
Dimmer, Loader, Placeholder, Tab, |
|||
} from 'semantic-ui-react'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
const CustomLoadingTabPane = (props) => { |
|||
const { loading, loadingMessage, children } = props; |
|||
const { t } = useTranslation(); |
|||
|
|||
return useMemo(() => { |
|||
if (loading) { |
|||
return ( |
|||
<Tab.Pane> |
|||
<Dimmer active inverted> |
|||
<Loader inverted> |
|||
{loadingMessage !== undefined |
|||
? loadingMessage |
|||
: t('custom.loading.tab.pane.default.generic.message')} |
|||
</Loader> |
|||
</Dimmer> |
|||
<Placeholder fluid> |
|||
<Placeholder.Line length="very long" /> |
|||
<Placeholder.Line length="medium" /> |
|||
<Placeholder.Line length="long" /> |
|||
</Placeholder> |
|||
</Tab.Pane> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<Tab.Pane> |
|||
{children} |
|||
</Tab.Pane> |
|||
); |
|||
}, [children, loading, loadingMessage, t]); |
|||
}; |
|||
|
|||
CustomLoadingTabPane.propTypes = { |
|||
loading: PropTypes.bool, |
|||
loadingMessage: PropTypes.string, |
|||
children: PropTypes.element, |
|||
}; |
|||
|
|||
export default CustomLoadingTabPane; |
@ -0,0 +1,76 @@ |
|||
import React from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
|
|||
import { Container, Progress } from 'semantic-ui-react'; |
|||
|
|||
// Images |
|||
import metamaskLogo from '../../../assets/images/metamask_logo.svg'; |
|||
import ethereumLogo from '../../../assets/images/ethereum_logo.svg'; |
|||
import ipfsLogo from '../../../assets/images/ipfs_logo.svg'; |
|||
import orbitdbLogo from '../../../assets/images/orbitdb_logo.svg'; |
|||
import appLogo from '../../../assets/images/app_logo_circle.svg'; |
|||
|
|||
import './style.css'; |
|||
|
|||
const LoadingComponent = (props) => { |
|||
const { |
|||
imageType, messageList, progressType, title, message, progress, |
|||
} = props; |
|||
let imageSrc; let imageAlt; let listItems; let indicating; let |
|||
error; |
|||
|
|||
if (imageType === 'metamask') { |
|||
imageSrc = metamaskLogo; |
|||
imageAlt = 'metamask_logo'; |
|||
} else if (imageType === 'ethereum') { |
|||
imageSrc = ethereumLogo; |
|||
imageAlt = 'ethereum_logo'; |
|||
} else if (imageType === 'ipfs') { |
|||
imageSrc = ipfsLogo; |
|||
imageAlt = 'ipfs_logo'; |
|||
} else if (imageType === 'orbit') { |
|||
imageSrc = orbitdbLogo; |
|||
imageAlt = 'orbitdb_logo'; |
|||
} else if (imageType === 'app') { |
|||
imageSrc = appLogo; |
|||
imageAlt = 'app_logo'; |
|||
} |
|||
|
|||
if (progressType === 'indicating') indicating = true; |
|||
else if (progressType === 'error') error = true; |
|||
|
|||
if (messageList) { |
|||
listItems = messageList.map((listItem) => <li>{listItem}</li>); |
|||
} |
|||
|
|||
const list = messageList ? <ul>{listItems}</ul> : ''; |
|||
|
|||
return ( |
|||
<main id="loading-screen"> |
|||
<Container id="loading-screen-container"> |
|||
<img src={imageSrc} alt={imageAlt} id="loading-img" /> |
|||
<p><strong>{title}</strong></p> |
|||
<p>{message}</p> |
|||
{list} |
|||
</Container> |
|||
<Progress |
|||
id="loading-screen-progress" |
|||
percent={progress} |
|||
size="small" |
|||
indicating={indicating} |
|||
error={error} |
|||
/> |
|||
</main> |
|||
); |
|||
}; |
|||
|
|||
LoadingComponent.propTypes = { |
|||
title: PropTypes.string.isRequired, |
|||
message: PropTypes.string.isRequired, |
|||
messageList: PropTypes.arrayOf(PropTypes.string), |
|||
imageType: PropTypes.string.isRequired, |
|||
progress: PropTypes.number.isRequired, |
|||
progressType: PropTypes.string.isRequired, |
|||
}; |
|||
|
|||
export default LoadingComponent; |
@ -0,0 +1,25 @@ |
|||
#loading-screen { |
|||
padding-top: 14em; |
|||
text-align: center; |
|||
font-size: large; |
|||
height: 100%; |
|||
} |
|||
|
|||
#loading-screen ul { |
|||
list-style-position: inside; |
|||
} |
|||
|
|||
#loading-img { |
|||
margin-bottom: 3em; |
|||
height: 12em; |
|||
} |
|||
|
|||
#loading-screen-container { |
|||
height: 26em; |
|||
} |
|||
|
|||
#loading-screen-progress { |
|||
width: 40vw; |
|||
margin-left: auto !important; |
|||
margin-right: auto !important; |
|||
} |
@ -0,0 +1,160 @@ |
|||
import React, { Children } from 'react'; |
|||
import { breezeConstants } from '@ecentrics/breeze'; |
|||
import { useSelector } from 'react-redux'; |
|||
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; |
|||
import CustomLoader from './CustomLoader'; |
|||
|
|||
const InitializationLoader = ({ children }) => { |
|||
const initializing = useSelector((state) => state.drizzleStatus.initializing); |
|||
const failed = useSelector((state) => state.drizzleStatus.failed); |
|||
const ipfsStatus = useSelector((state) => state.ipfs.status); |
|||
const orbitStatus = useSelector((state) => state.orbit.status); |
|||
const web3Status = useSelector((state) => state.web3.status); |
|||
const web3NetworkId = useSelector((state) => state.web3.networkId); |
|||
const web3NetworkFailed = useSelector((state) => state.web3.networkFailed); |
|||
const web3AccountsFailed = useSelector((state) => state.web3.accountsFailed); |
|||
const contractInitialized = useSelector((state) => state.contracts[FORUM_CONTRACT].initialized); |
|||
const contractDeployed = useSelector((state) => state.contracts[FORUM_CONTRACT].deployed); |
|||
const userFetched = useSelector((state) => state.user.address); |
|||
|
|||
if (!window.ethereum) { |
|||
return ( |
|||
<CustomLoader |
|||
title="Couldn't detect MetaMask!" |
|||
message={['Please make sure to install ', <a href="https://metamask.io/">MetaMask</a>, ' first.']} |
|||
imageType="metamask" |
|||
progress={10} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if ((web3Status === 'initializing' || !web3NetworkId) && !web3NetworkFailed) { |
|||
return ( |
|||
<CustomLoader |
|||
title="Connecting to the Ethereum network..." |
|||
message="Please make sure to unlock MetaMask and grant the app the right to connect to your account." |
|||
imageType="ethereum" |
|||
progress={20} |
|||
progressType="indicating" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (web3Status === 'failed' || web3NetworkFailed) { |
|||
return ( |
|||
<CustomLoader |
|||
title="No connection to the Ethereum network!" |
|||
message="Please make sure that:" |
|||
messageList={['MetaMask is unlocked and pointed to the correct, available network', |
|||
'The app has been granted the right to connect to your account']} |
|||
imageType="ethereum" |
|||
progress={20} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (web3Status === 'initialized' && web3AccountsFailed) { |
|||
return ( |
|||
<CustomLoader |
|||
title="We can't find any Ethereum accounts!" |
|||
message="Please make sure that MetaMask is unlocked." |
|||
imageType="ethereum" |
|||
progress={20} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (initializing || (!failed && !contractInitialized && contractDeployed)) { |
|||
return ( |
|||
<CustomLoader |
|||
title="Initializing contracts..." |
|||
message="" |
|||
imageType="ethereum" |
|||
progress={40} |
|||
progressType="indicating" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (!contractDeployed) { |
|||
return ( |
|||
<CustomLoader |
|||
title="No contracts found on the current network!" |
|||
message="Please make sure that you are connected to the correct network and the contracts are deployed." |
|||
imageType="ethereum" |
|||
progress={40} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (ipfsStatus === breezeConstants.STATUS_UNINITIALIZED || ipfsStatus === breezeConstants.STATUS_INITIALIZING) { |
|||
return ( |
|||
<CustomLoader |
|||
title="Initializing IPFS..." |
|||
message="" |
|||
imageType="ipfs" |
|||
progress={60} |
|||
progressType="indicating" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (ipfsStatus === breezeConstants.STATUS_FAILED) { |
|||
return ( |
|||
<CustomLoader |
|||
title="IPFS initialization failed!" |
|||
message="" |
|||
imageType="ipfs" |
|||
progress={60} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (orbitStatus === breezeConstants.STATUS_UNINITIALIZED || orbitStatus === breezeConstants.STATUS_INITIALIZING) { |
|||
const message = process.env.NODE_ENV === 'development' |
|||
? 'If needed, please sign the transaction in MetaMask to create the databases.' |
|||
: 'Please sign the transaction in MetaMask to create the databases.'; |
|||
return ( |
|||
<CustomLoader |
|||
title="Preparing OrbitDB..." |
|||
message={message} |
|||
imageType="orbit" |
|||
progress={80} |
|||
progressType="indicating" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (orbitStatus === breezeConstants.STATUS_FAILED) { |
|||
return ( |
|||
<CustomLoader |
|||
title="OrbitDB initialization failed!" |
|||
message="" |
|||
imageType="orbit" |
|||
progress={80} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (!userFetched) { |
|||
return ( |
|||
<CustomLoader |
|||
title="Loading dapp..." |
|||
message="" |
|||
imageType="app" |
|||
progress={90} |
|||
progressType="indicating" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
return Children.only(children); |
|||
}; |
|||
|
|||
export default InitializationLoader; |
@ -0,0 +1,8 @@ |
|||
import React from 'react'; |
|||
import { Loader } from 'semantic-ui-react'; |
|||
|
|||
const LoadingScreen = () => ( |
|||
<Loader active /> |
|||
); |
|||
|
|||
export default LoadingScreen; |
@ -0,0 +1,13 @@ |
|||
import React from 'react'; |
|||
import pageNotFound from '../assets/images/PageNotFound.jpg'; |
|||
|
|||
const NotFound = () => ( |
|||
<div style={{ |
|||
textAlign: 'center', |
|||
}} |
|||
> |
|||
<img src={pageNotFound} alt="Page not found!" /> |
|||
</div> |
|||
); |
|||
|
|||
export default NotFound; |
@ -0,0 +1,23 @@ |
|||
import React, { useMemo } from 'react'; |
|||
import { Icon, Pagination } from 'semantic-ui-react'; |
|||
|
|||
export const ITEMS_PER_PAGE = 10; |
|||
|
|||
const PaginationComponent = (props) => { |
|||
const { numberOfItems, onPageChange } = props; |
|||
return useMemo(() => ( |
|||
<Pagination |
|||
defaultActivePage={1} |
|||
ellipsisItem={{ content: <Icon name="ellipsis horizontal" />, icon: true }} |
|||
firstItem={{ content: <Icon name="angle double left" />, icon: true }} |
|||
lastItem={{ content: <Icon name="angle double right" />, icon: true }} |
|||
prevItem={{ content: <Icon name="angle left" />, icon: true }} |
|||
nextItem={{ content: <Icon name="angle right" />, icon: true }} |
|||
totalPages={Math.ceil(numberOfItems / ITEMS_PER_PAGE)} |
|||
disabled={numberOfItems <= ITEMS_PER_PAGE} |
|||
onPageChange={onPageChange} |
|||
/> |
|||
), [numberOfItems, onPageChange]); |
|||
}; |
|||
|
|||
export default PaginationComponent; |
@ -0,0 +1,50 @@ |
|||
import React, { memo } from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { |
|||
Button, Form, Icon, Input, |
|||
} from 'semantic-ui-react'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import './styles.css'; |
|||
|
|||
const PollOption = (props) => { |
|||
const { |
|||
id, removable, onChange, onRemove, |
|||
} = props; |
|||
const { t } = useTranslation(); |
|||
|
|||
return ( |
|||
<Form.Field className="form-poll-option" required> |
|||
<label className="form-topic-create-header" htmlFor="form-poll-create-field-subject"> |
|||
{t('poll.create.option.field.label', { id })} |
|||
</label> |
|||
<Input |
|||
id="form-poll-create-field-subject" |
|||
placeholder={t('poll.create.option.field.placeholder', { id })} |
|||
name="pollQuestionInput" |
|||
className="form-input" |
|||
onChange={(e) => onChange(e, id)} |
|||
/> |
|||
{removable |
|||
&& ( |
|||
<Button |
|||
className="remove-option-button" |
|||
key={`form-remove-option-button-${id}`} |
|||
negative |
|||
icon |
|||
onClick={(e) => onRemove(e, id)} |
|||
> |
|||
<Icon name="x" /> |
|||
</Button> |
|||
)} |
|||
</Form.Field> |
|||
); |
|||
}; |
|||
|
|||
PollOption.propTypes = { |
|||
id: PropTypes.number.isRequired, |
|||
onChange: PropTypes.func, |
|||
onRemove: PropTypes.func, |
|||
removable: PropTypes.bool, |
|||
}; |
|||
|
|||
export default memo(PollOption); |
@ -0,0 +1,4 @@ |
|||
.form-poll-option > .form-input{ |
|||
width: 50% !important; |
|||
margin-right: 0.25em; |
|||
} |
@ -0,0 +1,190 @@ |
|||
import React, { |
|||
useMemo, useState, useCallback, useEffect, forwardRef, useImperativeHandle, |
|||
} from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { useSelector } from 'react-redux'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import { |
|||
Button, Checkbox, Form, Icon, Input, |
|||
} from 'semantic-ui-react'; |
|||
import { VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; |
|||
import { POLL_CREATED_EVENT } from 'concordia-shared/src/constants/contracts/events/VotingContractEvents'; |
|||
import { POLLS_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases'; |
|||
import PollOption from './PollOption'; |
|||
import { breeze, drizzle } from '../../redux/store'; |
|||
import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../constants/TransactionStatus'; |
|||
import './styles.css'; |
|||
import { POLL_OPTIONS, POLL_QUESTION } from '../../constants/orbit/PollsDatabaseKeys'; |
|||
import { generatePollHash } from '../../utils/hashUtils'; |
|||
|
|||
const { contracts: { [VOTING_CONTRACT]: { methods: { createPoll } } } } = drizzle; |
|||
const { orbit: { stores } } = breeze; |
|||
|
|||
const PollCreate = forwardRef((props, ref) => { |
|||
const { account, onChange, onCreated } = props; |
|||
const transactionStack = useSelector((state) => state.transactionStack); |
|||
const transactions = useSelector((state) => state.transactions); |
|||
const [createPollCacheSendStackId, setCreatePollCacheSendStackId] = useState(''); |
|||
const [question, setQuestion] = useState(''); |
|||
const [options, setOptions] = useState( |
|||
[{ id: 1 }, { id: 2 }], |
|||
); |
|||
const [optionValues, setOptionValues] = useState( |
|||
['', ''], |
|||
); |
|||
const [optionsNextId, setOptionsNextId] = useState(3); |
|||
const [allowVoteChanges, setAllowVoteChanges] = useState(false); |
|||
const [creating, setCreating] = useState(false); |
|||
const [errored, setErrored] = useState(false); |
|||
const { t } = useTranslation(); |
|||
|
|||
const handlePollQuestionChange = useCallback((event) => { |
|||
const newQuestion = event.target.value.trim(); |
|||
if (newQuestion !== question) setQuestion(newQuestion); |
|||
}, [question]); |
|||
|
|||
const addOption = useCallback((e) => { |
|||
e.currentTarget.blur(); |
|||
const newOptions = [...options, { id: optionsNextId, removable: true }]; |
|||
const newOptionValues = [...optionValues, '']; |
|||
setOptionsNextId(optionsNextId + 1); |
|||
setOptions(newOptions); |
|||
setOptionValues(newOptionValues); |
|||
}, [optionValues, options, optionsNextId]); |
|||
|
|||
const removeOption = useCallback((e, id) => { |
|||
e.currentTarget.blur(); |
|||
const newOptions = [...options]; |
|||
newOptions.splice(id - 1, 1); |
|||
const newOptionValues = [...optionValues]; |
|||
newOptionValues.splice(id - 1, 1); |
|||
setOptions(newOptions); |
|||
setOptionValues(newOptionValues); |
|||
}, [optionValues, options]); |
|||
|
|||
const handlePollOptionChange = useCallback((event, id) => { |
|||
const newValue = event.target.value.trim(); |
|||
if (newValue !== optionValues[id - 1]) { |
|||
const newOptionValues = [...optionValues]; |
|||
newOptionValues[id - 1] = newValue; |
|||
setOptionValues(newOptionValues); |
|||
} |
|||
}, [optionValues]); |
|||
|
|||
const pollOptions = useMemo(() => options |
|||
.map((option, index) => { |
|||
const { id, removable } = option; |
|||
return ( |
|||
<PollOption |
|||
id={index + 1} |
|||
key={id} |
|||
removable={removable} |
|||
onRemove={removeOption} |
|||
onChange={handlePollOptionChange} |
|||
/> |
|||
); |
|||
}), [handlePollOptionChange, options, removeOption]); |
|||
|
|||
useEffect(() => { |
|||
onChange({ question, optionValues }); |
|||
}, [onChange, optionValues, question]); |
|||
|
|||
const handleCheckboxChange = useCallback((event, data) => { |
|||
setAllowVoteChanges(data.checked); |
|||
}, []); |
|||
|
|||
useImperativeHandle(ref, () => ({ |
|||
createPoll(topicId) { |
|||
setCreating(true); |
|||
const dataHash = generatePollHash(question, optionValues); |
|||
setCreatePollCacheSendStackId(createPoll.cacheSend( |
|||
...[topicId, options.length, dataHash, allowVoteChanges], { from: account }, |
|||
)); |
|||
}, |
|||
pollCreating() { |
|||
return creating; |
|||
}, |
|||
pollErrored() { |
|||
return errored; |
|||
}, |
|||
})); |
|||
|
|||
useEffect(() => { |
|||
if (creating && transactionStack && transactionStack[createPollCacheSendStackId] |
|||
&& transactions[transactionStack[createPollCacheSendStackId]]) { |
|||
if (transactions[transactionStack[createPollCacheSendStackId]].status === TRANSACTION_ERROR) { |
|||
setErrored(true); |
|||
setCreating(false); |
|||
onCreated(false); |
|||
} else if (transactions[transactionStack[createPollCacheSendStackId]].status === TRANSACTION_SUCCESS) { |
|||
const { |
|||
receipt: { |
|||
events: { |
|||
[POLL_CREATED_EVENT]: { |
|||
returnValues: { |
|||
topicID: topicId, |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
} = transactions[transactionStack[createPollCacheSendStackId]]; |
|||
|
|||
const pollsDb = Object.values(stores).find((store) => store.dbname === POLLS_DATABASE); |
|||
|
|||
pollsDb |
|||
.put(topicId, { [POLL_QUESTION]: question, [POLL_OPTIONS]: optionValues }) |
|||
.then(() => { |
|||
onCreated(topicId); |
|||
}) |
|||
.catch((reason) => { |
|||
console.error(reason); |
|||
setErrored(true); |
|||
setCreating(false); |
|||
onCreated(false); |
|||
}); |
|||
} |
|||
} |
|||
}, [createPollCacheSendStackId, creating, onCreated, optionValues, question, transactionStack, transactions]); |
|||
|
|||
return useMemo(() => ( |
|||
<div className="poll-create"> |
|||
<Form.Field required> |
|||
<label className="form-topic-create-header" htmlFor="form-poll-create-field-subject"> |
|||
{t('poll.create.question.field.label')} |
|||
</label> |
|||
<Input |
|||
id="form-poll-create-field-subject" |
|||
placeholder={t('poll.create.question.field.placeholder')} |
|||
name="pollQuestionInput" |
|||
className="form-input" |
|||
onChange={handlePollQuestionChange} |
|||
/> |
|||
</Form.Field> |
|||
<Form.Field> |
|||
<Checkbox |
|||
label={t('poll.create.allow.vote.changes.field.label')} |
|||
onClick={handleCheckboxChange} |
|||
/> |
|||
</Form.Field> |
|||
{pollOptions} |
|||
<Button |
|||
id="add-option-button" |
|||
key="form-add-option-button" |
|||
positive |
|||
icon |
|||
labelPosition="left" |
|||
onClick={addOption} |
|||
> |
|||
<Icon name="plus" /> |
|||
{t('poll.create.add.option.button')} |
|||
</Button> |
|||
</div> |
|||
), [addOption, handleCheckboxChange, handlePollQuestionChange, pollOptions, t]); |
|||
}); |
|||
|
|||
PollCreate.propTypes = { |
|||
onChange: PropTypes.func, |
|||
onCreated: PropTypes.func, |
|||
}; |
|||
|
|||
export default PollCreate; |
@ -0,0 +1,12 @@ |
|||
.poll-create { |
|||
padding-top: 1em; |
|||
padding-bottom: 1em; |
|||
} |
|||
|
|||
.poll-create .checkbox > label, |
|||
.poll-create .checkbox > label:focus, |
|||
.poll-create .checkbox > label:hover{ |
|||
color: white !important; |
|||
font-weight: 700; |
|||
} |
|||
|
@ -0,0 +1,23 @@ |
|||
import React from 'react'; |
|||
import { Container, Header, Icon } from 'semantic-ui-react'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
const PollDataInvalid = () => { |
|||
const { t } = useTranslation(); |
|||
|
|||
return ( |
|||
<Container id="topic-poll-data-invalid-container" textAlign="center"> |
|||
<Header as="h3" icon textAlign="center"> |
|||
<Icon name="warning sign" color="red" /> |
|||
<Header.Content> |
|||
{t('topic.poll.invalid.data.header')} |
|||
<Header.Subheader> |
|||
{t('topic.poll.invalid.data.sub.header')} |
|||
</Header.Subheader> |
|||
</Header.Content> |
|||
</Header> |
|||
</Container> |
|||
); |
|||
}; |
|||
|
|||
export default PollDataInvalid; |
@ -0,0 +1,67 @@ |
|||
import React, { useMemo } from 'react'; |
|||
import Chart from 'react-apexcharts'; |
|||
import PropTypes from 'prop-types'; |
|||
import { CHART_TYPE_BAR } from '../../../../constants/polls/PollGraph'; |
|||
|
|||
const PollChartBar = (props) => { |
|||
const { pollOptions, voteCounts, voterNames } = props; |
|||
|
|||
const chartOptions = useMemo(() => ({ |
|||
chart: { |
|||
id: 'chart-bar', |
|||
}, |
|||
theme: { |
|||
palette: 'palette8', |
|||
}, |
|||
plotOptions: { |
|||
bar: { |
|||
horizontal: true, |
|||
distributed: true, |
|||
}, |
|||
}, |
|||
xaxis: { |
|||
categories: pollOptions, |
|||
tickAmount: 1, |
|||
labels: { |
|||
formatter(val) { |
|||
if (val.toFixed) return val.toFixed(0); |
|||
return val; |
|||
}, |
|||
}, |
|||
}, |
|||
tooltip: { |
|||
enabled: true, |
|||
x: { |
|||
show: false, |
|||
}, |
|||
y: { |
|||
formatter(value, { dataPointIndex }) { |
|||
return `<div>${voterNames[dataPointIndex].join('</div><div>')}</div>`; |
|||
}, |
|||
title: { |
|||
formatter: () => null, |
|||
}, |
|||
}, |
|||
}, |
|||
}), [pollOptions, voterNames]); |
|||
|
|||
const chartSeries = useMemo(() => [{ |
|||
name: 'Votes', |
|||
data: voteCounts, |
|||
}], [voteCounts]); |
|||
|
|||
return ( |
|||
<Chart |
|||
options={chartOptions} |
|||
series={chartSeries} |
|||
type={CHART_TYPE_BAR} |
|||
/> |
|||
); |
|||
}; |
|||
|
|||
PollChartBar.propTypes = { |
|||
pollOptions: PropTypes.arrayOf(PropTypes.string).isRequired, |
|||
voteCounts: PropTypes.arrayOf(PropTypes.number).isRequired, |
|||
}; |
|||
|
|||
export default PollChartBar; |
@ -0,0 +1,63 @@ |
|||
import React, { useMemo } from 'react'; |
|||
import Chart from 'react-apexcharts'; |
|||
import PropTypes from 'prop-types'; |
|||
import { CHART_TYPE_DONUT } from '../../../../constants/polls/PollGraph'; |
|||
|
|||
const PollChartDonut = (props) => { |
|||
const { pollOptions, voteCounts, voterNames } = props; |
|||
|
|||
const chartOptions = useMemo(() => ({ |
|||
chart: { |
|||
id: 'chart-donut', |
|||
toolbar: { |
|||
show: true, |
|||
tools: { |
|||
download: true, |
|||
}, |
|||
}, |
|||
}, |
|||
theme: { |
|||
palette: 'palette8', |
|||
}, |
|||
plotOptions: { |
|||
pie: { |
|||
donut: { |
|||
labels: { |
|||
show: true, |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
labels: pollOptions, |
|||
tooltip: { |
|||
enabled: true, |
|||
fillSeriesColor: false, |
|||
y: { |
|||
formatter(value, { seriesIndex }) { |
|||
return `<div>${voterNames[seriesIndex].join('</div><div>')}</div>`; |
|||
}, |
|||
title: { |
|||
formatter: () => null, |
|||
}, |
|||
}, |
|||
}, |
|||
legend: { |
|||
position: 'bottom', |
|||
}, |
|||
}), [pollOptions, voterNames]); |
|||
|
|||
return ( |
|||
<Chart |
|||
options={chartOptions} |
|||
series={voteCounts} |
|||
type={CHART_TYPE_DONUT} |
|||
/> |
|||
); |
|||
}; |
|||
|
|||
PollChartDonut.propTypes = { |
|||
pollOptions: PropTypes.arrayOf(PropTypes.string).isRequired, |
|||
voteCounts: PropTypes.arrayOf(PropTypes.number).isRequired, |
|||
}; |
|||
|
|||
export default PollChartDonut; |
@ -0,0 +1,140 @@ |
|||
import React, { |
|||
useCallback, useEffect, useMemo, useState, |
|||
} from 'react'; |
|||
import { |
|||
Grid, |
|||
Header, Statistic, Tab, |
|||
} from 'semantic-ui-react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import PollChartBar from './PollChartBar'; |
|||
import PollChartDonut from './PollChartDonut'; |
|||
import './styles.css'; |
|||
import { CHART_TYPE_BAR, CHART_TYPE_DONUT } from '../../../constants/polls/PollGraph'; |
|||
|
|||
const PollGraph = (props) => { |
|||
const { |
|||
pollOptions, userVoteIndex, voteCounts, voterNames, |
|||
} = props; |
|||
const [totalVotes, setTotalVotes] = useState( |
|||
voteCounts.reduce((accumulator, voteCount) => accumulator + voteCount, 0), |
|||
); |
|||
const { t } = useTranslation(); |
|||
|
|||
useEffect(() => { |
|||
setTotalVotes(voteCounts.reduce((accumulator, voteCount) => accumulator + voteCount, 0)); |
|||
}, [voteCounts]); |
|||
|
|||
const footer = useMemo(() => ( |
|||
<div> |
|||
<Statistic size="mini"> |
|||
<Statistic.Value> |
|||
{ totalVotes } |
|||
</Statistic.Value> |
|||
<Statistic.Label> |
|||
{ totalVotes !== 1 ? t('topic.poll.tab.results.votes') : t('topic.poll.tab.results.vote') } |
|||
</Statistic.Label> |
|||
</Statistic> |
|||
{userVoteIndex !== -1 |
|||
&& ( |
|||
<div> |
|||
{t('topic.poll.tab.results.user.vote')} |
|||
<span className="poll-voted-option">{pollOptions[userVoteIndex]}</span> |
|||
</div> |
|||
)} |
|||
</div> |
|||
), [pollOptions, t, totalVotes, userVoteIndex]); |
|||
|
|||
function calculateChartBarWidth(nOptions) { |
|||
if (nOptions < 6) return 8; |
|||
if (nOptions < 10) return 10; |
|||
if (nOptions < 14) return 12; |
|||
if (nOptions < 18) return 14; |
|||
return 16; |
|||
} |
|||
|
|||
function calculateChartDonutWidth(nOptions) { |
|||
if (nOptions < 10) return 8; |
|||
if (nOptions < 16) return 10; |
|||
return 12; |
|||
} |
|||
|
|||
const generatePane = useCallback( |
|||
(type) => ( |
|||
<Tab.Pane attached={false}> |
|||
<Grid columns="equal"> |
|||
<Grid.Row> |
|||
<Grid.Column /> |
|||
<Grid.Column |
|||
width={type === CHART_TYPE_BAR |
|||
? calculateChartBarWidth(pollOptions.length) |
|||
: calculateChartDonutWidth(pollOptions.length)} |
|||
textAlign="center" |
|||
> |
|||
{type === CHART_TYPE_BAR |
|||
? ( |
|||
<PollChartBar |
|||
pollOptions={pollOptions} |
|||
voteCounts={voteCounts} |
|||
voterNames={voterNames} |
|||
/> |
|||
) : ( |
|||
<PollChartDonut |
|||
pollOptions={pollOptions} |
|||
voteCounts={voteCounts} |
|||
voterNames={voterNames} |
|||
/> |
|||
)} |
|||
</Grid.Column> |
|||
<Grid.Column /> |
|||
</Grid.Row> |
|||
<Grid.Row> |
|||
<Grid.Column textAlign="center"> |
|||
{footer} |
|||
</Grid.Column> |
|||
</Grid.Row> |
|||
</Grid> |
|||
</Tab.Pane> |
|||
), |
|||
[footer, pollOptions, voteCounts, voterNames], |
|||
); |
|||
|
|||
const panes = useMemo(() => { |
|||
const chartBarPane = generatePane(CHART_TYPE_BAR); |
|||
const chartDonutPane = generatePane(CHART_TYPE_DONUT); |
|||
|
|||
return ([ |
|||
{ menuItem: { key: 'chart-bar', icon: 'chart bar' }, render: () => chartBarPane }, |
|||
{ menuItem: { key: 'chart-donut', icon: 'chart pie' }, render: () => chartDonutPane }, |
|||
]); |
|||
}, [generatePane]); |
|||
return useMemo(() => ( |
|||
<> |
|||
{totalVotes > 0 |
|||
? ( |
|||
<Tab |
|||
menu={{ secondary: true }} |
|||
panes={panes} |
|||
/> |
|||
) |
|||
: ( |
|||
<Header as="h4" textAlign="center"> |
|||
{t('topic.poll.tab.results.no.votes')} |
|||
</Header> |
|||
)} |
|||
</> |
|||
), [panes, t, totalVotes]); |
|||
}; |
|||
|
|||
PollGraph.defaultProps = { |
|||
userVoteIndex: -1, |
|||
}; |
|||
|
|||
PollGraph.propTypes = { |
|||
pollOptions: PropTypes.arrayOf(PropTypes.string).isRequired, |
|||
voteCounts: PropTypes.arrayOf(PropTypes.number).isRequired, |
|||
voterNames: PropTypes.arrayOf(PropTypes.array).isRequired, |
|||
userVoteIndex: PropTypes.number, |
|||
}; |
|||
|
|||
export default PollGraph; |
@ -0,0 +1,10 @@ |
|||
#topic-poll-container .ui.segment.tab { |
|||
box-shadow: none; |
|||
border: none; |
|||
} |
|||
|
|||
.apexcharts-tooltip { |
|||
border: 1px solid #ddd !important; |
|||
background-color: white !important; |
|||
color: black !important; |
|||
} |
@ -0,0 +1,26 @@ |
|||
import React from 'react'; |
|||
import { Container, Header, Icon } from 'semantic-ui-react'; |
|||
import { Link } from 'react-router-dom'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
const PollGuestView = () => { |
|||
const { t } = useTranslation(); |
|||
|
|||
return ( |
|||
<Container id="topic-poll-guest-view-container" textAlign="center"> |
|||
<Header as="h3" icon textAlign="center"> |
|||
<Icon name="signup" /> |
|||
<Header.Content> |
|||
{t('topic.poll.guest.header')} |
|||
<Header.Subheader> |
|||
{t('topic.poll.guest.sub.header.pre')} |
|||
<Link to="/auth/register">{t('topic.poll.guest.sub.header.link')}</Link> |
|||
{t('topic.poll.guest.sub.header.post')} |
|||
</Header.Subheader> |
|||
</Header.Content> |
|||
</Header> |
|||
</Container> |
|||
); |
|||
}; |
|||
|
|||
export default PollGuestView; |
@ -0,0 +1,116 @@ |
|||
import React, { useEffect, useState } from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { useSelector } from 'react-redux'; |
|||
import { Button, Form } from 'semantic-ui-react'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import { VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; |
|||
import { drizzle } from '../../../redux/store'; |
|||
import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../../constants/TransactionStatus'; |
|||
import './styles.css'; |
|||
|
|||
const { contracts: { [VOTING_CONTRACT]: { methods: { vote } } } } = drizzle; |
|||
|
|||
const PollVote = (props) => { |
|||
const { |
|||
topicId, account, pollOptions, enableVoteChanges, hasUserVoted, userVoteIndex, |
|||
} = props; |
|||
const transactionStack = useSelector((state) => state.transactionStack); |
|||
const transactions = useSelector((state) => state.transactions); |
|||
const [voteCacheSendStackId, setVoteCacheSendStackId] = useState(''); |
|||
const [selectedOptionIndex, setSelectedOptionIndex] = useState(userVoteIndex); |
|||
const [voting, setVoting] = useState(false); |
|||
const { t } = useTranslation(); |
|||
|
|||
const onOptionSelected = (e, { value }) => { |
|||
setSelectedOptionIndex(value); |
|||
}; |
|||
|
|||
useEffect(() => { |
|||
setSelectedOptionIndex(userVoteIndex); |
|||
}, [userVoteIndex]); |
|||
|
|||
const onCastVote = () => { |
|||
setVoting(true); |
|||
setVoteCacheSendStackId(vote.cacheSend(...[topicId, selectedOptionIndex + 1], { from: account })); |
|||
}; |
|||
|
|||
const onUnvote = (e) => { |
|||
e.preventDefault(); |
|||
setVoting(true); |
|||
setVoteCacheSendStackId(vote.cacheSend(...[topicId, 0], { from: account })); |
|||
}; |
|||
|
|||
useEffect(() => { |
|||
if (voting && transactionStack && transactionStack[voteCacheSendStackId] |
|||
&& transactions[transactionStack[voteCacheSendStackId]]) { |
|||
if (transactions[transactionStack[voteCacheSendStackId]].status === TRANSACTION_ERROR |
|||
|| transactions[transactionStack[voteCacheSendStackId]].status === TRANSACTION_SUCCESS) { |
|||
setVoting(false); |
|||
} |
|||
} |
|||
}, [transactionStack, transactions, voteCacheSendStackId, voting]); |
|||
|
|||
if (hasUserVoted && !enableVoteChanges) { |
|||
return ( |
|||
<> |
|||
<div> |
|||
{t('topic.poll.tab.results.user.vote')} |
|||
<span className="poll-voted-option">{pollOptions[userVoteIndex]}</span> |
|||
</div> |
|||
<div> |
|||
{t('topic.poll.tab.vote.no.changes')} |
|||
</div> |
|||
</> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<Form onSubmit={onCastVote}> |
|||
<Form.Group grouped> |
|||
<label htmlFor="poll">{t('topic.poll.tab.vote.form.radio.label')}</label> |
|||
{pollOptions.map((pollOption, index) => ( |
|||
<Form.Radio |
|||
key={pollOption} |
|||
label={pollOption} |
|||
value={index} |
|||
className={index === userVoteIndex ? 'poll-voted-option' : null} |
|||
checked={index === selectedOptionIndex} |
|||
disabled={voting} |
|||
onChange={onOptionSelected} |
|||
/> |
|||
))} |
|||
</Form.Group> |
|||
<Button |
|||
type="submit" |
|||
className="primary-button" |
|||
disabled={voting || (selectedOptionIndex === userVoteIndex)} |
|||
> |
|||
{t('topic.poll.tab.vote.form.button.submit')} |
|||
</Button> |
|||
{hasUserVoted && enableVoteChanges |
|||
&& ( |
|||
<Button |
|||
type="submit" |
|||
disabled={voting} |
|||
onClick={onUnvote} |
|||
> |
|||
{t('topic.poll.tab.vote.form.button.unvote')} |
|||
</Button> |
|||
)} |
|||
</Form> |
|||
); |
|||
}; |
|||
|
|||
PollVote.defaultProps = { |
|||
userVoteIndex: -1, |
|||
}; |
|||
|
|||
PollVote.propTypes = { |
|||
topicId: PropTypes.number.isRequired, |
|||
pollOptions: PropTypes.arrayOf(PropTypes.string).isRequired, |
|||
enableVoteChanges: PropTypes.bool.isRequired, |
|||
hasUserVoted: PropTypes.bool.isRequired, |
|||
userVoteIndex: PropTypes.number, |
|||
}; |
|||
|
|||
export default PollVote; |
@ -0,0 +1,3 @@ |
|||
.poll-voted-option{ |
|||
font-weight: bold; |
|||
} |
@ -0,0 +1,211 @@ |
|||
import React, { |
|||
useEffect, useMemo, useState, |
|||
} from 'react'; |
|||
import { useDispatch, useSelector } from 'react-redux'; |
|||
import { VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; |
|||
import { |
|||
Container, Header, Icon, Loader, Tab, |
|||
} from 'semantic-ui-react'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import PropTypes from 'prop-types'; |
|||
import { POLLS_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases'; |
|||
import { breeze, drizzle } from '../../redux/store'; |
|||
import PollGraph from './PollGraph'; |
|||
import CustomLoadingTabPane from '../CustomLoadingTabPane'; |
|||
import { GRAPH_TAB, VOTE_TAB } from '../../constants/polls/PollTabs'; |
|||
import PollVote from './PollVote'; |
|||
import { FETCH_USER_DATABASE } from '../../redux/actions/peerDbReplicationActions'; |
|||
import { generatePollHash } from '../../utils/hashUtils'; |
|||
import { POLL_OPTIONS, POLL_QUESTION } from '../../constants/orbit/PollsDatabaseKeys'; |
|||
import PollDataInvalid from './PollDataInvalid'; |
|||
import PollGuestView from './PollGuestView'; |
|||
|
|||
const { |
|||
contracts: { |
|||
[VOTING_CONTRACT]: { |
|||
methods: { |
|||
getPoll: |
|||
{ cacheCall: getPollChainData, clearCacheCall: clearGetPollChainData }, |
|||
}, |
|||
}, |
|||
}, |
|||
} = drizzle; |
|||
const { orbit } = breeze; |
|||
|
|||
const PollView = (props) => { |
|||
const { topicId, topicAuthorAddress } = props; |
|||
const userAddress = useSelector((state) => state.user.address); |
|||
const hasSignedUp = useSelector((state) => state.user.hasSignedUp); |
|||
const getPollResults = useSelector((state) => state.contracts[VOTING_CONTRACT].getPoll); |
|||
const polls = useSelector((state) => state.orbitData.polls); |
|||
const [getPollCallHash, setGetPollCallHash] = useState(null); |
|||
const [pollHash, setPollHash] = useState(''); |
|||
const [pollChangeVoteEnabled, setPollChangeVoteEnabled] = useState(false); |
|||
const [pollOptions, setPollOptions] = useState([]); |
|||
const [voteCounts, setVoteCounts] = useState([]); |
|||
const [voters, setVoters] = useState([]); |
|||
const [voterNames, setVoterNames] = useState([]); |
|||
const [pollHashValid, setPollHashValid] = useState(true); |
|||
const [pollQuestion, setPollQuestion] = useState(''); |
|||
const [chainDataLoading, setChainDataLoading] = useState(true); |
|||
const [orbitDataLoading, setOrbitDataLoading] = useState(true); |
|||
const dispatch = useDispatch(); |
|||
const { t } = useTranslation(); |
|||
|
|||
useEffect(() => { |
|||
if (!getPollCallHash) { |
|||
setGetPollCallHash(getPollChainData(topicId)); |
|||
} |
|||
}, [getPollCallHash, topicId]); |
|||
|
|||
useEffect(() => { |
|||
dispatch({ |
|||
type: FETCH_USER_DATABASE, |
|||
orbit, |
|||
dbName: POLLS_DATABASE, |
|||
userAddress: topicAuthorAddress, |
|||
}); |
|||
}, [dispatch, topicAuthorAddress]); |
|||
|
|||
useEffect(() => { |
|||
if (getPollCallHash && getPollResults && getPollResults[getPollCallHash]) { |
|||
const pollResults = getPollResults[getPollCallHash]; |
|||
setPollHash(pollResults.value[1]); |
|||
setPollChangeVoteEnabled(pollResults.value[2]); |
|||
setVoteCounts(pollResults.value[4].map((voteCount) => parseInt(voteCount, 10))); |
|||
|
|||
const cumulativeSum = pollResults.value[4] |
|||
.map((voteCount) => parseInt(voteCount, 10)) |
|||
.reduce((accumulator, voteCount) => (accumulator.length === 0 |
|||
? [voteCount] |
|||
: [...accumulator, accumulator[accumulator.length - 1] + voteCount]), []); |
|||
|
|||
setVoters(cumulativeSum |
|||
.map((subArrayEnd, index) => pollResults.value[5] |
|||
.slice(index > 0 ? cumulativeSum[index - 1] : 0, |
|||
subArrayEnd))); |
|||
|
|||
setVoterNames(cumulativeSum |
|||
.map((subArrayEnd, index) => pollResults.value[6] |
|||
.slice(index > 0 ? cumulativeSum[index - 1] : 0, |
|||
subArrayEnd))); |
|||
|
|||
setChainDataLoading(false); |
|||
} |
|||
}, [getPollCallHash, getPollResults]); |
|||
|
|||
useEffect(() => { |
|||
const pollFound = polls |
|||
.find((poll) => poll.id === topicId); |
|||
|
|||
if (pollHash && pollFound) { |
|||
if (generatePollHash(pollFound[POLL_QUESTION], pollFound[POLL_OPTIONS]) === pollHash) { |
|||
setPollHashValid(true); |
|||
setPollQuestion(pollFound[POLL_QUESTION]); |
|||
setPollOptions([...pollFound[POLL_OPTIONS]]); |
|||
} else { |
|||
setPollHashValid(false); |
|||
} |
|||
|
|||
setOrbitDataLoading(false); |
|||
} |
|||
}, [pollHash, polls, topicId]); |
|||
|
|||
const userHasVoted = useMemo(() => hasSignedUp && voters |
|||
.some((optionVoters) => optionVoters.includes(userAddress)), |
|||
[hasSignedUp, userAddress, voters]); |
|||
|
|||
const userVoteIndex = useMemo(() => { |
|||
if (!chainDataLoading && !orbitDataLoading && userHasVoted) { |
|||
return voters |
|||
.findIndex((optionVoters) => optionVoters.includes(userAddress)); |
|||
} |
|||
|
|||
return -1; |
|||
}, [chainDataLoading, orbitDataLoading, userAddress, userHasVoted, voters]); |
|||
|
|||
const pollVoteTab = useMemo(() => { |
|||
if (!hasSignedUp) { |
|||
return <PollGuestView />; |
|||
} |
|||
|
|||
if (chainDataLoading || orbitDataLoading) { |
|||
return null; |
|||
} |
|||
|
|||
return ( |
|||
<PollVote |
|||
topicId={topicId} |
|||
pollOptions={pollOptions} |
|||
enableVoteChanges={pollChangeVoteEnabled} |
|||
hasUserVoted={userHasVoted} |
|||
userVoteIndex={userVoteIndex} |
|||
/> |
|||
); |
|||
}, [ |
|||
chainDataLoading, hasSignedUp, orbitDataLoading, pollChangeVoteEnabled, pollOptions, topicId, userHasVoted, |
|||
userVoteIndex, |
|||
]); |
|||
|
|||
const pollGraphTab = useMemo(() => ( |
|||
!chainDataLoading || orbitDataLoading |
|||
? ( |
|||
<PollGraph |
|||
pollOptions={pollOptions} |
|||
voteCounts={voteCounts} |
|||
userVoteIndex={userVoteIndex} |
|||
voterNames={voterNames} |
|||
/> |
|||
) |
|||
: null |
|||
), [chainDataLoading, orbitDataLoading, pollOptions, userVoteIndex, voteCounts, voterNames]); |
|||
|
|||
const panes = useMemo(() => { |
|||
const pollVotePane = ( |
|||
<CustomLoadingTabPane loading={chainDataLoading || orbitDataLoading}> |
|||
{pollVoteTab} |
|||
</CustomLoadingTabPane> |
|||
); |
|||
const pollGraphPane = ( |
|||
<CustomLoadingTabPane loading={chainDataLoading || orbitDataLoading}> |
|||
{pollGraphTab} |
|||
</CustomLoadingTabPane> |
|||
); |
|||
|
|||
return ([ |
|||
{ menuItem: t(GRAPH_TAB.intl_display_name_id), render: () => pollGraphPane }, |
|||
{ menuItem: t(VOTE_TAB.intl_display_name_id), render: () => pollVotePane }, |
|||
]); |
|||
}, [chainDataLoading, orbitDataLoading, pollGraphTab, pollVoteTab, t]); |
|||
|
|||
useEffect(() => () => { |
|||
clearGetPollChainData(); |
|||
}, []); |
|||
|
|||
return ( |
|||
<Container id="topic-poll-container" textAlign="left"> |
|||
{pollHashValid |
|||
? ( |
|||
<> |
|||
{!chainDataLoading && !orbitDataLoading ? ( |
|||
<> |
|||
<Header as="h3"> |
|||
<Icon name="chart area" size="large" /> |
|||
{pollQuestion} |
|||
</Header> |
|||
<Tab panes={panes} /> |
|||
</> |
|||
) : <Loader active inline="centered" />} |
|||
</> |
|||
) |
|||
: <PollDataInvalid />} |
|||
</Container> |
|||
); |
|||
}; |
|||
|
|||
PollView.propTypes = { |
|||
topicId: PropTypes.number.isRequired, |
|||
topicAuthorAddress: PropTypes.string, |
|||
}; |
|||
|
|||
export default PollView; |
@ -0,0 +1,162 @@ |
|||
import React, { |
|||
memo, useCallback, useEffect, useState, |
|||
} from 'react'; |
|||
import { |
|||
Button, Feed, Form, Icon, TextArea, |
|||
} from 'semantic-ui-react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import { useDispatch, useSelector } from 'react-redux'; |
|||
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; |
|||
import { POSTS_DATABASE, USER_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases'; |
|||
import { POST_CREATED_EVENT } from 'concordia-shared/src/constants/contracts/events/ForumContractEvents'; |
|||
import determineKVAddress from '../../utils/orbitUtils'; |
|||
import { FETCH_USER_DATABASE } from '../../redux/actions/peerDbReplicationActions'; |
|||
import { breeze, drizzle } from '../../redux/store'; |
|||
import './styles.css'; |
|||
import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../constants/TransactionStatus'; |
|||
import { POST_CONTENT } from '../../constants/orbit/PostsDatabaseKeys'; |
|||
|
|||
const { contracts: { [FORUM_CONTRACT]: { methods: { createPost } } } } = drizzle; |
|||
const { orbit } = breeze; |
|||
|
|||
const PostCreate = (props) => { |
|||
const { |
|||
topicId, initialPostSubject, account, |
|||
} = props; |
|||
const transactionStack = useSelector((state) => state.transactionStack); |
|||
const transactions = useSelector((state) => state.transactions); |
|||
const [postContent, setPostContent] = useState(''); |
|||
const [createPostCacheSendStackId, setCreatePostCacheSendStackId] = useState(''); |
|||
const [posting, setPosting] = useState(false); |
|||
const [storingPost, setStoringPost] = useState(false); |
|||
const userAddress = useSelector((state) => state.user.address); |
|||
const users = useSelector((state) => state.orbitData.users); |
|||
const dispatch = useDispatch(); |
|||
const { t } = useTranslation(); |
|||
|
|||
useEffect(() => { |
|||
if (userAddress) { |
|||
determineKVAddress({ orbit, dbName: USER_DATABASE, userAddress }) |
|||
.then((userOrbitAddress) => { |
|||
const userFound = users |
|||
.find((user) => user.id === userOrbitAddress); |
|||
|
|||
if (!userFound) { |
|||
dispatch({ |
|||
type: FETCH_USER_DATABASE, |
|||
orbit, |
|||
dbName: USER_DATABASE, |
|||
userAddress, |
|||
}); |
|||
} |
|||
}) |
|||
.catch((error) => { |
|||
console.error('Error during determination of key-value DB address:', error); |
|||
}); |
|||
} |
|||
}, [dispatch, userAddress, users]); |
|||
|
|||
const handleInputChange = useCallback((event) => { |
|||
if (posting) { |
|||
return; |
|||
} |
|||
|
|||
switch (event.target.name) { |
|||
case 'postContent': |
|||
setPostContent(event.target.value); |
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
}, [posting]); |
|||
|
|||
useEffect(() => { |
|||
if (posting && !storingPost && transactionStack && transactionStack[createPostCacheSendStackId] |
|||
&& transactions[transactionStack[createPostCacheSendStackId]]) { |
|||
if (transactions[transactionStack[createPostCacheSendStackId]].status === TRANSACTION_ERROR) { |
|||
setPosting(false); |
|||
} else if (transactions[transactionStack[createPostCacheSendStackId]].status === TRANSACTION_SUCCESS) { |
|||
const { |
|||
receipt: { events: { [POST_CREATED_EVENT]: { returnValues: { postID: contractPostId } } } }, |
|||
} = transactions[transactionStack[createPostCacheSendStackId]]; |
|||
|
|||
const { stores } = orbit; |
|||
const postsDb = Object.values(stores).find((store) => store.dbname === POSTS_DATABASE); |
|||
|
|||
postsDb |
|||
.put(contractPostId, { |
|||
[POST_CONTENT]: postContent, |
|||
}) |
|||
.then(() => { |
|||
setPostContent(''); |
|||
setPosting(false); |
|||
setStoringPost(false); |
|||
setCreatePostCacheSendStackId(''); |
|||
}) |
|||
.catch((reason) => { |
|||
console.error(reason); |
|||
}); |
|||
|
|||
setStoringPost(true); |
|||
} |
|||
} |
|||
}, [ |
|||
createPostCacheSendStackId, initialPostSubject, postContent, posting, storingPost, transactionStack, |
|||
transactions, |
|||
]); |
|||
|
|||
const savePost = useCallback(() => { |
|||
if (postContent === '') { |
|||
return; |
|||
} |
|||
|
|||
setPosting(true); |
|||
setCreatePostCacheSendStackId(createPost.cacheSend(...[topicId], { from: account })); |
|||
}, [account, postContent, topicId]); |
|||
|
|||
return ( |
|||
<Feed> |
|||
<Feed.Event> |
|||
<Feed.Content> |
|||
<Feed.Summary> |
|||
<Form> |
|||
<TextArea |
|||
placeholder={t('post.form.content.field.placeholder')} |
|||
name="postContent" |
|||
size="mini" |
|||
rows={4} |
|||
value={postContent} |
|||
onChange={handleInputChange} |
|||
/> |
|||
</Form> |
|||
</Feed.Summary> |
|||
<Feed.Meta id="post-button-div"> |
|||
<Feed.Like> |
|||
<Button |
|||
animated |
|||
type="button" |
|||
className="primary-button" |
|||
disabled={posting || postContent === ''} |
|||
onClick={savePost} |
|||
> |
|||
<Button.Content visible> |
|||
{t('post.create.form.send.button')} |
|||
</Button.Content> |
|||
<Button.Content hidden> |
|||
<Icon name="send" /> |
|||
</Button.Content> |
|||
</Button> |
|||
</Feed.Like> |
|||
</Feed.Meta> |
|||
</Feed.Content> |
|||
</Feed.Event> |
|||
</Feed> |
|||
); |
|||
}; |
|||
|
|||
PostCreate.propTypes = { |
|||
topicId: PropTypes.number.isRequired, |
|||
}; |
|||
|
|||
export default memo(PostCreate); |
@ -0,0 +1,18 @@ |
|||
.post-summary-meta-index { |
|||
float: right; |
|||
font-size: 12px; |
|||
opacity: 0.4; |
|||
} |
|||
|
|||
.like:hover .icon { |
|||
color: #fff !important; |
|||
} |
|||
|
|||
#post-button-div { |
|||
float: right; |
|||
margin: 1rem 0 4rem 0; |
|||
} |
|||
|
|||
#post-button-div button { |
|||
margin: 0; |
|||
} |
@ -0,0 +1,260 @@ |
|||
import React, { |
|||
memo, useEffect, useMemo, useState, useCallback, |
|||
} from 'react'; |
|||
import { |
|||
Dimmer, Feed, Form, Icon, Placeholder, Ref, TextArea, |
|||
} from 'semantic-ui-react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import TimeAgo from 'react-timeago'; |
|||
import ReactMarkdown from 'react-markdown'; |
|||
import { useDispatch, useSelector } from 'react-redux'; |
|||
import { Link } from 'react-router-dom'; |
|||
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; |
|||
import { POSTS_DATABASE, USER_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases'; |
|||
import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions'; |
|||
import { breeze, drizzle } from '../../../redux/store'; |
|||
import determineKVAddress from '../../../utils/orbitUtils'; |
|||
import { POST_CONTENT } from '../../../constants/orbit/PostsDatabaseKeys'; |
|||
import ProfileImage from '../../ProfileImage'; |
|||
import PostVoting from '../PostVoting'; |
|||
import targetBlank from '../../../utils/markdownUtils'; |
|||
import './styles.css'; |
|||
|
|||
const { orbit } = breeze; |
|||
|
|||
const { |
|||
contracts: { [FORUM_CONTRACT]: { methods: { getPost: { clearCacheCall: clearGetPostChainData } } } }, |
|||
} = drizzle; |
|||
|
|||
const PostListRow = (props) => { |
|||
const { |
|||
id: postId, postIndex, postCallHash, loading, focus, |
|||
} = props; |
|||
const getPostResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getPost); |
|||
const [postAuthorAddress, setPostAuthorAddress] = useState(null); |
|||
const [postAuthor, setPostAuthor] = useState(null); |
|||
const [timeAgo, setTimeAgo] = useState(null); |
|||
const [topicId, setTopicId] = useState(null); |
|||
const [postContent, setPostContent] = useState(null); |
|||
const [postAuthorMeta, setPostAuthorMeta] = useState(null); |
|||
const [editing, setEditing] = useState(false); |
|||
const [editedPostContent, setEditedPostContent] = useState(null); |
|||
const userAddress = useSelector((state) => state.user.address); |
|||
const posts = useSelector((state) => state.orbitData.posts); |
|||
const users = useSelector((state) => state.orbitData.users); |
|||
const dispatch = useDispatch(); |
|||
const { t } = useTranslation(); |
|||
|
|||
useEffect(() => { |
|||
if (!loading && postCallHash && getPostResults[postCallHash] !== undefined) { |
|||
setPostAuthorAddress(getPostResults[postCallHash].value[0]); |
|||
setPostAuthor(getPostResults[postCallHash].value[1]); |
|||
setTimeAgo(getPostResults[postCallHash].value[2] * 1000); |
|||
setTopicId(getPostResults[postCallHash].value[3]); |
|||
} |
|||
}, [getPostResults, loading, postCallHash]); |
|||
|
|||
useEffect(() => { |
|||
if (postAuthorAddress && userAddress !== postAuthorAddress) { |
|||
dispatch({ |
|||
type: FETCH_USER_DATABASE, |
|||
orbit, |
|||
dbName: POSTS_DATABASE, |
|||
userAddress: postAuthorAddress, |
|||
}); |
|||
|
|||
dispatch({ |
|||
type: FETCH_USER_DATABASE, |
|||
orbit, |
|||
dbName: USER_DATABASE, |
|||
userAddress: postAuthorAddress, |
|||
}); |
|||
} |
|||
}, [dispatch, postAuthorAddress, userAddress]); |
|||
|
|||
useEffect(() => { |
|||
const postFound = posts |
|||
.find((post) => post.id === postId); |
|||
|
|||
if (postFound) { |
|||
setPostContent(postFound[POST_CONTENT]); |
|||
} |
|||
}, [postId, posts]); |
|||
|
|||
useEffect(() => { |
|||
if (postAuthorAddress !== null) { |
|||
determineKVAddress({ orbit, dbName: USER_DATABASE, userAddress: postAuthorAddress }) |
|||
.then((userOrbitAddress) => { |
|||
const userFound = users |
|||
.find((user) => user.id === userOrbitAddress); |
|||
|
|||
if (userFound) { |
|||
setPostAuthorMeta(userFound); |
|||
} |
|||
}) |
|||
.catch((error) => { |
|||
console.error('Error during determination of key-value DB address:', error); |
|||
}); |
|||
} |
|||
}, [postAuthorAddress, users]); |
|||
|
|||
const focusRef = useCallback((node) => { |
|||
if (focus && node !== null) { |
|||
node.scrollIntoView({ behavior: 'smooth' }); |
|||
} |
|||
}, [focus]); |
|||
|
|||
// ---------- Post Editing ----------------- |
|||
|
|||
const editPost = useCallback(() => { |
|||
setEditedPostContent(postContent); |
|||
setEditing(true); |
|||
}, [postContent]); |
|||
|
|||
const discardChanges = useCallback(() => { |
|||
setEditing(false); |
|||
}, []); |
|||
|
|||
const saveChanges = useCallback(() => { |
|||
setEditing(false); |
|||
if (editedPostContent !== postContent) { |
|||
const { stores } = orbit; |
|||
const postsDb = Object.values(stores).find((store) => store.dbname === POSTS_DATABASE); |
|||
postsDb |
|||
.put(postId.toString(), { |
|||
[POST_CONTENT]: editedPostContent, |
|||
}) |
|||
.catch((reason) => { |
|||
console.error(reason); |
|||
}); |
|||
} |
|||
}, [editedPostContent, postContent, postId]); |
|||
|
|||
const editPostButtons = useMemo(() => { |
|||
if (postContent !== null && userAddress === postAuthorAddress) { |
|||
if (editing) { |
|||
return ( |
|||
<> |
|||
<Icon |
|||
className="post-list-edit-button" |
|||
name="check" |
|||
color="green" |
|||
fitted |
|||
onClick={saveChanges} |
|||
/> |
|||
<Icon |
|||
className="post-list-edit-button" |
|||
name="x" |
|||
color="red" |
|||
fitted |
|||
onClick={discardChanges} |
|||
/> |
|||
</> |
|||
); |
|||
} |
|||
return ( |
|||
<Icon |
|||
className="post-list-edit-button" |
|||
name="pencil" |
|||
fitted |
|||
onClick={editPost} |
|||
/> |
|||
); |
|||
} |
|||
return null; |
|||
}, [discardChanges, editPost, editing, postAuthorAddress, postContent, saveChanges, userAddress]); |
|||
|
|||
const postContentArea = useMemo(() => { |
|||
const handleInputChange = (event, { value }) => { |
|||
setEditedPostContent(value); |
|||
}; |
|||
|
|||
if (postContent !== null) { |
|||
if (!editing) { |
|||
return ( |
|||
<ReactMarkdown |
|||
source={postContent} |
|||
renderers={{ |
|||
link: targetBlank(), |
|||
linkReference: targetBlank(), |
|||
}} |
|||
/> |
|||
); |
|||
} |
|||
return ( |
|||
<Form> |
|||
<TextArea |
|||
value={editedPostContent} |
|||
onChange={handleInputChange} |
|||
/> |
|||
</Form> |
|||
); |
|||
} |
|||
return (<Placeholder><Placeholder.Line length="long" /></Placeholder>); |
|||
}, [editedPostContent, editing, postContent]); |
|||
|
|||
useEffect(() => () => clearGetPostChainData(postId), [postId]); |
|||
|
|||
return useMemo(() => ( |
|||
<Dimmer.Dimmable |
|||
as={Feed.Event} |
|||
blurring |
|||
dimmed={loading} |
|||
id={`post-${postId}`} |
|||
className="post-list-row" |
|||
> |
|||
<Ref innerRef={focusRef}> |
|||
<Feed.Label className="post-profile-picture"> |
|||
<ProfileImage |
|||
profileAddress={postAuthorAddress} |
|||
profileUsername={postAuthor} |
|||
profileUserMeta={postAuthorMeta} |
|||
size="42" |
|||
link |
|||
/> |
|||
</Feed.Label> |
|||
</Ref> |
|||
<Feed.Content className="post-content"> |
|||
<Feed.Summary> |
|||
<Link to={`/topics/${topicId}/#post-${postId}`}> |
|||
<span className="post-summary-meta-index"> |
|||
{t('post.list.row.post.id', { id: postIndex })} |
|||
</span> |
|||
</Link> |
|||
{postAuthor !== null && setPostAuthorAddress !== null && timeAgo !== null |
|||
? ( |
|||
<> |
|||
<Feed.User as={Link} to={`/users/${postAuthorAddress}`}>{postAuthor}</Feed.User> |
|||
<Feed.Date> |
|||
<TimeAgo date={timeAgo} /> |
|||
</Feed.Date> |
|||
{editPostButtons} |
|||
</> |
|||
) |
|||
: <Placeholder><Placeholder.Line length="medium" /></Placeholder>} |
|||
</Feed.Summary> |
|||
<Feed.Extra> |
|||
{postContentArea} |
|||
</Feed.Extra> |
|||
<PostVoting postId={postId} postAuthorAddress={postAuthorAddress} /> |
|||
</Feed.Content> |
|||
</Dimmer.Dimmable> |
|||
), [editPostButtons, focusRef, loading, postAuthor, postAuthorAddress, postAuthorMeta, |
|||
postContentArea, postId, postIndex, t, timeAgo, topicId]); |
|||
}; |
|||
|
|||
PostListRow.defaultProps = { |
|||
loading: false, |
|||
focus: false, |
|||
}; |
|||
|
|||
PostListRow.propTypes = { |
|||
id: PropTypes.number.isRequired, |
|||
postIndex: PropTypes.number.isRequired, |
|||
postCallHash: PropTypes.string, |
|||
loading: PropTypes.bool, |
|||
focus: PropTypes.bool, |
|||
}; |
|||
|
|||
export default memo(PostListRow); |