initial commit
This commit is contained in:
commit
d25e5ba718
|
@ -0,0 +1,29 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.embark
|
||||
embarkArtifacts
|
||||
/dist
|
||||
chains.json
|
||||
config/livenet/password
|
||||
config/production/password
|
|
@ -0,0 +1,107 @@
|
|||
pragma solidity ^0.5.0;
|
||||
|
||||
// @notice Contract to create posts
|
||||
contract DReddit {
|
||||
|
||||
enum Ballot { NONE, UPVOTE, DOWNVOTE }
|
||||
|
||||
struct Post {
|
||||
uint creationDate;
|
||||
string description;
|
||||
address owner;
|
||||
uint upvotes;
|
||||
uint downvotes;
|
||||
mapping(address => Ballot) voters;
|
||||
}
|
||||
|
||||
Post[] public posts;
|
||||
|
||||
event NewPost (
|
||||
uint indexed postId,
|
||||
address owner,
|
||||
string description
|
||||
);
|
||||
|
||||
event Vote(
|
||||
uint indexed postId,
|
||||
address voter,
|
||||
uint8 vote
|
||||
);
|
||||
|
||||
// @notice Number of posts created
|
||||
// @return Num of posts
|
||||
function numPosts()
|
||||
public
|
||||
view
|
||||
returns(uint)
|
||||
{
|
||||
return posts.length;
|
||||
}
|
||||
|
||||
// @notice Create Post
|
||||
// @param _ipfsHash IPFS hash of the content of the post
|
||||
function create(string memory _ipfsHash) public
|
||||
{
|
||||
require(bytes(_ipfsHash).length > 0, "IPFS hash is required");
|
||||
|
||||
posts.push(Post({
|
||||
creationDate: now,
|
||||
description: _ipfsHash,
|
||||
owner: msg.sender,
|
||||
upvotes: 0,
|
||||
downvotes: 0
|
||||
}));
|
||||
|
||||
emit NewPost(posts.length - 1, msg.sender, _ipfsHash);
|
||||
}
|
||||
|
||||
// @notice Vote on a post
|
||||
// @param _postId Id of the post to up/downvote
|
||||
// @param _vote Vote selection: 0 -> none, 1 -> upvote, 2 -> downvote
|
||||
function vote(uint _postId, uint8 _vote)
|
||||
public
|
||||
{
|
||||
Post storage p = posts[_postId];
|
||||
require(p.creationDate != 0, 'Post does not exist.');
|
||||
require(p.voters[msg.sender] == Ballot.NONE, 'You already voted on this post.');
|
||||
|
||||
Ballot b = Ballot(_vote);
|
||||
require(b != Ballot.NONE, 'Invalid vote');
|
||||
|
||||
if (b == Ballot.UPVOTE) {
|
||||
p.upvotes++;
|
||||
} else {
|
||||
p.downvotes++;
|
||||
}
|
||||
p.voters[msg.sender] = b;
|
||||
|
||||
emit Vote(_postId, msg.sender, _vote);
|
||||
}
|
||||
|
||||
// @notice Determine if the sender can vote on a post
|
||||
// @param _postId Id of the post
|
||||
// @return bool that indicates if the sender can vote or not
|
||||
function canVote(uint _postId)
|
||||
public
|
||||
view
|
||||
returns (bool)
|
||||
{
|
||||
if (_postId > posts.length - 1) return false;
|
||||
|
||||
Post storage p = posts[_postId];
|
||||
return (p.voters[msg.sender] == Ballot.NONE);
|
||||
}
|
||||
|
||||
// @notice Obtain vote for specific post
|
||||
// @param _postId Id of the post
|
||||
// @return uint that represents the vote: 0 -> none, 1 -> upvote, 2 -> downvote
|
||||
function getVote(uint _postId)
|
||||
public
|
||||
view
|
||||
returns (uint8)
|
||||
{
|
||||
Post storage p = posts[_postId];
|
||||
return uint8(p.voters[msg.sender]);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"options": {
|
||||
"solc": {
|
||||
"optimize": true,
|
||||
"optimize-runs": 200
|
||||
}
|
||||
},
|
||||
"app": {},
|
||||
"contracts": [
|
||||
"contracts/**"
|
||||
],
|
||||
"buildDir": "dist/",
|
||||
"config": "embarkConfig/",
|
||||
"versions": {
|
||||
"web3": "1.0.0-beta",
|
||||
"solc": "0.5.0",
|
||||
"ipfs-api": "17.2.4"
|
||||
},
|
||||
"plugins": {
|
||||
"embarkjs-connector-web3": {}
|
||||
},
|
||||
"generationDir": "src/embarkArtifacts"
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
module.exports = {
|
||||
// applies to all environments
|
||||
default: {
|
||||
enabled: true,
|
||||
rpcHost: "localhost", // HTTP-RPC server listening interface (default: "localhost")
|
||||
rpcPort: 8545, // HTTP-RPC server listening port (default: 8545)
|
||||
rpcCorsDomain: "auto", // Comma separated list of domains from which to accept cross origin requests (browser enforced)
|
||||
// When set to "auto", Embark will automatically set the cors to the address of the webserver
|
||||
wsRPC: true, // Enable the WS-RPC server
|
||||
wsOrigins: "auto", // Origins from which to accept websockets requests
|
||||
// When set to "auto", Embark will automatically set the cors to the address of the webserver
|
||||
wsHost: "localhost", // WS-RPC server listening interface (default: "localhost")
|
||||
wsPort: 8546 // WS-RPC server listening port (default: 8546)
|
||||
|
||||
// Accounts to use as node accounts
|
||||
// The order here corresponds to the order of `web3.eth.getAccounts`, so the first one is the `defaultAccount`
|
||||
/*,accounts: [
|
||||
{
|
||||
nodeAccounts: true, // Accounts use for the node
|
||||
numAddresses: "1", // Number of addresses/accounts (defaults to 1)
|
||||
password: "config/development/devpassword" // Password file for the accounts
|
||||
},
|
||||
// Below are additional accounts that will count as `nodeAccounts` in the `deployment` section of your contract config
|
||||
// Those will not be unlocked in the node itself
|
||||
{
|
||||
privateKey: "your_private_key"
|
||||
},
|
||||
{
|
||||
privateKeyFile: "path/to/file", // Either a keystore or a list of keys, separated by , or ;
|
||||
password: "passwordForTheKeystore" // Needed to decrypt the keystore file
|
||||
},
|
||||
{
|
||||
mnemonic: "12 word mnemonic",
|
||||
addressIndex: "0", // Optionnal. The index to start getting the address
|
||||
numAddresses: "1", // Optionnal. The number of addresses to get
|
||||
hdpath: "m/44'/60'/0'/0/" // Optionnal. HD derivation path
|
||||
}
|
||||
]*/
|
||||
},
|
||||
|
||||
// default environment, merges with the settings in default
|
||||
// assumed to be the intended environment by `embark run` and `embark blockchain`
|
||||
development: {
|
||||
ethereumClientName: "geth", // Can be geth or parity (default:geth)
|
||||
//ethereumClientBin: "geth", // path to the client binary. Useful if it is not in the global PATH
|
||||
networkType: "custom", // Can be: testnet, rinkeby, livenet or custom, in which case, it will use the specified networkId
|
||||
networkId: 1337, // Network id used when networkType is custom
|
||||
isDev: true, // Uses and ephemeral proof-of-authority network with a pre-funded developer account, mining enabled
|
||||
datadir: ".embark/development/datadir", // Data directory for the databases and keystore (Geth 1.8.15 and Parity 2.0.4 can use the same base folder, till now they does not conflict with each other)
|
||||
mineWhenNeeded: true, // Uses our custom script (if isDev is false) to mine only when needed
|
||||
nodiscover: true, // Disables the peer discovery mechanism (manual peer addition)
|
||||
maxpeers: 0, // Maximum number of network peers (network disabled if set to 0) (default: 25)
|
||||
proxy: true, // Proxy is used to present meaningful information about transactions
|
||||
targetGasLimit: 8000000, // Target gas limit sets the artificial target gas floor for the blocks to mine
|
||||
simulatorBlocktime: 0 // Specify blockTime in seconds for automatic mining. Default is 0 and no auto-mining.
|
||||
},
|
||||
|
||||
// merges with the settings in default
|
||||
// used with "embark run privatenet" and/or "embark blockchain privatenet"
|
||||
privatenet: {
|
||||
networkType: "custom",
|
||||
networkId: 1337,
|
||||
isDev: false,
|
||||
datadir: ".embark/privatenet/datadir",
|
||||
// -- mineWhenNeeded --
|
||||
// This options is only valid when isDev is false.
|
||||
// Enabling this option uses our custom script to mine only when needed.
|
||||
// Embark creates a development account for you (using `geth account new`) and funds the account. This account can be used for
|
||||
// development (and even imported in to MetaMask). To enable correct usage, a password for this account must be specified
|
||||
// in the `account > password` setting below.
|
||||
// NOTE: once `mineWhenNeeded` is enabled, you must run an `embark reset` on your dApp before running
|
||||
// `embark blockchain` or `embark run` for the first time.
|
||||
mineWhenNeeded: true,
|
||||
// -- genesisBlock --
|
||||
// This option is only valid when mineWhenNeeded is true (which is only valid if isDev is false).
|
||||
// When enabled, geth uses POW to mine transactions as it would normally, instead of using POA as it does in --dev mode.
|
||||
// On the first `embark blockchain or embark run` after this option is enabled, geth will create a new chain with a
|
||||
// genesis block, which can be configured using the `genesisBlock` configuration option below.
|
||||
genesisBlock: "config/privatenet/genesis.json", // Genesis block to initiate on first creation of a development node
|
||||
nodiscover: true,
|
||||
maxpeers: 0,
|
||||
proxy: true,
|
||||
accounts: [
|
||||
{
|
||||
nodeAccounts: true,
|
||||
password: "config/privatenet/password" // Password to unlock the account
|
||||
}
|
||||
],
|
||||
targetGasLimit: 8000000,
|
||||
simulatorBlocktime: 0
|
||||
},
|
||||
|
||||
privateparitynet: {
|
||||
ethereumClientName: "parity",
|
||||
networkType: "custom",
|
||||
networkId: 1337,
|
||||
isDev: false,
|
||||
genesisBlock: "config/privatenet/genesis-parity.json", // Genesis block to initiate on first creation of a development node
|
||||
datadir: ".embark/privatenet/datadir",
|
||||
mineWhenNeeded: false,
|
||||
nodiscover: true,
|
||||
maxpeers: 0,
|
||||
proxy: true,
|
||||
accounts: [
|
||||
{
|
||||
nodeAccounts: true,
|
||||
password: "config/privatenet/password"
|
||||
}
|
||||
],
|
||||
targetGasLimit: 8000000,
|
||||
simulatorBlocktime: 0
|
||||
},
|
||||
|
||||
// merges with the settings in default
|
||||
// used with "embark run testnet" and/or "embark blockchain testnet"
|
||||
testnet: {
|
||||
networkType: "testnet",
|
||||
syncMode: "light",
|
||||
accounts: [
|
||||
{
|
||||
nodeAccounts: true,
|
||||
password: "config/testnet/password"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// merges with the settings in default
|
||||
// used with "embark run livenet" and/or "embark blockchain livenet"
|
||||
livenet: {
|
||||
networkType: "livenet",
|
||||
syncMode: "light",
|
||||
rpcCorsDomain: "http://localhost:8000",
|
||||
wsOrigins: "http://localhost:8000",
|
||||
accounts: [
|
||||
{
|
||||
nodeAccounts: true,
|
||||
password: "config/livenet/password"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// you can name an environment with specific settings and then specify with
|
||||
// "embark run custom_name" or "embark blockchain custom_name"
|
||||
//custom_name: {
|
||||
//}
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
module.exports = {
|
||||
// default applies to all environments
|
||||
default: {
|
||||
enabled: true,
|
||||
provider: "whisper", // Communication provider. Currently, Embark only supports whisper
|
||||
available_providers: ["whisper"], // Array of available providers
|
||||
},
|
||||
|
||||
// default environment, merges with the settings in default
|
||||
// assumed to be the intended environment by `embark run`
|
||||
development: {
|
||||
connection: {
|
||||
host: "localhost", // Host of the blockchain node
|
||||
port: 8546, // Port of the blockchain node
|
||||
type: "ws" // Type of connection (ws or rpc)
|
||||
}
|
||||
},
|
||||
|
||||
// merges with the settings in default
|
||||
// used with "embark run privatenet"
|
||||
privatenet: {
|
||||
},
|
||||
|
||||
// merges with the settings in default
|
||||
// used with "embark run testnet"
|
||||
testnet: {
|
||||
},
|
||||
|
||||
// merges with the settings in default
|
||||
// used with "embark run livenet"
|
||||
livenet: {
|
||||
},
|
||||
|
||||
// you can name an environment with specific settings and then specify with
|
||||
// "embark run custom_name"
|
||||
//custom_name: {
|
||||
//}
|
||||
// Use this section when you need a specific symmetric or private keys in whisper
|
||||
/*
|
||||
,keys: {
|
||||
symmetricKey: "your_symmetric_key",// Symmetric key for message decryption
|
||||
privateKey: "your_private_key" // Private Key to be used as a signing key and for message decryption
|
||||
}
|
||||
*/
|
||||
//}
|
||||
};
|
|
@ -0,0 +1,91 @@
|
|||
module.exports = {
|
||||
// default applies to all environments
|
||||
default: {
|
||||
// Blockchain node to deploy the contracts
|
||||
deployment: {
|
||||
host: "localhost", // Host of the blockchain node
|
||||
port: 8546, // Port of the blockchain node
|
||||
type: "ws" // Type of connection (ws or rpc),
|
||||
// Accounts to use instead of the default account to populate your wallet
|
||||
// The order here corresponds to the order of `web3.eth.getAccounts`, so the first one is the `defaultAccount`
|
||||
/*,accounts: [
|
||||
{
|
||||
privateKey: "your_private_key",
|
||||
balance: "5 ether" // You can set the balance of the account in the dev environment
|
||||
// Balances are in Wei, but you can specify the unit with its name
|
||||
},
|
||||
{
|
||||
privateKeyFile: "path/to/file", // Either a keystore or a list of keys, separated by , or ;
|
||||
password: "passwordForTheKeystore" // Needed to decrypt the keystore file
|
||||
},
|
||||
{
|
||||
mnemonic: "12 word mnemonic",
|
||||
addressIndex: "0", // Optionnal. The index to start getting the address
|
||||
numAddresses: "1", // Optionnal. The number of addresses to get
|
||||
hdpath: "m/44'/60'/0'/0/" // Optionnal. HD derivation path
|
||||
},
|
||||
{
|
||||
"nodeAccounts": true // Uses the Ethereum node's accounts
|
||||
}
|
||||
]*/
|
||||
},
|
||||
// order of connections the dapp should connect to
|
||||
dappConnection: [
|
||||
"$WEB3", // uses pre existing web3 object if available (e.g in Mist)
|
||||
"ws://localhost:8546",
|
||||
"http://localhost:8545"
|
||||
],
|
||||
|
||||
// Automatically call `ethereum.enable` if true.
|
||||
// If false, the following code must run before sending any transaction: `await EmbarkJS.enableEthereum();`
|
||||
// Default value is true.
|
||||
// dappAutoEnable: true,
|
||||
|
||||
gas: "auto",
|
||||
|
||||
// Strategy for the deployment of the contracts:
|
||||
// - implicit will try to deploy all the contracts located inside the contracts directory
|
||||
// or the directory configured for the location of the contracts. This is default one
|
||||
// when not specified
|
||||
// - explicit will only attempt to deploy the contracts that are explicity specified inside the
|
||||
// contracts section.
|
||||
//strategy: 'implicit',
|
||||
|
||||
contracts: {
|
||||
// example:
|
||||
//SimpleStorage: {
|
||||
// args: [ 100 ]
|
||||
//}
|
||||
}
|
||||
},
|
||||
|
||||
// default environment, merges with the settings in default
|
||||
// assumed to be the intended environment by `embark run`
|
||||
development: {
|
||||
dappConnection: [
|
||||
"ws://localhost:8546",
|
||||
"http://localhost:8545",
|
||||
"$WEB3" // uses pre existing web3 object if available (e.g in Mist)
|
||||
]
|
||||
},
|
||||
|
||||
// merges with the settings in default
|
||||
// used with "embark run privatenet"
|
||||
privatenet: {
|
||||
},
|
||||
|
||||
// merges with the settings in default
|
||||
// used with "embark run testnet"
|
||||
testnet: {
|
||||
},
|
||||
|
||||
// merges with the settings in default
|
||||
// used with "embark run livenet"
|
||||
livenet: {
|
||||
},
|
||||
|
||||
// you can name an environment with specific settings and then specify with
|
||||
// "embark run custom_name" or "embark blockchain custom_name"
|
||||
//custom_name: {
|
||||
//}
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
dev_password
|
|
@ -0,0 +1,39 @@
|
|||
module.exports = {
|
||||
// default applies to all environments
|
||||
default: {
|
||||
enabled: true,
|
||||
available_providers: ["ens"],
|
||||
provider: "ens"
|
||||
},
|
||||
|
||||
// default environment, merges with the settings in default
|
||||
// assumed to be the intended environment by `embark run`
|
||||
development: {
|
||||
register: {
|
||||
rootDomain: "embark.eth",
|
||||
subdomains: {
|
||||
'status': '0x1a2f3b98e434c02363f3dac3174af93c1d690914'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// merges with the settings in default
|
||||
// used with "embark run privatenet"
|
||||
privatenet: {
|
||||
},
|
||||
|
||||
// merges with the settings in default
|
||||
// used with "embark run testnet"
|
||||
testnet: {
|
||||
},
|
||||
|
||||
// merges with the settings in default
|
||||
// used with "embark run livenet"
|
||||
livenet: {
|
||||
},
|
||||
|
||||
// you can name an environment with specific settings and then specify with
|
||||
// "embark run custom_name" or "embark blockchain custom_name"
|
||||
//custom_name: {
|
||||
//}
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
// Embark has support for Flow enabled by default in its built-in webpack
|
||||
// config: type annotations will automatically be stripped out of DApp sources
|
||||
// without any additional configuration. Note that type checking is not
|
||||
// performed during builds.
|
||||
|
||||
// To enable Flow type checking refer to the preconfigured template:
|
||||
// https://github.com/embark-framework/embark-flow-template
|
||||
// A new DApp can be created from that template with:
|
||||
// embark new --template flow
|
||||
|
||||
module.exports = {
|
||||
typescript: false,
|
||||
enabled:false
|
||||
// Setting `typescript: true` in this config will disable Flow support in
|
||||
// Embark's default webpack config and enable TypeScript support: .ts and
|
||||
// .tsx sources will automatically be transpiled into JavaScript without any
|
||||
// additional configuration. Note that type checking is not performed during
|
||||
// builds.
|
||||
|
||||
// To enable TypeScript type checking refer to the preconfigured template:
|
||||
// https://github.com/embark-framework/embark-typescript-template
|
||||
// A new DApp can be created from that template with:
|
||||
// embark new --template typescript
|
||||
};
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"config": {
|
||||
"homesteadBlock": 0,
|
||||
"byzantiumBlock": 0,
|
||||
"daoForkSupport": true
|
||||
},
|
||||
"nonce": "0x0000000000000042",
|
||||
"difficulty": "0x0",
|
||||
"alloc": {
|
||||
"0x3333333333333333333333333333333333333333": {"balance": "15000000000000000000"}
|
||||
},
|
||||
"mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"coinbase": "0x3333333333333333333333333333333333333333",
|
||||
"timestamp": "0x00",
|
||||
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"extraData": "0x",
|
||||
"gasLimit": "0x7a1200"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
dev_password
|
|
@ -0,0 +1,59 @@
|
|||
module.exports = {
|
||||
// default applies to all environments
|
||||
default: {
|
||||
enabled: true,
|
||||
ipfs_bin: "ipfs",
|
||||
available_providers: ["ipfs"],
|
||||
upload: {
|
||||
provider: "ipfs",
|
||||
host: "localhost",
|
||||
port: 5001
|
||||
},
|
||||
dappConnection: [
|
||||
{
|
||||
provider: "ipfs",
|
||||
host: "localhost",
|
||||
port: 5001,
|
||||
getUrl: "http://localhost:8080/ipfs/"
|
||||
}
|
||||
]
|
||||
// Configuration to start Swarm in the same terminal as `embark run`
|
||||
/*,account: {
|
||||
address: "YOUR_ACCOUNT_ADDRESS", // Address of account accessing Swarm
|
||||
password: "PATH/TO/PASSWORD/FILE" // File containing the password of the account
|
||||
},
|
||||
swarmPath: "PATH/TO/SWARM/EXECUTABLE" // Path to swarm executable (default: swarm)*/
|
||||
},
|
||||
|
||||
// default environment, merges with the settings in default
|
||||
// assumed to be the intended environment by `embark run`
|
||||
development: {
|
||||
enabled: true,
|
||||
upload: {
|
||||
provider: "ipfs",
|
||||
host: "localhost",
|
||||
port: 5001,
|
||||
getUrl: "http://localhost:8080/ipfs/"
|
||||
}
|
||||
},
|
||||
|
||||
// merges with the settings in default
|
||||
// used with "embark run privatenet"
|
||||
privatenet: {
|
||||
},
|
||||
|
||||
// merges with the settings in default
|
||||
// used with "embark run testnet"
|
||||
testnet: {
|
||||
},
|
||||
|
||||
// merges with the settings in default
|
||||
// used with "embark run livenet"
|
||||
livenet: {
|
||||
},
|
||||
|
||||
// you can name an environment with specific settings and then specify with
|
||||
// "embark run custom_name"
|
||||
//custom_name: {
|
||||
//}
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
test_password
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
enabled: false
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"name": "dreddit-unicef-surge-2019",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.1.2",
|
||||
"@babel/plugin-proposal-class-properties": "7.1.0",
|
||||
"@babel/plugin-proposal-decorators": "7.1.2",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.0.0",
|
||||
"@babel/plugin-proposal-function-sent": "7.1.0",
|
||||
"@babel/plugin-proposal-json-strings": "7.0.0",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.0.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.0.0",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.0.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.0.0",
|
||||
"@babel/plugin-syntax-import-meta": "7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime-corejs2": "^7.3.1",
|
||||
"@material-ui/core": "^3.9.3",
|
||||
"@material-ui/icons": "^3.0.2",
|
||||
"@material-ui/lab": "^3.0.0-alpha.30",
|
||||
"dateformat": "^3.0.3",
|
||||
"embarkjs-connector-web3": "^4.1.0-beta.1",
|
||||
"embarkjs-ipfs": "/Users/emizzle/Code/__Github/embk-fw/embark/packages/embarkjs-ipfs",
|
||||
"embarkjs-whisper": "/Users/emizzle/Code/__Github/embk-fw/embark/packages/embarkjs-whisper",
|
||||
"lodash": "^4.17.11",
|
||||
"markdown": "^0.5.0",
|
||||
"react": "^16.8.6",
|
||||
"react-blockies": "^1.4.1",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-scripts": "2.1.8"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "PORT=3001 react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "embark test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
],
|
||||
"description": ""
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 162 KiB |
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<title>DReddit with Embark 4</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"short_name": "DReddit",
|
||||
"name": "DReddit with Embark 4",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import React, {Component} from 'react';
|
||||
import Create from './Create';
|
||||
import Header from './Header';
|
||||
import Post from './Post';
|
||||
import _ from 'lodash';
|
||||
import {MuiThemeProvider, createMuiTheme} from '@material-ui/core/styles';
|
||||
|
||||
import EmbarkJS from '../embarkArtifacts/embarkjs';
|
||||
import DReddit from '../embarkArtifacts/contracts/DReddit';
|
||||
|
||||
import '../css/app.css';
|
||||
const theme = createMuiTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
dark: '#398689',
|
||||
light: '#78BE97',
|
||||
main: '#0B476D'
|
||||
// contrastText: will be calculated to contrast with palette.primary.main
|
||||
},
|
||||
secondary: {
|
||||
dark: '#E75E3B',
|
||||
// light: will be calculated to contrast with palette.secondary.main
|
||||
main: '#FAB266'
|
||||
},
|
||||
// error: will use the default color
|
||||
},
|
||||
typography: {
|
||||
useNextVariants: true,
|
||||
suppressDeprecationWarnings: true
|
||||
}
|
||||
});
|
||||
|
||||
class App extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
'displayForm': false,
|
||||
'list': [],
|
||||
'sortBy': 'age',
|
||||
'sortOrder': 'desc',
|
||||
'filterBy': ''
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EmbarkJS.onReady(() => {
|
||||
this._loadPosts();
|
||||
});
|
||||
}
|
||||
|
||||
_toggleForm = () => {
|
||||
this.setState({displayForm: !this.state.displayForm});
|
||||
}
|
||||
|
||||
_setSortOrder = (sortBy) => {
|
||||
const sortOrder = (this.state.sortOrder === 'asc' && this.state.sortBy === sortBy) || this.state.sortBy !== sortBy ? 'desc' : 'asc';
|
||||
this.setState({sortBy, sortOrder});
|
||||
}
|
||||
|
||||
_loadPosts = async () => {
|
||||
const {posts, numPosts} = DReddit.methods;
|
||||
|
||||
let list = [];
|
||||
|
||||
const total = await numPosts().call();
|
||||
|
||||
if(total > 0){
|
||||
for (let i = 0; i < total; i++) {
|
||||
const currentPost = posts(i).call();
|
||||
list.push(currentPost);
|
||||
}
|
||||
|
||||
list = await Promise.all(list);
|
||||
list = list.map((value, index) => {
|
||||
value.id = index;
|
||||
value.upvotes = parseInt(value.upvotes, 10);
|
||||
value.downvotes = parseInt(value.downvotes, 10);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({list});
|
||||
}
|
||||
|
||||
_search = (filterBy) => {
|
||||
this.setState({filterBy});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {displayForm, list, sortBy, sortOrder, filterBy} = this.state;
|
||||
|
||||
let orderedList;
|
||||
if(sortBy === 'rating'){
|
||||
orderedList = _.orderBy(list, [function(o) { return o.upvotes - o.downvotes; }, 'creationDate'], [sortOrder, sortOrder]);
|
||||
} else {
|
||||
orderedList = _.orderBy(list, 'creationDate', sortOrder);
|
||||
}
|
||||
|
||||
return <MuiThemeProvider theme={theme}>
|
||||
<Header toggleForm={this._toggleForm} sortOrder={this._setSortOrder} search={this._search} onCancelSearch={this._search} />
|
||||
{ displayForm && <Create afterPublish={this._loadPosts} /> }
|
||||
{ orderedList.map((record) => <Post key={record.id} {...record} filterBy={filterBy} />) }
|
||||
</MuiThemeProvider>;
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,128 @@
|
|||
/* global web3 */
|
||||
import React, {Component, Fragment} from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import LinearProgress from '@material-ui/core/LinearProgress';
|
||||
import PropTypes from 'prop-types';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import {withStyles} from '@material-ui/core/styles';
|
||||
|
||||
import EmbarkJS from '../embarkArtifacts/embarkjs';
|
||||
import DReddit from '../embarkArtifacts/contracts/DReddit';
|
||||
|
||||
const styles = theme => ({
|
||||
textField: {
|
||||
marginRight: theme.spacing.unit * 2
|
||||
}
|
||||
});
|
||||
|
||||
class Create extends Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
'title': '',
|
||||
'content': '',
|
||||
'isSubmitting': false,
|
||||
'error': ''
|
||||
};
|
||||
}
|
||||
|
||||
handleClick = async event => {
|
||||
event.preventDefault();
|
||||
|
||||
if(this.state.title.trim() === ''){
|
||||
this.setState({'error': 'Required field'});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isSubmitting: true,
|
||||
error: ''
|
||||
});
|
||||
|
||||
|
||||
const textToSave = {
|
||||
'title': this.state.title,
|
||||
'content': this.state.content
|
||||
};
|
||||
|
||||
const ipfsHash = await EmbarkJS.Storage.saveText(JSON.stringify(textToSave));
|
||||
|
||||
const {create} = DReddit.methods;
|
||||
const toSend = await create(ipfsHash);
|
||||
//const estimatedGas = await toSend.estimateGas();
|
||||
|
||||
let newState = {
|
||||
isSubmitting: false
|
||||
};
|
||||
try {
|
||||
await toSend.send({from: web3.eth.defaultAccount, gas: 1000000}); //estimatedGas + 1000});
|
||||
|
||||
newState.content = '';
|
||||
newState.title = '';
|
||||
|
||||
this.setState(newState);
|
||||
this.props.afterPublish();
|
||||
}
|
||||
catch (error) {
|
||||
newState.error = error.message;
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = name => event => {
|
||||
this.setState({
|
||||
[name]: event.target.value
|
||||
});
|
||||
};
|
||||
|
||||
render(){
|
||||
const {classes} = this.props;
|
||||
const {error, content, title, isSubmitting} = this.state;
|
||||
|
||||
return (<Fragment>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<TextField
|
||||
id="title"
|
||||
label="Title"
|
||||
error={error !== ""}
|
||||
multiline
|
||||
rowsMax="20"
|
||||
fullWidth
|
||||
value={title}
|
||||
onChange={this.handleChange('title')}
|
||||
className={classes.textField}
|
||||
margin="normal" />
|
||||
<TextField
|
||||
id="description"
|
||||
label="Description"
|
||||
error={error !== ""}
|
||||
multiline
|
||||
rowsMax="20"
|
||||
fullWidth
|
||||
value={content}
|
||||
helperText={error}
|
||||
onChange={this.handleChange('content')}
|
||||
className={classes.textField}
|
||||
margin="normal" />
|
||||
{
|
||||
<Button variant="contained" color="primary" onClick={this.handleClick} disabled={isSubmitting }>Publish</Button>
|
||||
}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{ this.state.isSubmitting && <LinearProgress /> }
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Create.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
afterPublish: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withStyles(styles)(Create);
|
|
@ -0,0 +1,120 @@
|
|||
import React, {Component} from 'react';
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Hidden from '@material-ui/core/Hidden';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||
import PropTypes from 'prop-types';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import {withStyles} from '@material-ui/core/styles';
|
||||
import SearchBar from './SearchBar';
|
||||
import logo from '../images/embark-logo.png';
|
||||
|
||||
const styles = {
|
||||
root: {
|
||||
flexGrow: 1
|
||||
},
|
||||
flex: {
|
||||
flexGrow: 1
|
||||
},
|
||||
logo: {
|
||||
display: "inline-block",
|
||||
verticalAlign: "middle",
|
||||
width: "40px",
|
||||
marginRight: "10px"
|
||||
}
|
||||
};
|
||||
|
||||
const options = [
|
||||
'Sort by age',
|
||||
'Sort by rating'
|
||||
];
|
||||
class Header extends Component {
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
anchorEl: null,
|
||||
sortIndex: 0
|
||||
};
|
||||
}
|
||||
|
||||
handleClick = event => {
|
||||
event.preventDefault();
|
||||
this.setState({anchorEl: event.currentTarget});
|
||||
};
|
||||
|
||||
handleMenuClick = index => event => {
|
||||
event.preventDefault();
|
||||
this.setState({selectedIndex: index, anchorEl: null});
|
||||
this.props.sortOrder(index === 0 ? 'age' : 'rating');
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
this.setState({anchorEl: null});
|
||||
};
|
||||
|
||||
render(){
|
||||
const {classes, toggleForm, search, onCancelSearch} = this.props;
|
||||
const {anchorEl, sortIndex} = this.state;
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
return (
|
||||
<div className={classes.root} >
|
||||
<AppBar position="fixed">
|
||||
<Toolbar className={classes.toolBar}>
|
||||
<Hidden xsDown>
|
||||
<img src={logo} alt="Logo" className={classes.logo} />
|
||||
<Typography variant="h4" color="inherit" className={classes.flex}>
|
||||
DReddit
|
||||
</Typography>
|
||||
</Hidden>
|
||||
<SearchBar
|
||||
placeholder="Search..."
|
||||
style={{
|
||||
margin: '10px 10px',
|
||||
maxWidth: 280
|
||||
}}
|
||||
onChange={(searchValue) => search(searchValue)}
|
||||
onCancelSearch={onCancelSearch}
|
||||
/>
|
||||
<Button color="inherit" onClick={toggleForm}>
|
||||
<AddIcon />
|
||||
</Button>
|
||||
<Button color="inherit" onClick={this.handleClick}>
|
||||
<MoreVertIcon />
|
||||
</Button>
|
||||
<Menu
|
||||
id="long-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={this.handleClose}
|
||||
PaperProps={{
|
||||
style: {
|
||||
width: 200
|
||||
}
|
||||
}}>
|
||||
{options.map((option, i) => (
|
||||
<MenuItem key={option} selected={i === sortIndex} onClick={this.handleMenuClick(i)}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
toggleForm: PropTypes.func.isRequired,
|
||||
sortOrder: PropTypes.func.isRequired,
|
||||
search: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withStyles(styles)(Header);
|
|
@ -0,0 +1,168 @@
|
|||
import {Card, CardActions, CardContent, CardHeader} from '@material-ui/core';
|
||||
import React, {Component} from 'react';
|
||||
import Blockies from 'react-blockies';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import DownvoteIcon from '@material-ui/icons/ExpandMore';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||
import PropTypes from 'prop-types';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import UpvoteIcon from '@material-ui/icons/ExpandLess';
|
||||
import dateformat from 'dateformat';
|
||||
import markdownJS from "markdown";
|
||||
import {withStyles} from '@material-ui/core/styles';
|
||||
|
||||
import EmbarkJS from '../embarkArtifacts/embarkjs';
|
||||
import DReddit from '../embarkArtifacts/contracts/DReddit';
|
||||
|
||||
const markdown = markdownJS.markdown;
|
||||
|
||||
const styles = theme => ({
|
||||
actions: {
|
||||
marginRight: theme.spacing.unit * 5,
|
||||
fontSize: 15,
|
||||
display: 'flex'
|
||||
},
|
||||
card: {
|
||||
margin: theme.spacing.unit,
|
||||
marginTop: theme.spacing.unit * 4,
|
||||
position: 'relative'
|
||||
},
|
||||
title: {
|
||||
borderBottom: '1px solid #ccc',
|
||||
color: '#666'
|
||||
},
|
||||
spinner: {
|
||||
position: 'absolute',
|
||||
right: theme.spacing.unit * 3
|
||||
}
|
||||
});
|
||||
|
||||
const ballot = {
|
||||
NONE: 0,
|
||||
UPVOTE: 1,
|
||||
DOWNVOTE: 2
|
||||
};
|
||||
|
||||
const contains = (filterBy, content, title, date, owner) => {
|
||||
if(!filterBy) return true;
|
||||
filterBy = filterBy.trim().toLowerCase();
|
||||
if(filterBy === '') return true;
|
||||
return content.toLowerCase().indexOf(filterBy) > -1 ||
|
||||
title.toLowerCase().indexOf(filterBy) > -1 ||
|
||||
date.indexOf(filterBy) > -1 ||
|
||||
owner.toLowerCase().indexOf(filterBy) > -1;
|
||||
};
|
||||
|
||||
class Post extends Component {
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
title: '',
|
||||
content: '',
|
||||
isSubmitting: false,
|
||||
canVote: true,
|
||||
upvotes: props.upvotes,
|
||||
downvotes: props.downvotes
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
EmbarkJS.onReady(() => {
|
||||
this._loadAttributes();
|
||||
});
|
||||
}
|
||||
|
||||
_loadAttributes = async () => {
|
||||
// const ipfsHash = web3.utils.toAscii(this.props.description);
|
||||
|
||||
const ipfsText = await EmbarkJS.Storage.get(this.props.description);
|
||||
|
||||
const jsonContent = JSON.parse(ipfsText);
|
||||
const title = jsonContent.title;
|
||||
const content = jsonContent.content;
|
||||
|
||||
const canVote = await DReddit.methods.canVote(this.props.id).call();
|
||||
|
||||
this.setState({
|
||||
title,
|
||||
content,
|
||||
canVote
|
||||
});
|
||||
}
|
||||
|
||||
_vote = choice => async event => {
|
||||
event.preventDefault();
|
||||
this.setState({isSubmitting: true});
|
||||
|
||||
const {vote} = DReddit.methods;
|
||||
const toSend = vote(this.props.id, choice);
|
||||
const estimatedGas = await toSend.estimateGas();
|
||||
|
||||
await toSend.send({gas: estimatedGas + 1000});
|
||||
|
||||
this.setState({
|
||||
canVote: false,
|
||||
upvotes: this.state.upvotes + (choice === ballot.UPVOTE ? 1 : 0),
|
||||
downvotes: this.state.downvotes + (choice === ballot.DOWNVOTE ? 1 : 0)
|
||||
});
|
||||
|
||||
this.setState({isSubmitting: false});
|
||||
}
|
||||
|
||||
render(){
|
||||
const {title, content, upvotes, downvotes, isSubmitting, canVote} = this.state;
|
||||
const {creationDate, classes, owner, filterBy} = this.props;
|
||||
const disabled = isSubmitting || !canVote;
|
||||
const formattedDate = dateformat(new Date(creationDate * 1000), "yyyy-mm-dd HH:MM:ss");
|
||||
const mdText = markdown.toHTML(content);
|
||||
|
||||
const display = contains(filterBy, content, title, formattedDate, owner);
|
||||
|
||||
return display&& <Card className={classes.card}>
|
||||
<CardHeader title={owner} subheader={formattedDate}
|
||||
avatar={
|
||||
<Blockies seed={owner} size={7} scale={5} />
|
||||
}
|
||||
action={
|
||||
<IconButton>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
} />
|
||||
<CardContent>
|
||||
<Typography variant="h6" className={classes.title} gutterBottom>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography component="div" dangerouslySetInnerHTML={{__html: mdText}} />
|
||||
</CardContent>
|
||||
<CardActions disableActionSpacing>
|
||||
<IconButton className={classes.actions} disabled={disabled} onClick={this._vote(ballot.UPVOTE)}>
|
||||
<UpvoteIcon />
|
||||
{upvotes}
|
||||
</IconButton>
|
||||
<IconButton className={classes.actions} disabled={disabled} onClick={this._vote(ballot.DOWNVOTE)}>
|
||||
<DownvoteIcon />
|
||||
{downvotes}
|
||||
</IconButton>
|
||||
{ isSubmitting && <CircularProgress size={14} className={classes.spinner} /> }
|
||||
</CardActions>
|
||||
</Card>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Post.propTypes = {
|
||||
filterBy: PropTypes.string,
|
||||
upvotes: PropTypes.number.isRequired,
|
||||
downvotes: PropTypes.number.isRequired,
|
||||
classes: PropTypes.object.isRequired,
|
||||
id: PropTypes.number.isRequired,
|
||||
owner: PropTypes.string.isRequired,
|
||||
creationDate: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default withStyles(styles)(Post);
|
|
@ -0,0 +1,220 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import Input from '@material-ui/core/Input'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import ClearIcon from '@material-ui/icons/Clear'
|
||||
import SearchIcon from '@material-ui/icons/Search'
|
||||
import { grey } from '@material-ui/core/colors'
|
||||
import withStyles from '@material-ui/core/styles/withStyles'
|
||||
import classNames from 'classnames'
|
||||
|
||||
const styles = {
|
||||
root: {
|
||||
height: 48,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
iconButton: {
|
||||
opacity: 0.54,
|
||||
transform: 'scale(1, 1)',
|
||||
transition: 'transform 200ms cubic-bezier(0.4, 0.0, 0.2, 1)'
|
||||
},
|
||||
iconButtonHidden: {
|
||||
transform: 'scale(0, 0)',
|
||||
'& > $icon': {
|
||||
opacity: 0
|
||||
}
|
||||
},
|
||||
iconButtonDisabled: {
|
||||
opacity: 0.38
|
||||
},
|
||||
searchIconButton: {
|
||||
marginRight: -48
|
||||
},
|
||||
icon: {
|
||||
opacity: 0.54,
|
||||
transition: 'opacity 200ms cubic-bezier(0.4, 0.0, 0.2, 1)'
|
||||
},
|
||||
input: {
|
||||
width: '100%'
|
||||
},
|
||||
searchContainer: {
|
||||
margin: 'auto 16px',
|
||||
width: 'calc(100% - 48px - 32px)' // 48px button + 32px margin
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Material design search bar
|
||||
* @see [Search patterns](https://material.io/guidelines/patterns/search.html)
|
||||
*/
|
||||
class SearchBar extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
focus: false,
|
||||
value: this.props.value,
|
||||
active: false
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (this.props.value !== nextProps.value) {
|
||||
this.setState({...this.state, value: nextProps.value})
|
||||
}
|
||||
}
|
||||
|
||||
handleFocus = (e) => {
|
||||
this.setState({focus: true})
|
||||
if (this.props.onFocus) {
|
||||
this.props.onFocus(e)
|
||||
}
|
||||
}
|
||||
|
||||
handleBlur = (e) => {
|
||||
this.setState({focus: false})
|
||||
if (this.state.value.trim().length === 0) {
|
||||
this.setState({value: ''})
|
||||
}
|
||||
if (this.props.onBlur) {
|
||||
this.props.onBlur(e)
|
||||
}
|
||||
}
|
||||
|
||||
handleInput = (e) => {
|
||||
this.setState({value: e.target.value})
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(e.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
handleCancel = () => {
|
||||
this.setState({active: false, value: ''})
|
||||
if (this.props.onCancelSearch) {
|
||||
this.props.onCancelSearch()
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp = (e) => {
|
||||
if (e.charCode === 13 || e.key === 'Enter') {
|
||||
this.handleRequestSearch()
|
||||
} else if (this.props.cancelOnEscape && (e.charCode === 27 || e.key === 'Escape')) {
|
||||
this.handleCancel()
|
||||
}
|
||||
if (this.props.onKeyUp) {
|
||||
this.props.onKeyUp(e)
|
||||
}
|
||||
}
|
||||
|
||||
handleRequestSearch = () => {
|
||||
if (this.props.onRequestSearch) {
|
||||
this.props.onRequestSearch(this.state.value)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value } = this.state
|
||||
const {
|
||||
cancelOnEscape,
|
||||
className,
|
||||
classes,
|
||||
closeIcon,
|
||||
disabled,
|
||||
onCancelSearch,
|
||||
onRequestSearch,
|
||||
searchIcon,
|
||||
style,
|
||||
...inputProps
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<Paper
|
||||
className={classNames(classes.root, className)}
|
||||
style={style}
|
||||
>
|
||||
<div className={classes.searchContainer}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
onBlur={this.handleBlur}
|
||||
value={value}
|
||||
onChange={this.handleInput}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onFocus={this.handleFocus}
|
||||
fullWidth
|
||||
className={classes.input}
|
||||
disableUnderline
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
onClick={this.handleRequestSearch}
|
||||
classes={{
|
||||
root: classNames(classes.iconButton, classes.searchIconButton, {
|
||||
[classes.iconButtonHidden]: value !== ''
|
||||
}),
|
||||
disabled: classes.iconButtonDisabled
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
{React.cloneElement(searchIcon, {
|
||||
classes: { root: classes.icon }
|
||||
})}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={this.handleCancel}
|
||||
classes={{
|
||||
root: classNames(classes.iconButton, {
|
||||
[classes.iconButtonHidden]: value === ''
|
||||
}),
|
||||
disabled: classes.iconButtonDisabled
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
{React.cloneElement(closeIcon, {
|
||||
classes: { root: classes.icon }
|
||||
})}
|
||||
</IconButton>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SearchBar.defaultProps = {
|
||||
className: '',
|
||||
closeIcon: <ClearIcon style={{ color: grey[500] }} />,
|
||||
disabled: false,
|
||||
placeholder: 'Search',
|
||||
searchIcon: <SearchIcon style={{ color: grey[500] }} />,
|
||||
style: null,
|
||||
value: ''
|
||||
}
|
||||
|
||||
SearchBar.propTypes = {
|
||||
/** Whether to clear search on escape */
|
||||
cancelOnEscape: PropTypes.bool,
|
||||
/** Override or extend the styles applied to the component. */
|
||||
classes: PropTypes.object.isRequired,
|
||||
/** Custom top-level class */
|
||||
className: PropTypes.string,
|
||||
/** Override the close icon. */
|
||||
closeIcon: PropTypes.node,
|
||||
/** Disables text field. */
|
||||
disabled: PropTypes.bool,
|
||||
/** Fired when the search is cancelled. */
|
||||
onCancelSearch: PropTypes.func,
|
||||
/** Fired when the text value changes. */
|
||||
onChange: PropTypes.func,
|
||||
/** Fired when the search icon is clicked. */
|
||||
onRequestSearch: PropTypes.func,
|
||||
/** Sets placeholder text for the embedded text field. */
|
||||
placeholder: PropTypes.string,
|
||||
/** Override the search icon. */
|
||||
searchIcon: PropTypes.node,
|
||||
/** Override the inline-styles of the root element. */
|
||||
style: PropTypes.object,
|
||||
/** The value of the text field. */
|
||||
value: PropTypes.string
|
||||
}
|
||||
|
||||
export default withStyles(styles)(SearchBar)
|
|
@ -0,0 +1,9 @@
|
|||
body {
|
||||
margin: 0;
|
||||
background: #e8e8e8;
|
||||
padding-top: 60px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 9.6 KiB |
|
@ -0,0 +1,13 @@
|
|||
import EmbarkJS from './embarkArtifacts/embarkjs';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import App from './components/App';
|
||||
|
||||
// import your contracts
|
||||
// e.g if you have a contract named SimpleStorage:
|
||||
//import SimpleStorage from 'Embark/contracts/SimpleStorage';
|
||||
|
||||
EmbarkJS.onReady(() => {
|
||||
render(<App />, document.getElementById('root'));
|
||||
});
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export function register(config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/* global web3, config, contract, assert */
|
||||
const DReddit = require('Embark/contracts/DReddit');
|
||||
|
||||
const ipfsHash = 'Qmc5gCcjYypU7y28oCALwfSvxCBskLuPKWpK4qpterKC7z';
|
||||
let accounts = [];
|
||||
let postId = -1;
|
||||
|
||||
config({
|
||||
contracts: {
|
||||
DReddit: {}
|
||||
}
|
||||
}, (err, _accounts) => {
|
||||
accounts = _accounts;
|
||||
});
|
||||
|
||||
contract('DReddit', () => {
|
||||
|
||||
it('should work', () => {
|
||||
assert.ok(true);
|
||||
});
|
||||
|
||||
it('should be able to create a post and receive it via contract event', async () => {
|
||||
let receipt = await DReddit.methods.create(web3.utils.fromAscii(ipfsHash)).send();
|
||||
const event = receipt.events.NewPost;
|
||||
postId = event.returnValues.postId;
|
||||
assert.equal(web3.utils.toAscii(event.returnValues.description), ipfsHash);
|
||||
});
|
||||
|
||||
it ('post should have correct data', async () => {
|
||||
const post = await DReddit.methods.posts(postId).call();
|
||||
assert.equal(web3.utils.toAscii(post.description), ipfsHash);
|
||||
assert.equal(post.owner, accounts[0]);
|
||||
});
|
||||
|
||||
it("should not be able to vote in an unexisting post", async () => {
|
||||
const userCanVote = await DReddit.methods.canVote("123").call();
|
||||
assert.equal(userCanVote, false);
|
||||
});
|
||||
|
||||
it("should be able to vote in a post if account hasn't voted before", async () => {
|
||||
const userCanVote = await DReddit.methods.canVote(postId).call();
|
||||
assert.equal(userCanVote, true);
|
||||
});
|
||||
|
||||
it('should be able to vote in a post', async () => {
|
||||
const receipt = await DReddit.methods.vote(postId, 1).send();
|
||||
const Vote = receipt.events.Vote;
|
||||
assert.equal(Vote.returnValues.voter, accounts[0]);
|
||||
});
|
||||
|
||||
it('shouldn\'t be able to vote twice', async () => {
|
||||
try {
|
||||
await DReddit.methods.vote(postId, 1).send();
|
||||
assert.fail('should have reverted before');
|
||||
} catch (error){
|
||||
assert(error.message.search('revert') > -1, 'Revert should happen');
|
||||
}
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue