@ -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 |
* 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 |
||||
/node_modules |
/node_modules |
||||
package-lock.json |
packages/*/node_modules |
||||
|
packages/concordia-contracts/build |
||||
|
|
||||
# Testing |
# IDE |
||||
/coverage |
.DS_Store |
||||
|
.idea |
||||
|
|
||||
# Production |
# Build Directories |
||||
/build |
/build |
||||
/src/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 |
# Misc |
||||
.DS_Store |
|
||||
.env.local |
.env.local |
||||
.env.development.local |
.env.development.local |
||||
.env.test.local |
.env.test.local |
||||
.env.production.local |
.env.production.local |
||||
|
|
||||
npm-debug.log* |
# Lerna |
||||
yarn-debug.log* |
*.lerna_backup |
||||
yarn-error.log* |
|
||||
|
|
||||
# Jetbrains |
yarn-clean.sh |
||||
.idea |
|
||||
|
@ -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", |
"name": "concordia", |
||||
"version": "0.1.0", |
|
||||
"private": true, |
"private": true, |
||||
"repository": { |
"workspaces": { |
||||
"type": "git", |
"packages": [ |
||||
"url": "https://gitlab.com/Ezerous/Apella.git" |
"packages/*" |
||||
}, |
], |
||||
"devDependencies": { |
"nohoist": [ |
||||
"truffle-contract": "^3.0.4" |
"**/web3", |
||||
}, |
"**/web3/**" |
||||
"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" |
|
||||
} |
} |
||||
} |
} |
||||
|
@ -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); |