1
0
mirror of https://github.com/dap-ps/discover.git synced 2025-02-07 15:05:07 +00:00

Merge with master

This commit is contained in:
Lyubomir Kiprov 2019-06-03 11:39:13 +03:00
commit f52e24b345
50 changed files with 1824 additions and 1558 deletions

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"trailingComma": "all",
"semi": false,
"singleQuote": true
}

View File

@ -78,7 +78,7 @@ module.exports = {
instanceOf: 'MiniMeToken',
address: '0x2764b5da3696E3613Ef9864E9B4613f9fA478E75',
},
Discover: { address: '0x9591a20b9B601651eDF1072A1Dda994C0B1a5bBf' },
Discover: { address: '0x74B32E54A50DDB18aB278EafA905e9cb595331B7' },
// SNT: {
// instanceOf: 'MiniMeToken',
// args: [

View File

@ -1,2 +1,2 @@
module.exports.mnemonic =
''
'erupt point century seek certain escape solution flee elegant hard please pen'

View File

@ -32,7 +32,8 @@
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"web3-utils": "^1.0.0-beta.35",
"webpack": "4.28.3"
"webpack": "4.28.3",
"idb": "4.0.3"
},
"scripts": {
"start": "react-scripts start",

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -1,175 +1,188 @@
/* global web3 */
import { broadcastContractFn } from '../helpers';
import MetadataClient from '../../../clients/metadata-client';
import { broadcastContractFn } from '../helpers'
import MetadataClient from '../../../clients/metadata-client'
import BlockchainService from '../blockchain-service';
import BlockchainService from '../blockchain-service'
import DiscoverValidator from './discover-validator';
import DiscoverContract from '../../../../embarkArtifacts/contracts/Discover';
import DiscoverValidator from './discover-validator'
import DiscoverContract from '../../../../embarkArtifacts/contracts/Discover'
class DiscoverService extends BlockchainService {
constructor(sharedContext) {
super(sharedContext, DiscoverContract, DiscoverValidator);
super(sharedContext, DiscoverContract, DiscoverValidator)
}
// View methods
async upVoteEffect(id, amount) {
await this.validator.validateUpVoteEffect(id, amount);
await this.validator.validateUpVoteEffect(id, amount)
return DiscoverContract.methods
.upvoteEffect(id, amount)
.call({ from: this.sharedContext.account });
.call({ from: this.sharedContext.account })
}
async downVoteCost(id) {
const dapp = await this.getDAppById(id);
const dapp = await this.getDAppById(id)
return DiscoverContract.methods
.downvoteCost(dapp.id)
.call({ from: this.sharedContext.account });
.call({ from: this.sharedContext.account })
}
async getDAppsCount() {
return DiscoverContract.methods
.getDAppsCount()
.call({ from: this.sharedContext.account });
.call({ from: this.sharedContext.account })
}
async getDAppByIndexWithMetadata(index) {
try {
const dapp = await DiscoverContract.methods
.dapps(index)
.call({ from: this.sharedContext.account })
dapp.metadata = await MetadataClient.retrieveMetadata(dapp.metadata)
return dapp
} catch (error) {
throw new Error(`Error fetching dapps. Details: ${error.message}`)
}
}
async getDAppById(id) {
let dapp;
let dapp
try {
const dappId = await DiscoverContract.methods
.id2index(id)
.call({ from: this.sharedContext.account });
.call({ from: this.sharedContext.account })
dapp = await DiscoverContract.methods
.dapps(dappId)
.call({ from: this.sharedContext.account });
.call({ from: this.sharedContext.account })
} catch (error) {
throw new Error('Searching DApp does not exists');
throw new Error('Searching DApp does not exists')
}
if (dapp.id != id) {
throw new Error('Error fetching correct data from contract');
throw new Error('Error fetching correct data from contract')
}
return dapp;
return dapp
}
async getDAppDataById(id) {
const dapp = await this.getDAppById(id);
const dapp = await this.getDAppById(id)
try {
const dappMetadata = await MetadataClient.retrieveMetadata(dapp.metadata);
dapp.metadata = dappMetadata.details;
dapp.metadata.status = dappMetadata.status;
const dappMetadata = await MetadataClient.retrieveMetadata(dapp.metadata)
dapp.metadata = dappMetadata.details
dapp.metadata.status = dappMetadata.status
return dapp;
return dapp
} catch (error) {
throw new Error('Error fetching correct data');
throw new Error('Error fetching correct data')
}
}
async safeMax() {
return DiscoverContract.methods
.safeMax()
.call({ from: this.sharedContext.account });
.call({ from: this.sharedContext.account })
}
async isDAppExists(id) {
return DiscoverContract.methods
.existingIDs(id)
.call({ from: this.sharedContext.account });
.call({ from: this.sharedContext.account })
}
async checkIfCreatorOfDApp(id) {
const dapp = this.getDAppById(id);
this.sharedContext.account = await super.getAccount();
const dapp = this.getDAppById(id)
this.sharedContext.account = await super.getAccount()
return dapp.developer == this.sharedContext.account;
return dapp.developer == this.sharedContext.account
}
// Transaction methods
async createDApp(amount, metadata) {
await super.__unlockServiceAccount();
await super.__unlockServiceAccount()
const dappMetadata = JSON.parse(JSON.stringify(metadata));
dappMetadata.uploader = this.sharedContext.account;
const dappMetadata = JSON.parse(JSON.stringify(metadata))
dappMetadata.uploader = this.sharedContext.account
const dappId = web3.utils.keccak256(JSON.stringify(dappMetadata));
await this.validator.validateDAppCreation(dappId, amount);
const dappId = web3.utils.keccak256(JSON.stringify(dappMetadata))
await this.validator.validateDAppCreation(dappId, amount)
const uploadedMetadata = await MetadataClient.upload(dappMetadata);
const uploadedMetadata = await MetadataClient.upload(dappMetadata)
const callData = DiscoverContract.methods
.createDApp(dappId, amount, uploadedMetadata)
.encodeABI();
.encodeABI()
const createdTx = await this.sharedContext.SNTService.approveAndCall(
this.contract,
amount,
callData
);
callData,
)
return { tx: createdTx, id: dappId };
return { tx: createdTx, id: dappId }
}
async upVote(id, amount) {
await this.validator.validateUpVoting(id, amount);
await this.validator.validateUpVoting(id, amount)
const callData = DiscoverContract.methods.upvote(id, amount).encodeABI();
const callData = DiscoverContract.methods.upvote(id, amount).encodeABI()
return this.sharedContext.SNTService.approveAndCall(
this.contract,
amount,
callData
);
callData,
)
}
async downVote(id) {
const dapp = await this.getDAppById(id);
const amount = (await this.downVoteCost(dapp.id)).c;
const dapp = await this.getDAppById(id)
const amount = (await this.downVoteCost(dapp.id)).c
const callData = DiscoverContract.methods
.downvote(dapp.id, amount)
.encodeABI();
.encodeABI()
return this.sharedContext.SNTService.approveAndCall(
this.contract,
amount,
callData
);
callData,
)
}
async withdraw(id, amount) {
await super.__unlockServiceAccount();
await this.validator.validateWithdrawing(id, amount);
await super.__unlockServiceAccount()
await this.validator.validateWithdrawing(id, amount)
try {
return broadcastContractFn(
DiscoverContract.methods.withdraw(id, amount).send,
this.sharedContext.account
);
this.sharedContext.account,
)
} catch (error) {
throw new Error(`Transfer on withdraw failed. Details: ${error.message}`);
throw new Error(`Transfer on withdraw failed. Details: ${error.message}`)
}
}
async setMetadata(id, metadata) {
await super.__unlockServiceAccount();
await this.validator.validateMetadataSet(id);
await super.__unlockServiceAccount()
await this.validator.validateMetadataSet(id)
const dappMetadata = JSON.parse(JSON.stringify(metadata));
dappMetadata.uploader = this.sharedContext.account;
const dappMetadata = JSON.parse(JSON.stringify(metadata))
dappMetadata.uploader = this.sharedContext.account
const uploadedMetadata = await MetadataClient.upload(dappMetadata);
const uploadedMetadata = await MetadataClient.upload(dappMetadata)
try {
return broadcastContractFn(
DiscoverContract.methods.setMetadata(id, uploadedMetadata).send,
this.sharedContext.account
);
this.sharedContext.account,
)
} catch (error) {
throw new Error(`Uploading metadata failed. Details: ${error.message}`);
throw new Error(`Uploading metadata failed. Details: ${error.message}`)
}
}
}
export default DiscoverService;
export default DiscoverService

View File

@ -1,45 +1,45 @@
import HTTPClient from '../http-client';
import HTTPClient from '../http-client'
import * as helpers from '../../utils/metadata-utils';
import metadataClientEndpoints from '../endpoints/metadata-client-endpoints';
import * as helpers from '../../utils/metadata-utils'
import metadataClientEndpoints from '../endpoints/metadata-client-endpoints'
class APIClient {
async upload(metadata) {
const uploadedDataResponse = await HTTPClient.postRequest(
metadataClientEndpoints.UPLOAD,
{ details: metadata }
);
metadata,
)
return helpers.getBytes32FromIpfsHash(uploadedDataResponse.data.hash);
return helpers.getBytes32FromIpfsHash(uploadedDataResponse.data.hash)
}
async retrieveMetadata(metadataBytes32) {
const convertedHash = helpers.getIpfsHashFromBytes32(metadataBytes32);
const convertedHash = helpers.getIpfsHashFromBytes32(metadataBytes32)
const retrievedMetadataResponse = await HTTPClient.getRequest(
`${metadataClientEndpoints.RETRIEVE_METADATA}/${convertedHash}`
);
`${metadataClientEndpoints.RETRIEVE_METADATA}/${convertedHash}`,
)
return retrievedMetadataResponse.data;
return retrievedMetadataResponse.data
}
async retrieveAllDappsMetadata() {
const retrievedDAppsMetadataResponse = await HTTPClient.getRequest(
`${metadataClientEndpoints.RETRIEVE_ALL_METADATA}`
);
`${metadataClientEndpoints.RETRIEVE_ALL_METADATA}`,
)
const formatedDappsMetadata = {};
const metadataHashes = Object.keys(retrievedDAppsMetadataResponse.data);
const formatedDappsMetadata = {}
const metadataHashes = Object.keys(retrievedDAppsMetadataResponse.data)
for (let i = 0; i < metadataHashes.length; i++) {
const convertedDappMetadataHash = helpers.getBytes32FromIpfsHash(
metadataHashes[i]
);
metadataHashes[i],
)
formatedDappsMetadata[convertedDappMetadataHash] =
retrievedDAppsMetadataResponse.data[metadataHashes[i]];
retrievedDAppsMetadataResponse.data[metadataHashes[i]]
}
return formatedDappsMetadata;
return formatedDappsMetadata
}
}
export default APIClient;
export default APIClient

View File

@ -3,20 +3,57 @@ import PropTypes from 'prop-types'
import { DappListModel } from '../../utils/models'
import DappListItem from '../DappListItem'
const DappList = props => {
const { dapps, isRanked, showActionButtons } = props
return (
dapps &&
dapps.map((dapp, i) => (
<DappListItem
dapp={dapp}
key={dapp.name}
isRanked={isRanked}
position={i + 1}
showActionButtons={showActionButtons}
/>
))
)
class DappList extends React.Component {
constructor(props) {
super(props)
this.state = { dappIdsMap: new Set(), mounted: false }
}
componentDidMount() {
const { dapps } = this.props
const { dappIdsMap } = this.state
dapps.forEach(dapp => dappIdsMap.add(dapp.id))
this.setState({ dappIdsMap, mounted: true })
}
componentDidUpdate() {
const { dapps } = this.props
const { dappIdsMap } = this.state
let update = false
for (let i = 0; i < dapps.length; i += 1) {
if (dappIdsMap.has(dapps[i].id) === false) update = true
}
if (!update) return
for (let i = 0; i < dapps.length; i += 1) dappIdsMap.add(dapps[i].id)
requestAnimationFrame(() => {
requestAnimationFrame(() => {
this.setState({ dappIdsMap })
})
})
}
animate() {}
render() {
const { dapps, isRanked, showActionButtons } = this.props
const { dappIdsMap, mounted } = this.state
return (
dapps &&
dapps.map((dapp, i) => (
<DappListItem
dapp={dapp}
key={dapp.id}
isRanked={isRanked}
position={i + 1}
visible={!mounted || dappIdsMap.has(dapp.id)}
showActionButtons={showActionButtons}
/>
))
)
}
}
DappList.defaultProps = {

View File

@ -8,8 +8,6 @@ import {
import { toggleProfileModalAction } from '../../../modules/Profile/Profile.reducer'
const mapDispatchToProps = dispatch => ({
onClickUpVote: () => dispatch(showUpVoteAction()),
onClickDownVote: () => dispatch(showDownVoteAction()),
onClickUpVote: dapp => {
dispatch(showUpVoteAction(dapp))
dispatch(fetchVoteRatingAction(dapp, true, 0))

View File

@ -15,12 +15,12 @@ const DappListItem = props => {
onClickDownVote,
isRanked,
position,
category,
visible,
showActionButtons,
onToggleProfileModal,
} = props
const { name, description, url, image } = dapp
const { name, desc, url, image } = dapp
const handleUpVote = () => {
onClickUpVote(dapp)
@ -31,7 +31,11 @@ const DappListItem = props => {
}
return (
<div className={isRanked ? styles.rankedListItem : styles.listItem}>
<div
className={`${styles.dappListItem} ${
isRanked ? styles.rankedListItem : styles.listItem
} ${visible ? '' : styles.transparent}`}
>
{isRanked && <div className={styles.position}>{position}</div>}
<div
className={styles.imgWrapper}
@ -51,7 +55,7 @@ const DappListItem = props => {
className={styles.description}
style={{ WebkitBoxOrient: 'vertical' }}
>
{description}
{desc}
</p>
</div>
<a className={styles.url} href={url}>
@ -84,12 +88,14 @@ const DappListItem = props => {
DappListItem.defaultProps = {
isRanked: false,
showActionButtons: false,
visible: true,
}
DappListItem.propTypes = {
dapp: PropTypes.shape(DappModel).isRequired,
isRanked: PropTypes.bool,
showActionButtons: PropTypes.bool,
visible: PropTypes.bool,
position: PropTypes.number.isRequired,
onClickUpVote: PropTypes.func.isRequired,
onClickDownVote: PropTypes.func.isRequired,

View File

@ -1,5 +1,14 @@
@import '../../styles/variables';
.dappListItem {
transition-property: opacity;
transition-duration: 1s;
}
.dappListItem.transparent {
opacity: 0;
}
.listItem {
font-family: $font;
background: $background;

126
src/common/data/dapp.js Normal file
View File

@ -0,0 +1,126 @@
import * as Categories from './categories'
export default class DappModel {
constructor() {
// blockchain
this.id = ''
this.sntValue = 0
// metadata
this.name = ''
this.image = ''
this.desc = ''
this.category = ''
this.dateAdded = 0
}
clone() {
return Object.assign(new DappModel(), this)
}
static instanceFromBlockchainWithMetadata(source) {
return Object.assign(new DappModel(), source.metadata, {
id: source.id,
sntValue: parseInt(source.effectiveBalance, 10),
})
}
}
const RECENTLY_ADDED_SIZE = 50
const HIGHEST_RANKED_SIZE = 50
export class DappState {
constructor() {
this.loaded = true
this.dapps = []
this.dappsHightestRanked = null
this.dappsRecentlyAdded = null
this.categoryMap = new Map()
this.categoryMap.set(Categories.EXCHANGES, null)
this.categoryMap.set(Categories.MARKETPLACES, null)
this.categoryMap.set(Categories.COLLECTIBLES, null)
this.categoryMap.set(Categories.GAMES, null)
this.categoryMap.set(Categories.SOCIAL_NETWORKS, null)
this.categoryMap.set(Categories.UTILITIES, null)
this.categoryMap.set(Categories.OTHER, null)
}
clone() {
const result = new DappState()
result.dapps = [...this.dapps]
return result
}
creditDapp(dapp) {
for (let i = 0; i < this.dapps.length; i += 1)
if (this.dapps[i].id === dapp.id) return this.updateDapp(dapp)
return this.addDapp(dapp)
}
addDapp(dapp) {
const result = new DappState()
let pushed = false
for (let i = 0; i < this.dapps.length; i += 1) {
if (!pushed && dapp.sntValue > this.dapps[i].sntValue) {
result.dapps.push(dapp)
pushed = true
}
result.dapps.push(this.dapps[i].clone())
}
if (!pushed) result.dapps.push(dapp)
return result
}
addDapps(dapps) {
const result = new DappState()
result.dapps = this.dapps.concat(dapps)
result.dapps.sort((a, b) => {
return b.sntValue - a.sntValue
})
return result
}
updateDapp(dapp) {
const result = new DappState()
for (let i = 0; i < this.dapps.length; i += 1) {
if (dapp.id === this.dapps[i].id) result.dapps.push(dapp)
else result.dapps.push(this.dapps[i].clone())
}
result.dapps.sort((a, b) => {
return b.sntValue - a.sntValue
})
return result
}
getDappsByCategory(category) {
let filtered = this.categoryMap.get(category)
if (filtered === null) {
filtered = this.dapps.filter(dapp => dapp.category === category)
this.categoryMap.set(category, filtered)
}
return filtered
}
getHighestRanked() {
if (this.dappsHightestRanked === null)
this.dappsHightestRanked = this.dapps.slice(0, HIGHEST_RANKED_SIZE)
return this.dappsHightestRanked
}
getRecentlyAdded() {
if (this.dappsRecentlyAdded === null) {
this.dappsRecentlyAdded = [...this.dapps]
.sort((a, b) => {
return (
new Date().getTime(b.dateAdded) - new Date(a.dateAdded).getTime()
)
})
.slice(0, RECENTLY_ADDED_SIZE)
}
return this.dappsRecentlyAdded
}
}
const dappsInitialState = new DappState()
dappsInitialState.loaded = false
export { dappsInitialState }

View File

@ -1,633 +0,0 @@
import * as Categories from './categories'
const Dapps = [
{
metadata: {
name: 'Airswap',
url: 'https://instant.airswap.io/',
image: '/images/dapps/airswap.png',
description: 'Meet the future of trading',
category: Categories.EXCHANGES,
dateAdded: '2019-05-05',
categoryPosition: 13,
},
rate: 45,
},
{
metadata: {
name: 'Bancor',
url: 'https://www.bancor.network/',
image: '/images/dapps/bancor.png',
description: 'Bancor is a decentralized liquidity network',
category: Categories.EXCHANGES,
dateAdded: '2019-03-05',
categoryPosition: 12,
},
rate: 345,
},
{
metadata: {
name: 'Kyber',
url: 'https://web3.kyber.network',
description:
'On-chain, instant and liquid platform for exchange and payment',
image: '/images/dapps/kyber.png',
category: Categories.EXCHANGES,
dateAdded: '2019-04-05',
categoryPosition: 11,
},
rate: 2345,
},
{
metadata: {
name: 'Uniswap',
url: 'https://uniswap.exchange/',
description:
'Seamlessly exchange ERC20 tokens, or use a formalized model to pool liquidity reserves',
image: '/images/dapps/uniswap.png',
category: Categories.EXCHANGES,
dateAdded: '2019-04-23',
categoryPosition: 10,
},
rate: 12345,
},
{
metadata: {
name: 'DAI by MakerDao',
url: 'https://dai.makerdao.com',
description: 'Stability for the blockchain',
image: '/images/dapps/dai.png',
category: Categories.EXCHANGES,
dateAdded: '2019-04-05',
categoryPosition: 9,
},
rate: 22345,
},
{
metadata: {
name: 'Augur',
url: 'https://augur.net',
description:
'A prediction market protocol owned and operated by the people that use it',
image: '/images/dapps/augur.svg',
category: Categories.EXCHANGES,
dateAdded: '2019-04-11',
categoryPosition: 8,
},
rate: 32345,
},
{
metadata: {
name: 'LocalEthereum',
url: 'https://localethereum.com/',
description: 'The smartest way to buy and sell Ether',
image: '/images/dapps/local-ethereum.png',
category: Categories.EXCHANGES,
dateAdded: '2019-04-05',
categoryPosition: 7,
},
rate: 42345,
},
{
metadata: {
name: 'Eth2phone',
url: 'https://eth2.io',
description: 'Send Ether by phone number',
image: '/images/dapps/eth2phone.png',
category: Categories.EXCHANGES,
dateAdded: '2019-04-05',
categoryPosition: 6,
},
rate: 52345,
},
{
metadata: {
name: 'DDEX',
url: 'https://ddex.io/',
description:
'Instant, real-time order matching with secure on-chain settlement',
image: '/images/dapps/ddex.png',
category: Categories.EXCHANGES,
dateAdded: '2019-04-05',
categoryPosition: 5,
},
rate: 62345,
},
{
metadata: {
name: 'Nuo',
url: 'https://app.nuo.network/lend/',
description:
'The non-custodial way to lend, borrow or margin trade cryptocurrency',
image: '/images/dapps/nuo.png',
category: Categories.EXCHANGES,
dateAdded: '2019-04-05',
categoryPosition: 4,
},
rate: 72345,
},
{
metadata: {
name: 'EasyTrade',
url: 'https://easytrade.io',
description: 'One exchange for every token',
image: '/images/dapps/easytrade.png',
category: Categories.EXCHANGES,
dateAdded: '2019-04-05',
categoryPosition: 3,
},
rate: 82345,
},
{
metadata: {
name: 'slow.trade',
url: 'https://slow.trade/',
description:
'Trade fairly priced crypto assets on the first platform built with the DutchX protocol',
image: '/images/dapps/slowtrade.png',
category: Categories.EXCHANGES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 92345,
},
{
metadata: {
name: 'Expo Trading',
url: 'https://expotrading.com/trade/',
description: 'The simplest way to margin trade cryptocurrency',
image: '/images/dapps/expotrading.png',
category: Categories.EXCHANGES,
dateAdded: '2019-04-11',
categoryPosition: 1,
},
rate: 102345,
},
{
metadata: {
name: 'Bidali',
url: 'https://commerce.bidali.com/dapp',
description: 'Buy from top brands with crypto',
image: '/images/dapps/bidali.png',
category: Categories.MARKETPLACES,
dateAdded: '2019-05-01',
},
rate: 10246,
},
{
metadata: {
name: 'blockimmo',
url: 'https://blockimmo.ch',
description:
'blockimmo is a blockchain powered, regulated platform enabling shared property investments and ownership',
image: '/images/dapps/blockimmo.png',
category: Categories.MARKETPLACES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'CryptoCribs',
url: 'https://cryptocribs.com',
description: 'Travel the globe. Pay in crypto',
image: '/images/dapps/cryptocribs.png',
category: Categories.MARKETPLACES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Ethlance',
url: 'https://ethlance.com',
description:
'The future of work is now. Hire people or work yourself in return for ETH',
image: '/images/dapps/ethlance.png',
category: Categories.MARKETPLACES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'OpenSea',
url: 'https://opensea.io',
description: 'The largest decentralized marketplace for cryptogoods',
image: '/images/dapps/opensea.png',
category: Categories.MARKETPLACES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'KnownOrigin',
url: 'https://dapp.knownorigin.io/gallery',
description: 'Discover, buy and collect digital artwork',
image: '/images/dapps/knownorigin.png',
category: Categories.MARKETPLACES,
dateAdded: '2019-04-11',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'dBay',
url: 'https://dbay.ai',
description: 'Buy from all your favorite DApps in one place',
image: '/images/dapps/dBay.png',
category: Categories.MARKETPLACES,
dateAdded: '2019-04-23',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Name Bazaar',
url: 'https://namebazaar.io',
description: 'ENS name marketplace',
image: '/images/dapps/name-bazaar.png',
category: Categories.MARKETPLACES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'The Bounties Network',
url: 'https://bounties.network/',
description: 'Bounties on any task, paid in any token',
image: '/images/dapps/bounties-network.png',
category: Categories.MARKETPLACES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Emoon',
url: 'https://www.emoon.io/',
description:
'A decentralized marketplace for buying & selling crypto assets',
image: '/images/dapps/emoon.png',
category: Categories.MARKETPLACES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Astro Ledger',
url: 'https://www.astroledger.org/#/onSale',
description: 'Funding space grants with blockchain star naming',
image: '/images/dapps/astroledger.svg',
category: Categories.MARKETPLACES,
dateAdded: '2019-04-11',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'SuperRare',
url: 'https://superrare.co/market',
description:
'Buy, sell and collect unique digital creations by artists around the world',
image: '/images/dapps/superrare.png',
category: Categories.MARKETPLACES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'CryptoCare',
url: 'https://cryptocare.tech',
description:
'Give your Ether some heart! Collectibles that make the world a better place',
image: '/images/dapps/cryptocare.jpg',
category: Categories.COLLECTIBLES,
dateAdded: '2019-04-11',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'CryptoKitties',
url: 'https://www.cryptokitties.co',
description: 'Collect and breed adorable digital cats',
image: '/images/dapps/cryptokitties.png',
category: Categories.COLLECTIBLES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Cryptographics',
url: 'https://cryptographics.app/',
description:
'A digital art hub for creation, trading, and collecting unique items',
image: '/images/dapps/cryptographics.png',
category: Categories.COLLECTIBLES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'CryptoPunks',
url: 'https://www.larvalabs.com/cryptopunks',
description: '10,000 unique collectible punks',
image: '/images/dapps/cryptopunks.png',
category: Categories.COLLECTIBLES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Crypto Takeovers',
url: 'https://cryptotakeovers.com/',
description: 'Predict and conquer the world. Make a crypto fortune',
image: '/images/dapps/cryptotakeovers.png',
category: Categories.GAMES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'CryptoFighters',
url: 'https://cryptofighters.io',
description: 'Collect train and fight digital fighters',
image: '/images/dapps/cryptofighters.png',
category: Categories.GAMES,
dateAdded: '2019-04-11',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Decentraland',
url: 'https://market.decentraland.org/',
description:
'A virtual reality platform powered by the Ethereum blockchain',
image: '/images/dapps/decentraland.png',
category: Categories.GAMES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Dragonereum',
url: 'https://dapp.dragonereum.io',
description: 'Own and trade dragons, fight with other players',
image: '/images/dapps/dragonereum.png',
category: Categories.GAMES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Etherbots',
url: 'https://etherbots.io/',
description: 'Robot wars on Ethereum',
image: '/images/dapps/etherbots.png',
category: Categories.GAMES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Etheremon',
url: 'https://www.etheremon.com/',
description: 'Decentralized World of Ether Monsters',
image: '/images/dapps/etheremon.png',
category: Categories.GAMES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'CryptoStrikers',
url: 'https://www.cryptostrikers.com/',
description: 'The Beautiful (card) Game',
image: '/images/dapps/cryptostrikers.png',
category: Categories.GAMES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
// {
// metadata: {
// name: 'FairHouse',
// url: 'https://fairhouse.io',
// description: 'Fair and transparent entertainment games.',
// image: '/images/dapps/fairhouse.png',
// category: Categories.GAMES,
// dateAdded: '2019-04-11',
// categoryPosition: 2,
// },
// rate: 12345,
// },
{
metadata: {
name: 'Cent',
url: 'https://beta.cent.co/',
description: 'Get wisdom, get money',
image: '/images/dapps/cent.png',
category: Categories.SOCIAL_NETWORKS,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Kickback',
url: 'https://kickback.events/',
description:
'Event no shows? No problem. Kickback asks event attendees to put skin in the game with Ethereum',
image: '/images/dapps/kickback.png',
category: Categories.SOCIAL_NETWORKS,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Peepeth',
url: 'https://peepeth.com/',
description: 'Blockchain-powered microblogging',
image: '/images/dapps/peepeth.png',
category: Categories.SOCIAL_NETWORKS,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'livepeer.tv',
url: 'http://livepeer.tv/',
description: 'Decentralized video broadcasting',
image: '/images/dapps/livepeer.png',
category: Categories.OTHER,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Aragon',
url: 'https://mainnet.aragon.org/',
description: 'Build unstoppable organizations on Ethereum',
image: '/images/dapps/aragon.png',
category: Categories.UTILITIES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Compound Finance',
url: 'https://app.compound.finance/',
description:
'An open-source protocol for algorithmic, efficient Money Markets on Ethereum',
image: '/images/dapps/compoundfinance.png',
category: Categories.UTILITIES,
dateAdded: '2019-04-11',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'InstaDApp',
url: 'https://instadapp.io/',
description: 'Decentralized Banking',
image: '/images/dapps/instadapp.jpg',
category: Categories.UTILITIES,
dateAdded: '2019-04-11',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Livepeer',
url: 'https://explorer.livepeer.org/',
description: 'Decentralized video broadcasting',
image: '/images/dapps/livepeer.png',
category: Categories.UTILITIES,
dateAdded: '2019-04-11',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'ETHLend',
url: 'https://app.ethlend.io',
description: 'Decentralized lending on Ethereum',
image: '/images/dapps/ethlend.png',
category: Categories.UTILITIES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Civitas',
url: 'https://communities.colu.com/',
description: 'Blockchain-powered local communities',
image: '/images/dapps/civitas.png',
category: Categories.UTILITIES,
dateAdded: '2019-04-11',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: '3Box',
url: 'https://3box.io/',
description: 'Create and manage your Ethereum Profile',
image: '/images/dapps/3Box.png',
category: Categories.UTILITIES,
dateAdded: '2019-04-11',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Hexel',
url: 'https://www.onhexel.com/',
description: 'Create your own cryptocurrency',
image: '/images/dapps/hexel.png',
category: Categories.UTILITIES,
dateAdded: '2019-04-11',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'Smartz',
url: 'https://smartz.io',
description: 'Easy smart contract management',
image: '/images/dapps/smartz.png',
category: Categories.UTILITIES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
{
metadata: {
name: 'SNT Voting DApp',
url: 'https://vote.status.im',
description:
'Let your SNT be heard! Vote on decisions exclusive to SNT holders, or create a poll of your own.',
image: '/images/dapps/snt-voting.png',
category: Categories.UTILITIES,
dateAdded: '2019-04-05',
categoryPosition: 2,
},
rate: 12345,
},
]
export default Dapps

View File

@ -0,0 +1,35 @@
import { openDB } from 'idb'
import DappModel from './dapp'
const DB_NAME = 'status_discover'
const DB_STORE_DAPPS = 'store_dapps'
function open() {
return openDB(DB_NAME, 1, {
upgrade(db) {
console.log('on create')
db.createObjectStore(DB_STORE_DAPPS, {
keyPath: 'id',
})
},
})
}
export default class Database {
static async fetchAllDapps() {
const result = []
const db = await open()
let cursor = await db.transaction(DB_STORE_DAPPS).store.openCursor()
while (cursor) {
result.push(Object.assign(new DappModel(), cursor.value))
cursor = await cursor.continue()
}
return result
}
static async creditDapp(dapp) {
const db = await open()
await db.put(DB_STORE_DAPPS, dapp)
}
}

View File

@ -4,6 +4,8 @@ const vote = {
isUpvote: false,
sntValue: '0',
afterVoteRating: null,
learnMoreUpVote: false,
learnMoreDownVote: false,
}
export default vote

View File

@ -0,0 +1,7 @@
const withdraw = {
visible: false,
dapp: null,
sntValue: '0',
}
export default withdraw

View File

@ -9,6 +9,7 @@ import desktopMenu from '../../modules/DesktopMenu/DesktopMenu.reducer'
import transactionStatus from '../../modules/TransactionStatus/TransactionStatus.recuder'
import alert from '../../modules/Alert/Alert.reducer'
import howToSubmit from '../../modules/HowToSubmit/HowToSubmit.reducer'
import withdraw from '../../modules/Withdraw/Withdraw.reducer'
export default history =>
combineReducers({
@ -22,4 +23,5 @@ export default history =>
transactionStatus,
alert,
howToSubmit,
withdraw,
})

View File

@ -4,9 +4,9 @@ export const DappModel = {
name: PropTypes.string,
url: PropTypes.string,
image: PropTypes.string,
description: PropTypes.string,
desc: PropTypes.string,
category: PropTypes.string,
dateAdded: PropTypes.string,
dateAdded: PropTypes.number,
sntValue: PropTypes.number,
categoryPosition: PropTypes.number,
}

View File

@ -12,6 +12,7 @@ import Terms from '../Terms/Terms'
import TransactionStatus from '../TransactionStatus'
import Alert from '../Alert'
import HowToSubmit from '../HowToSubmit'
import Withdraw from '../Withdraw'
class Router extends React.Component {
componentDidMount() {
@ -27,13 +28,15 @@ class Router extends React.Component {
<Route path="/all" component={Dapps} />
<Route path="/recently-added" component={RecentlyAdded} />
<Route path="/terms" component={Terms} />
<Route path="/:dapp_name" component={Profile} />
<Route path="/:dapp_name" component={Home} />
</Switch>,
<Vote key={2} />,
<Submit key={3} />,
<HowToSubmit key={4} />,
<TransactionStatus key={5} />,
<Alert key={6} />,
<Route key={7} path="/:dapp_name" component={Profile} />,
<Withdraw key={8} />,
]
}
}

View File

@ -1,18 +1,8 @@
import { connect } from 'react-redux'
import Dapps from './Dapps'
// import selector from './Dapps.selector'
import { fetchByCategoryAction } from './Dapps.reducer'
const mapStateToProps = state => ({
dappsCategoryMap: state.dapps.dappsCategoryMap,
})
const mapDispatchToProps = dispatch => ({
fetchByCategory: category => {
dispatch(fetchByCategoryAction(category))
},
dappState: state.dapps,
})
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Dapps)
export default connect(mapStateToProps)(Dapps)

View File

@ -5,6 +5,7 @@ import DappList from '../../common/components/DappList'
import CategoryHeader from '../CategoryHeader'
import styles from './Dapps.module.scss'
import { headerElements, getYPosition } from './Dapps.utils'
import { DappState } from '../../common/data/dapp'
class Dapps extends React.Component {
static scanHeaderPositions() {
@ -25,35 +26,15 @@ class Dapps extends React.Component {
componentDidMount() {
this.boundScroll = debounce(this.handleScroll.bind(this), 1)
window.addEventListener('scroll', this.boundScroll)
this.fetchDapps()
}
componentDidUpdate() {
this.fetchDapps()
}
componentWillUnmount() {
window.removeEventListener('scroll', this.boundScroll)
}
onFetchByCategory(category) {
const { fetchByCategory } = this.props
fetchByCategory(category)
}
getCategories() {
const { dappsCategoryMap } = this.props
return [...dappsCategoryMap.keys()]
}
fetchDapps() {
const { dappsCategoryMap, fetchByCategory } = this.props
dappsCategoryMap.forEach((dappState, category) => {
if (dappState.canFetch() === false) return
if (dappState.items.length >= 1) return
fetchByCategory(category)
})
const { dappState } = this.props
return [...dappState.categoryMap.keys()]
}
handleScroll() {
@ -89,7 +70,7 @@ class Dapps extends React.Component {
}
render() {
const { dappsCategoryMap } = this.props
const { dappState } = this.props
const categories = this.getCategories()
return (
@ -102,15 +83,7 @@ class Dapps extends React.Component {
active={this.isCurrentCategory(category)}
/>
</div>
<DappList dapps={dappsCategoryMap.get(category).items} />
{dappsCategoryMap.get(category).canFetch() && (
<div
className={styles.loadMore}
onClick={this.onFetchByCategory.bind(this, category)}
>
Load more dApps from {category}{' '}
</div>
)}
<DappList dapps={dappState.getDappsByCategory(category)} />
</div>
))}
</div>
@ -118,13 +91,8 @@ class Dapps extends React.Component {
}
}
// Dapps.propTypes = {
// categories: PropTypes.arrayOf(
// PropTypes.shape({ category: PropTypes.string, dapps: DappListModel }),
// ).isRequired,
// }
Dapps.propTypes = {
dappsCategoryMap: PropTypes.instanceOf(Map).isRequired,
dappState: PropTypes.instanceOf(DappState).isRequired,
fetchByCategory: PropTypes.func.isRequired,
}

View File

@ -4,13 +4,3 @@
margin-top: calculateRem(50);
margin-bottom: calculateRem(20);
}
.loadMore {
color: $link-color;
text-transform: uppercase;
font-family: $font;
font-size: 12px;
font-weight: 600;
margin-left: calculateRem(40 + 16 + 16);
cursor: pointer;
}

View File

@ -1,336 +1,88 @@
// import hardcodedDapps from '../../common/data/dapps'
import * as Categories from '../../common/data/categories';
import reducerUtil from '../../common/utils/reducer';
import { showAlertAction } from '../Alert/Alert.reducer';
import BlockchainSDK from '../../common/blockchain';
import { TYPE_SUBMIT } from '../TransactionStatus/TransactionStatus.utilities';
import reducerUtil from '../../common/utils/reducer'
import { showAlertAction } from '../Alert/Alert.reducer'
import BlockchainSDK from '../../common/blockchain'
import { TYPE_SUBMIT } from '../TransactionStatus/TransactionStatus.utilities'
import DappModel, { dappsInitialState, DappState } from '../../common/data/dapp'
import Database from '../../common/data/database'
const ON_FINISH_FETCH_ALL_DAPPS_ACTION =
'DAPPS_ON_FINISH_FETCH_ALL_DAPPS_ACTION';
const ON_UPDATE_DAPPS = 'DAPPS_ON_UPDATE_DAPPS'
const ON_UPDATE_DAPP_DATA = 'DAPPS_ON_UPDATE_DAPP_DATA'
const ON_START_FETCH_HIGHEST_RANKED = 'DAPPS_ON_START_FETCH_HIGHEST_RANKED';
const ON_FINISH_FETCH_HIGHEST_RANKED = 'DAPPS_ON_FINISH_FETCH_HIGHEST_RANKED';
const ON_START_FETCH_RECENTLY_ADDED = 'DAPPS_ON_START_FETCH_RECENTLY_ADDED';
const ON_FINISH_FETCH_RECENTLY_ADDED = 'DAPPS_ON_FINISH_FETCH_RECENTLY_ADDED';
const ON_START_FETCH_BY_CATEGORY = 'DAPPS_ON_START_FETCH_BY_CATEGORY';
const ON_FINISH_FETCH_BY_CATEGORY = 'DAPPS_ON_FINISH_FETCH_BY_CATEGORY';
const ON_UPDATE_DAPP_DATA = 'DAPPS_ON_UPDATE_DAPP_DATA';
const RECENTLY_ADDED_SIZE = 50;
const HIGHEST_RANKED_SIZE = 50;
class DappsState {
constructor() {
this.items = [];
this.hasMore = true;
this.fetched = null;
}
canFetch() {
return this.hasMore && this.fetched !== true;
}
setFetched(fetched) {
this.fetched = fetched;
}
appendItems(items) {
const availableNames = new Set();
let addedItems = 0;
for (let i = 0; i < this.items.length; i += 1)
availableNames.add(this.items[i].name);
for (let i = 0; i < items.length; i += 1) {
if (availableNames.has(items[i].name) === false) {
addedItems += 1;
this.items.push(items[i]);
}
}
this.hasMore = addedItems !== 0;
}
cloneWeakItems() {
this.items = [...this.items];
return this;
}
}
export const onFinishFetchAllDappsAction = dapps => ({
type: ON_FINISH_FETCH_ALL_DAPPS_ACTION,
payload: dapps
});
export const onStartFetchHighestRankedAction = () => ({
type: ON_START_FETCH_HIGHEST_RANKED,
payload: null
});
export const onFinishFetchHighestRankedAction = highestRanked => ({
type: ON_FINISH_FETCH_HIGHEST_RANKED,
payload: highestRanked
});
export const onStartFetchRecentlyAddedAction = () => ({
type: ON_START_FETCH_RECENTLY_ADDED,
payload: null
});
export const onFinishFetchRecentlyAddedAction = recentlyAdded => ({
type: ON_FINISH_FETCH_RECENTLY_ADDED,
payload: recentlyAdded
});
export const onStartFetchByCategoryAction = category => ({
type: ON_START_FETCH_BY_CATEGORY,
payload: category
});
export const onFinishFetchByCategoryAction = (category, dapps) => ({
type: ON_FINISH_FETCH_BY_CATEGORY,
payload: { category, dapps }
});
const fetchAllDappsInState = async (dispatch, getState) => {
const state = getState();
const { transactionStatus } = state;
const stateDapps = state.dapps;
if (stateDapps.dapps === null) {
try {
const blockchain = await BlockchainSDK.getInstance();
let dapps = await blockchain.DiscoverService.getDApps();
dapps = dapps.map(dapp => {
return Object.assign(dapp.metadata, {
id: dapp.id,
sntValue: parseInt(dapp.effectiveBalance, 10)
});
});
dapps.sort((a, b) => {
return b.sntValue - a.sntValue;
});
if (transactionStatus.type === TYPE_SUBMIT) {
for (let i = 0; i < dapps.length; i += 1) {
if (dapps[i].id === transactionStatus.dappId) {
dapps.splice(i, 1);
break;
}
}
}
dispatch(onFinishFetchAllDappsAction(dapps));
return dapps;
} catch (e) {
dispatch(showAlertAction(e.message));
dispatch(onFinishFetchAllDappsAction([]));
return [];
}
}
return stateDapps.dapps;
};
export const onUpdateDappsAction = dappState => ({
type: ON_UPDATE_DAPPS,
payload: dappState,
})
export const fetchAllDappsAction = () => {
return async (dispatch, getState) => {
dispatch(onStartFetchHighestRankedAction());
dispatch(onStartFetchRecentlyAddedAction());
const state = getState()
let dappState = state.dapps
const dapps = await fetchAllDappsInState(dispatch, getState);
const dapps = await Database.fetchAllDapps()
dappState = dappState.addDapps(dapps)
dispatch(onUpdateDappsAction(dappState))
const highestRanked = dapps.slice(0, HIGHEST_RANKED_SIZE);
let recentlyAdded = [...dapps];
recentlyAdded.sort((a, b) => {
return new Date().getTime(b.dateAdded) - new Date(a.dateAdded).getTime();
});
recentlyAdded = recentlyAdded.slice(0, RECENTLY_ADDED_SIZE);
try {
const blockchain = await BlockchainSDK.getInstance()
const discoverService = blockchain.DiscoverService
const N = await discoverService.getDAppsCount()
if (N === 0) {
dispatch(onUpdateDappsAction(dappState.clone()))
return
}
dispatch(onFinishFetchHighestRankedAction(highestRanked));
dispatch(onFinishFetchRecentlyAddedAction(recentlyAdded));
};
};
export const fetchByCategoryAction = category => {
return async (dispatch, getState) => {
dispatch(onStartFetchByCategoryAction(category));
const dapps = await fetchAllDappsInState(dispatch, getState);
const filteredByCategory = dapps.filter(dapp => dapp.category === category);
const dappsCategoryState = getState().dapps.dappsCategoryMap.get(category);
const from = dappsCategoryState.items.length;
const to = Math.min(from + 5, filteredByCategory.length);
const dappsCategorySlice = filteredByCategory.slice(from, to);
dispatch(onFinishFetchByCategoryAction(category, dappsCategorySlice));
};
};
const { transactionStatus } = state
let dappSource = await discoverService.getDAppByIndexWithMetadata(0)
let dappModel = DappModel.instanceFromBlockchainWithMetadata(dappSource)
dappState = dappState.creditDapp(dappModel)
if (
dappModel.id !== transactionStatus.dappId ||
transactionStatus.type !== TYPE_SUBMIT
) {
dispatch(onUpdateDappsAction(dappState))
Database.creditDapp(dappModel)
}
for (let i = N - 1; i >= 1; i -= 1) {
dappSource = await discoverService.getDAppByIndexWithMetadata(i)
dappModel = DappModel.instanceFromBlockchainWithMetadata(dappSource)
dappState = dappState.creditDapp(dappModel)
if (
dappModel.id !== transactionStatus.dappId ||
transactionStatus.type !== TYPE_SUBMIT
) {
dispatch(onUpdateDappsAction(dappState))
Database.creditDapp(dappModel)
}
}
} catch (e) {
console.log('error', e)
// setTimeout(() => {
// dispatch(showAlertAction(e.message))
// }, 1000)
// no need to show current blockchain errors, cache is used to there will be any data
// dispatch(showAlertAction(e.message))
dispatch(onUpdateDappsAction(dappState.clone()))
}
}
}
export const onUpdateDappDataAction = dapp => ({
type: ON_UPDATE_DAPP_DATA,
payload: dapp
});
const onFinishFetchAllDapps = (state, dapps) => {
return Object.assign({}, state, { dapps });
};
const onStartFetchHightestRanked = state => {
return Object.assign({}, state, {
highestRankedFetched: false
});
};
const onFinishFetchHighestRanked = (state, payload) => {
return Object.assign({}, state, {
highestRanked: payload,
highestRankedFetched: true
});
};
const onStartFetchRecentlyAdded = state => {
return Object.assign({}, state, {
recentlyAddedFetched: false
});
};
const onFinishFetchRecentlyAdded = (state, payload) => {
return Object.assign({}, state, {
recentlyAdded: payload,
recentlyAddedFetched: true
});
};
const onStartFetchByCategory = (state, payload) => {
const dappsCategoryMap = new Map();
state.dappsCategoryMap.forEach((dappState, category) => {
dappsCategoryMap.set(category, dappState.cloneWeakItems());
if (category === payload) dappState.setFetched(true);
});
return Object.assign({}, state, {
dappsCategoryMap
});
};
const onFinishFetchByCategory = (state, payload) => {
const { category, dapps } = payload;
const dappsCategoryMap = new Map();
state.dappsCategoryMap.forEach((dappState, category_) => {
dappsCategoryMap.set(category_, dappState);
if (category_ === category) {
dappState.setFetched(false);
dappState.appendItems(dapps);
}
});
return Object.assign({}, state, {
dappsCategoryMap
});
};
const insertDappIntoSortedArray = (source, dapp, cmp) => {
for (let i = 0; i < source.length; i += 1) {
if (cmp(source[i], dapp) === true) {
source.splice(i, 0, dapp);
break;
}
}
};
payload: dapp,
})
const onUpdateDappData = (state, dapp) => {
const dappsCategoryMap = new Map();
const { dapps } = state;
let { highestRanked, recentlyAdded } = state;
let update = false;
Database.creditDapp(dapp)
return state.creditDapp(dapp)
}
state.dappsCategoryMap.forEach((dappState, category_) => {
dappsCategoryMap.set(category_, dappState.cloneWeakItems());
});
for (let i = 0; i < dapps.length; i += 1) {
if (dapps[i].id === dapp.id) {
dapps[i] = dapp;
update = true;
break;
}
}
if (update === false) {
insertDappIntoSortedArray(dapps, dapp, (target, dappItem) => {
return target.sntValue < dappItem.sntValue;
});
insertDappIntoSortedArray(highestRanked, dapp, (target, dappItem) => {
return target.sntValue < dappItem.sntValue;
});
highestRanked = state.highestRanked.splice(0, HIGHEST_RANKED_SIZE);
insertDappIntoSortedArray(recentlyAdded, dapp, (target, dappItem) => {
return (
new Date().getTime(target.dateAdded) <
new Date(dappItem.dateAdded).getTime()
);
});
recentlyAdded = recentlyAdded.splice(0, RECENTLY_ADDED_SIZE);
const dappState = dappsCategoryMap.get(dapp.category);
insertDappIntoSortedArray(dappState.items, dapp, (target, dappItem) => {
return target.sntValue < dappItem.sntValue;
});
} else {
for (let i = 0; i < highestRanked.length; i += 1) {
if (highestRanked[i].id === dapp.id) {
highestRanked[i] = dapp;
break;
}
}
for (let i = 0; i < recentlyAdded.length; i += 1) {
if (recentlyAdded[i].id === dapp.id) {
recentlyAdded[i] = dapp;
break;
}
}
dappsCategoryMap.forEach(dappState => {
const dappStateRef = dappState;
for (let i = 0; i < dappStateRef.items.length; i += 1) {
if (dappStateRef.items[i].id === dapp.id) {
dappStateRef.items[i] = dapp;
break;
}
}
});
}
return Object.assign({}, state, {
dapps: [...dapps],
highestRanked: [...highestRanked],
recentlyAdded: [...recentlyAdded],
dappsCategoryMap
});
};
const onUpdateDapps = (state, dappState) => {
return Object.assign(new DappState(), dappState)
}
const map = {
[ON_FINISH_FETCH_ALL_DAPPS_ACTION]: onFinishFetchAllDapps,
[ON_START_FETCH_HIGHEST_RANKED]: onStartFetchHightestRanked,
[ON_FINISH_FETCH_HIGHEST_RANKED]: onFinishFetchHighestRanked,
[ON_START_FETCH_RECENTLY_ADDED]: onStartFetchRecentlyAdded,
[ON_FINISH_FETCH_RECENTLY_ADDED]: onFinishFetchRecentlyAdded,
[ON_START_FETCH_BY_CATEGORY]: onStartFetchByCategory,
[ON_FINISH_FETCH_BY_CATEGORY]: onFinishFetchByCategory,
[ON_UPDATE_DAPP_DATA]: onUpdateDappData
};
[ON_UPDATE_DAPP_DATA]: onUpdateDappData,
[ON_UPDATE_DAPPS]: onUpdateDapps,
}
const dappsCategoryMap = new Map();
dappsCategoryMap.set(Categories.EXCHANGES, new DappsState());
dappsCategoryMap.set(Categories.MARKETPLACES, new DappsState());
dappsCategoryMap.set(Categories.COLLECTIBLES, new DappsState());
dappsCategoryMap.set(Categories.GAMES, new DappsState());
dappsCategoryMap.set(Categories.SOCIAL_NETWORKS, new DappsState());
dappsCategoryMap.set(Categories.UTILITIES, new DappsState());
dappsCategoryMap.set(Categories.OTHER, new DappsState());
const dappsInitialState = {
dapps: null,
highestRanked: [],
highestRankedFetched: null,
recentlyAdded: [],
recentlyAddedFetched: null,
dappsCategoryMap
};
export default reducerUtil(map, dappsInitialState);
export default reducerUtil(map, dappsInitialState)

View File

@ -1,17 +1,8 @@
import { connect } from 'react-redux'
import Filtered from './Filtered'
import { fetchByCategoryAction } from '../Dapps/Dapps.reducer'
const mapStateToProps = state => ({
dappsCategoryMap: state.dapps.dappsCategoryMap,
})
const mapDispatchToProps = dispatch => ({
fetchByCategory: category => {
dispatch(fetchByCategoryAction(category))
},
dappState: state.dapps,
})
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Filtered)
export default connect(mapStateToProps)(Filtered)

View File

@ -3,69 +3,18 @@ import PropTypes from 'prop-types'
import CategorySelector from '../CategorySelector'
import DappList from '../../common/components/DappList'
import styles from './Filtered.module.scss'
const getScrollY =
window.scrollY !== undefined
? () => {
return window.scrollY
}
: () => {
return document.documentElement.scrollTop
}
import { DappState } from '../../common/data/dapp'
class Filtered extends React.Component {
constructor(props) {
super(props)
this.onScroll = this.onScroll.bind(this)
}
componentDidMount() {
this.fetchDapps()
document.addEventListener('scroll', this.onScroll)
}
componentDidUpdate() {
this.fetchDapps()
}
onScroll() {
this.fetchDapps()
}
getDappList() {
const { dappsCategoryMap, match } = this.props
const result =
match !== undefined ? dappsCategoryMap.get(match.params.id).items : []
return result
}
fetchDapps() {
const { dappsCategoryMap, match, fetchByCategory } = this.props
if (match === undefined) return
const dappState = dappsCategoryMap.get(match.params.id)
if (dappState.canFetch() === false) return
const root = document.getElementById('root')
const bottom = window.innerHeight + getScrollY()
const isNearEnd = bottom + window.innerHeight > root.offsetHeight
if (isNearEnd === false && dappState.items.length >= 10) return
fetchByCategory(match.params.id)
}
render() {
const { match } = this.props
const result = this.getDappList()
const { match, dappState } = this.props
const category = match !== undefined ? match.params.id : undefined
return (
<>
<CategorySelector
category={match !== undefined ? match.params.id : undefined}
/>
<CategorySelector category={category} />
<div className={styles.list}>
<DappList dapps={result} />
<DappList dapps={dappState.getDappsByCategory(category)} />
</div>
</>
)
@ -77,8 +26,7 @@ Filtered.defaultProps = {
}
Filtered.propTypes = {
dappsCategoryMap: PropTypes.instanceOf(Map).isRequired,
fetchByCategory: PropTypes.func.isRequired,
dappState: PropTypes.instanceOf(DappState).isRequired,
match: PropTypes.shape({
params: PropTypes.shape({
id: PropTypes.node,

View File

@ -2,7 +2,7 @@ import { connect } from 'react-redux'
import HighestRanked from './HighestRanked'
const mapStateToProps = state => ({
dapps: state.dapps.highestRanked,
dapps: state.dapps.getHighestRanked(),
})
export default connect(mapStateToProps)(HighestRanked)

View File

@ -9,15 +9,15 @@
.grid {
display: grid;
grid-auto-flow: column;
grid-auto-flow: row;
grid-auto-columns: calc(90%);
// grid-template-rows: 1fr 1fr 1fr;
grid-template-rows: 1fr;
overflow-x: scroll;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
@media (min-width: $desktop) {
grid-auto-flow: row;
// grid-auto-flow: row;
grid-template-columns: 1fr 1fr;
overflow-x: hidden;
}

View File

@ -18,8 +18,7 @@ class Home extends React.Component {
render() {
const { dapps } = this.props
const loaded =
dapps.highestRankedFetched === true && dapps.recentlyAddedFetched === true
const loaded = dapps.loaded
return (
<>

View File

@ -98,7 +98,8 @@ class HowToSubmit extends React.Component {
<ol>
<li>Malicious code injection</li>
<li>
Violation of <a>Status' principles</a>
Violation of{' '}
<a href="https://status.im/about/">Status' principles</a>
</li>
<li>Lack of usability (especially on mobile)</li>
<li>Vote manipulation.</li>
@ -110,8 +111,8 @@ class HowToSubmit extends React.Component {
UI choices for the same underlying contract. Note that
Discover is not affiliated with Status directly, we have
simply chosen to use SNT as a token of value, to abide by{' '}
<a>Status principles</a>, and to take a mobile-first approach
to development.
<a href="https://status.im/about/">Status principles</a>, and
to take a mobile-first approach to development.
</p>
</div>
</>

View File

@ -1,15 +1,20 @@
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import Profile from './Profile'
import { toggleProfileModalAction } from './Profile.reducer'
import { showWithdrawAction } from '../Withdraw/Withdraw.reducer'
import { showSubmitAction } from '../Submit/Submit.reducer'
const mapStateToProps = state => state
const mapStateToProps = state => ({ dappState: state.dapps })
const mapDispatchToProps = dispatch => ({
openModal: dapp => dispatch(toggleProfileModalAction(dapp)),
onClickWithdraw: dapp => dispatch(showWithdrawAction(dapp)),
onClickUpdateMetadata: dapp => dispatch(showSubmitAction(dapp)),
})
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Profile)
export default withRouter(
connect(
mapStateToProps,
mapDispatchToProps,
)(Profile),
)

View File

@ -1,27 +1,23 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ReactImageFallback from 'react-image-fallback'
import { push } from 'connected-react-router'
import Modal from '../../common/components/Modal'
import styles from './Profile.module.scss'
import icon from '../../common/assets/images/icon.svg'
import chat from '../../common/assets/images/chat.svg'
const DesktopScreen = props => {
return <Modal visible={props.visible}>{props.children}</Modal>
}
const MobileScreen = props => {
return <>{props.children}</>
}
import { DappListModel } from '../../common/utils/models'
import { DappState } from '../../common/data/dapp'
const ProfileContent = ({
name,
url,
description,
desc,
image,
position,
category,
highestRankedPosition,
categoryPosition,
onClickWithdraw,
onClickUpdateMetadata,
}) => {
return (
<>
@ -37,13 +33,13 @@ const ProfileContent = ({
<div className={styles.information}>
<h4 className={styles.header}>{name}</h4>
<span className={styles.category}>{category}</span>
<a href="#" target="_blank" className={styles.button}>
<a href={url} target="_blank" className={styles.button}>
Open
</a>
</div>
<div className={styles.description}>
<span className={styles.heading}>Description</span>
<p>{description}</p>
<p>{desc}</p>
</div>
<div className={styles.chat}>
<ReactImageFallback
@ -70,23 +66,35 @@ const ProfileContent = ({
<span className={styles.heading}>Ranking</span>
<div className={styles.rank}>
<div className={styles.rank_position_1}>
<span className={styles.rank_position_span}>{position}</span>
<span className={styles.rank_position_span}>
{categoryPosition}
</span>
</div>
<span className={styles.rank_position_text}>
<span></span>
{position} in {category}
{categoryPosition} in {category}
</span>
</div>
<div className={styles.rank}>
<span className={styles.rank_position_2}>
<span className={styles.rank_position_span}>{position}</span>
<span className={styles.rank_position_span}>
{highestRankedPosition}
</span>
</span>
<span className={styles.rank_position_text}>
<span></span>
{position} in highest ranked DApps
{highestRankedPosition} in highest ranked DApps
</span>
</div>
</div>
<div className={styles.actions}>
<div className={styles.button} onClick={onClickUpdateMetadata}>
Edit metadata
</div>
<div className={styles.button} onClick={onClickWithdraw}>
Withdraw SNT
</div>
</div>
</div>
</>
)
@ -95,60 +103,80 @@ const ProfileContent = ({
class Profile extends Component {
constructor(props) {
super(props)
this.state = {
screenSize: 0,
visible: true,
}
this.onClickClose = this.onClickClose.bind(this)
}
componentDidMount() {
const { innerWidth } = window
const { match, openModal } = this.props
const { params } = match
const { dapp_name } = params
if (innerWidth >= 1024) {
openModal(dapp_name)
}
onClickClose() {
window.history.back()
}
this.setState({
screenSize: innerWidth,
visible: true,
})
onClickWithdraw(dapp) {
const { onClickWithdraw } = this.props
this.onClickClose()
setTimeout(() => {
onClickWithdraw(dapp)
}, 1)
}
onClickUpdateMetadata(dapp) {
const { onClickUpdateMetadata } = this.props
this.onClickClose()
setTimeout(() => {
onClickUpdateMetadata(dapp)
}, 1)
}
render() {
const { match, dapps } = this.props
const { match, dappState } = this.props
const { dapps } = dappState
const { params } = match
const { dapp_name } = params
let dapp = null
let highestRankedPosition = 1
let categoryPosition = 1
const { screenSize, visible } = this.state
if (
dapps.highestRankedFetched === true &&
dapps.recentlyAddedFetched === true
) {
const dapp = dapps.dapps.find(item =>
item.name.toLowerCase() === dapp_name.toLowerCase() ? item : '',
)
return screenSize >= 1024 ? (
<DesktopScreen visible={visible}>
<ProfileContent {...dapp} />
</DesktopScreen>
) : (
<MobileScreen {...this.props}>
<ProfileContent {...dapp} />
</MobileScreen>
)
for (let i = 0; i < dapps.length; i += 1) {
const item = dapps[i]
if (item.name.toLowerCase() === dapp_name.toLowerCase()) {
highestRankedPosition = i + 1
dapp = item
break
}
}
return null
if (dapp !== null) {
const dappsInCategory = dappState.getDappsByCategory(dapp.category)
for (let i = 0; i < dappsInCategory.length; i += 1) {
const item = dappsInCategory[i]
if (item.id === dapp.id) {
categoryPosition = i + 1
break
}
}
}
return (
<Modal
visible={dapp !== null}
windowClassName={styles.modalWindow}
onClickClose={this.onClickClose}
>
<ProfileContent
{...dapp}
highestRankedPosition={highestRankedPosition}
categoryPosition={categoryPosition}
onClickWithdraw={this.onClickWithdraw.bind(this, dapp)}
onClickUpdateMetadata={this.onClickUpdateMetadata.bind(this, dapp)}
/>
</Modal>
)
}
}
Profile.propTypes = {
visible: PropTypes.bool,
dapp: PropTypes.object,
}
Profile.defaultProps = {
// visible: false,
Profile.propTypes = {
dappState: PropTypes.instanceOf(DappState),
onClickWithdraw: PropTypes.func.isRequired,
onClickUpdateMetadata: PropTypes.func.isRequired,
}
export default Profile

View File

@ -1,5 +1,15 @@
@import '../../common/styles/variables';
.modalWindow {
height: 100%;
}
@media (min-width: $desktop) {
.modalWindow {
height: auto;
}
}
a {
text-decoration: none;
}
@ -142,3 +152,27 @@ a {
.rank_position_text {
margin-top: calculateRem(10);
}
.actions {
display: flex;
justify-content: center;
border-bottom: 1px solid #eef2f5;
box-shadow: inset 0px 1px 0px #eef2f5;
padding: calculateRem(10) calculateRem(20);
.button {
cursor: pointer;
}
.button:first-child {
margin-right: calculateRem(20);
}
}
@media (max-width: 356px) {
.actions {
.button {
font-size: 13px;
}
}
}

View File

@ -2,40 +2,16 @@ import profileState from '../../common/data/profile'
import reducerUtil from '../../common/utils/reducer'
import { history } from '../../common/redux/store'
const DESKTOP_NAVIGATE = 'DESKTOP_NAVIGATE'
const MOBILE_NAVIGATE = 'MOBILE_NAVIGATE'
const PROFILE_NAVIGATE = 'PROFILE_NAVIGATE'
export const toggleProfileModalAction = dapp => {
const { innerWidth } = window
history.push(`/${dapp.trim()}`, dapp)
if (innerWidth <= 1024) {
return {
type: MOBILE_NAVIGATE,
payload: dapp,
}
}
return {
type: DESKTOP_NAVIGATE,
payload: dapp,
type: PROFILE_NAVIGATE,
payload: null,
}
}
const toggleProfileModal = (state, payload) => {
return Object.assign({}, state, {
visible: !state.visible,
dapp: payload,
})
}
const navigateProfile = (state, payload) => {
return Object.assign({}, state, {
visible: false,
dapp: payload,
})
}
const map = {
[DESKTOP_NAVIGATE]: toggleProfileModal,
[MOBILE_NAVIGATE]: navigateProfile,
}
const map = {}
export default reducerUtil(map, profileState)

View File

@ -2,7 +2,7 @@ import { connect } from 'react-redux'
import RecentlyAdded from './RecentlyAdded'
const mapStateToProps = state => ({
dapps: state.dapps.recentlyAdded,
dapps: state.dapps.getRecentlyAdded(),
})
export default connect(mapStateToProps)(RecentlyAdded)

View File

@ -9,15 +9,15 @@
.grid {
display: grid;
grid-auto-flow: column;
grid-auto-flow: row;
grid-auto-columns: calc(90%);
// grid-template-rows: 1fr 1fr 1fr;
grid-template-rows: 1fr;
overflow-x: scroll;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
@media (min-width: $desktop) {
grid-auto-flow: row;
// grid-auto-flow: row;
grid-template-columns: 1fr 1fr;
overflow-x: hidden;
}

View File

@ -17,10 +17,11 @@ import {
submitAction,
switchToRatingAction,
onInputSntValueAction,
updateAction,
} from './Submit.reducer'
const mapStateToProps = state =>
Object.assign(state.submit, { dapps: state.dapps.dapps })
Object.assign(state.submit, { dappState: state.dapps })
const mapDispatchToProps = dispatch => ({
onClickClose: () => dispatch(closeSubmitAction()),
onInputName: name => dispatch(onInputNameAction(name)),
@ -33,6 +34,7 @@ const mapDispatchToProps = dispatch => ({
onImgCancel: () => dispatch(onImgCancelAction()),
onImgDone: imgBase64 => dispatch(onImgDoneAction(imgBase64)),
onSubmit: (dapp, sntValue) => dispatch(submitAction(dapp, sntValue)),
onUpdate: (dappId, metadata) => dispatch(updateAction(dappId, metadata)),
onClickTerms: () => dispatch(push('/terms')),
switchToRating: () => dispatch(switchToRatingAction()),
onInputSntValue: sntValue => dispatch(onInputSntValueAction(sntValue)),

View File

@ -11,6 +11,7 @@ import icon from '../../common/assets/images/icon.svg'
import sntIcon from '../../common/assets/images/SNT.svg'
import 'rc-slider/assets/index.css'
import 'rc-tooltip/assets/bootstrap.css'
import { DappState } from '../../common/data/dapp'
const getCategoryName = category =>
Categories.find(x => x.key === category).value
@ -150,8 +151,18 @@ class Submit extends React.Component {
}
onSubmit() {
const { onSubmit, name, desc, url, img, category, sntValue } = this.props
const dapp = {
const {
onSubmit,
onUpdate,
id,
name,
desc,
url,
img,
category,
sntValue,
} = this.props
const metadata = {
name,
url,
img,
@ -159,7 +170,8 @@ class Submit extends React.Component {
desc,
}
onSubmit(dapp, parseInt(sntValue, 10))
if (id === '') onSubmit(metadata, parseInt(sntValue, 10))
else onUpdate(id, metadata)
}
handleSNTChange(e) {
@ -182,10 +194,11 @@ class Submit extends React.Component {
render() {
const {
dapps,
dappState,
visible_submit,
visible_rating,
onClickClose,
id,
name,
desc,
url,
@ -212,7 +225,7 @@ class Submit extends React.Component {
let afterVoteCategoryPosition = null
if (visible_rating) {
dappsByCategory = dapps.filter(dapp_ => dapp_.category === category)
dappsByCategory = dappState.getDappsByCategory(category)
catPosition = dappsByCategory.length + 1
if (sntValue !== '') {
@ -315,7 +328,7 @@ class Submit extends React.Component {
className={styles.submitButton}
type="submit"
disabled={!canSubmit}
onClick={switchToRating}
onClick={id === '' ? switchToRating : this.onSubmit}
>
Continue
</button>
@ -467,9 +480,11 @@ Submit.propTypes = {
onImgCancel: PropTypes.func.isRequired,
onImgDone: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
onInputSntValue: PropTypes.func.isRequired,
onClickTerms: PropTypes.func.isRequired,
switchToRating: PropTypes.func.isRequired,
dappState: PropTypes.instanceOf(DappState).isRequired,
}
export default Submit

View File

@ -1,199 +1,247 @@
import submitInitialState from '../../common/data/submit';
import reducerUtil from '../../common/utils/reducer';
import submitInitialState from '../../common/data/submit'
import reducerUtil from '../../common/utils/reducer'
import {
onReceiveTransactionInfoAction,
checkTransactionStatusAction,
onStartProgressAction,
hideAction
} from '../TransactionStatus/TransactionStatus.recuder';
import { TYPE_SUBMIT } from '../TransactionStatus/TransactionStatus.utilities';
import { showAlertAction } from '../Alert/Alert.reducer';
hideAction,
} from '../TransactionStatus/TransactionStatus.recuder'
import {
TYPE_SUBMIT,
TYPE_UPDATE,
} from '../TransactionStatus/TransactionStatus.utilities'
import { showAlertAction } from '../Alert/Alert.reducer'
import BlockchainSDK from '../../common/blockchain';
import BlockchainSDK from '../../common/blockchain'
const SHOW_SUBMIT_AFTER_CHECK = 'SUBMIT_SHOW_SUBMIT_AFTER_CHECK';
const CLOSE_SUBMIT = 'SUBMIT_CLOSE_SUBMIT';
const ON_INPUT_NAME = 'SUBMIT_ON_INPUT_NAME';
const ON_INPUT_DESC = 'SUBMIT_ON_INPUT_DESC';
const ON_INPUT_URL = 'SUBMIT_ON_INPUT_URL';
const ON_SELECT_CATEGORY = 'SUBMIT_ON_SELECT_CATEGORY';
const ON_IMG_READ = 'SUBMIT_ON_IMG_READ';
const ON_IMG_ZOOM = 'SUBMIT_ON_IMG_ZOOM';
const ON_IMG_MOVE_CONTROL = 'SUBMIT_ON_IMG_MOVE_CONTROL';
const ON_IMG_MOVE = 'SUBMIT_ON_IMG_MOVE';
const ON_IMG_CANCEL = 'SUBMIT_ON_IMG_CANCEL';
const ON_IMG_DONE = 'SUBMIT_ON_IMG_DONE';
const SHOW_SUBMIT_AFTER_CHECK = 'SUBMIT_SHOW_SUBMIT_AFTER_CHECK'
const CLOSE_SUBMIT = 'SUBMIT_CLOSE_SUBMIT'
const ON_INPUT_NAME = 'SUBMIT_ON_INPUT_NAME'
const ON_INPUT_DESC = 'SUBMIT_ON_INPUT_DESC'
const ON_INPUT_URL = 'SUBMIT_ON_INPUT_URL'
const ON_SELECT_CATEGORY = 'SUBMIT_ON_SELECT_CATEGORY'
const ON_IMG_READ = 'SUBMIT_ON_IMG_READ'
const ON_IMG_ZOOM = 'SUBMIT_ON_IMG_ZOOM'
const ON_IMG_MOVE_CONTROL = 'SUBMIT_ON_IMG_MOVE_CONTROL'
const ON_IMG_MOVE = 'SUBMIT_ON_IMG_MOVE'
const ON_IMG_CANCEL = 'SUBMIT_ON_IMG_CANCEL'
const ON_IMG_DONE = 'SUBMIT_ON_IMG_DONE'
const SWITCH_TO_RATING = 'SUBMIT_SWITCH_TO_RATING';
const ON_INPUT_SNT_VALUE = 'SUBMIT_ON_INPUT_SNT_VALUE';
const SWITCH_TO_RATING = 'SUBMIT_SWITCH_TO_RATING'
const ON_INPUT_SNT_VALUE = 'SUBMIT_ON_INPUT_SNT_VALUE'
export const showSubmitActionAfterCheck = () => {
window.location.hash = 'submit';
const showSubmitAfterCheckAction = dapp => {
window.location.hash = 'submit'
return {
type: SHOW_SUBMIT_AFTER_CHECK,
payload: null
};
};
payload: dapp,
}
}
export const showSubmitAction = () => {
export const showSubmitAction = dapp => {
return (dispatch, getState) => {
const state = getState();
if (state.transactionStatus.progress) {
const state = getState()
if (
state.transactionStatus.progress &&
state.transactionStatus.dappTx !== ''
) {
dispatch(
showAlertAction(
'There is an active transaction. Please wait for it to finish and then you could be able to create your Ðapp'
)
);
} else dispatch(showSubmitActionAfterCheck());
};
};
'There is an active transaction. Please wait for it to finish and then you could be able to create your Ðapp',
),
)
} else if (dapp !== undefined) {
// convert dapp's image from url ot base64
const toDataUrl = (url, callback) => {
const xhr = new XMLHttpRequest()
xhr.onload = () => {
const reader = new FileReader()
reader.onloadend = () => {
callback(reader.result)
}
reader.readAsDataURL(xhr.response)
}
xhr.open('GET', url)
xhr.responseType = 'blob'
xhr.send()
}
toDataUrl(dapp.image, base64 => {
dapp.image = base64
dispatch(showSubmitAfterCheckAction(dapp))
})
} else dispatch(showSubmitAfterCheckAction(dapp))
}
}
export const closeSubmitAction = () => {
window.history.back();
window.history.back()
return {
type: CLOSE_SUBMIT,
payload: null
};
};
payload: null,
}
}
export const onInputNameAction = name => ({
type: ON_INPUT_NAME,
payload: name
});
payload: name,
})
export const onInputDescAction = desc => ({
type: ON_INPUT_DESC,
payload: desc.substring(0, 140)
});
payload: desc.substring(0, 140),
})
export const onInputUrlAction = url => ({
type: ON_INPUT_URL,
payload: url
});
payload: url,
})
export const onSelectCategoryAction = category => ({
type: ON_SELECT_CATEGORY,
payload: category
});
payload: category,
})
export const onImgReadAction = imgBase64 => ({
type: ON_IMG_READ,
payload: imgBase64
});
payload: imgBase64,
})
export const onImgZoomAction = zoom => ({
type: ON_IMG_ZOOM,
payload: zoom
});
payload: zoom,
})
export const onImgMoveControlAction = move => ({
type: ON_IMG_MOVE_CONTROL,
payload: move
});
payload: move,
})
export const onImgMoveAction = (x, y) => ({
type: ON_IMG_MOVE,
payload: { x, y }
});
payload: { x, y },
})
export const onImgCancelAction = () => ({
type: ON_IMG_CANCEL,
payload: null
});
payload: null,
})
export const onImgDoneAction = imgBase64 => ({
type: ON_IMG_DONE,
payload: imgBase64
});
payload: imgBase64,
})
export const submitAction = (dapp, sntValue) => {
return async dispatch => {
dispatch(closeSubmitAction());
dispatch(closeSubmitAction())
dispatch(
onStartProgressAction(
dapp.name,
dapp.img,
'Status is an open source mobile DApp browser and messenger build for #Etherium',
TYPE_SUBMIT
)
);
TYPE_SUBMIT,
),
)
try {
const blockchain = await BlockchainSDK.getInstance();
const blockchain = await BlockchainSDK.getInstance()
const { tx, id } = await blockchain.DiscoverService.createDApp(sntValue, {
name: dapp.name,
url: dapp.url,
description: dapp.desc,
category: dapp.category,
image: dapp.img,
dateAdded: Date.now()
});
dispatch(onReceiveTransactionInfoAction(id, tx));
dispatch(checkTransactionStatusAction(tx));
dateAdded: Date.now(),
})
dispatch(onReceiveTransactionInfoAction(id, tx))
dispatch(checkTransactionStatusAction(tx))
} catch (e) {
dispatch(hideAction());
dispatch(showAlertAction(e.message));
dispatch(hideAction())
dispatch(showAlertAction(e.message))
}
};
};
}
}
export const updateAction = (dappId, metadata) => {
return async dispatch => {
dispatch(closeSubmitAction())
dispatch(
onStartProgressAction(
metadata.name,
metadata.img,
'Status is an open source mobile DApp browser and messenger build for #Etherium',
TYPE_UPDATE,
),
)
try {
const blockchain = await BlockchainSDK.getInstance()
const tx = await blockchain.DiscoverService.setMetadata(dappId, metadata)
dispatch(onReceiveTransactionInfoAction(dappId, tx))
dispatch(checkTransactionStatusAction(tx))
} catch (e) {
dispatch(hideAction())
dispatch(showAlertAction(e.message))
}
}
}
export const switchToRatingAction = () => ({
type: SWITCH_TO_RATING,
paylaod: null
});
paylaod: null,
})
export const onInputSntValueAction = sntValue => ({
type: ON_INPUT_SNT_VALUE,
payload: sntValue
});
payload: sntValue,
})
const showSubmitAfterCheck = state => {
const showSubmitAfterCheck = (state, dapp) => {
return Object.assign({}, state, {
visible_submit: true,
visible_rating: false,
id: '',
name: '',
desc: '',
url: '',
img: '',
category: '',
id: dapp !== undefined ? dapp.id : '',
name: dapp !== undefined ? dapp.name : '',
desc: dapp !== undefined ? dapp.desc : '',
url: dapp !== undefined ? dapp.url : '',
img: dapp !== undefined ? dapp.image : '',
category: dapp !== undefined ? dapp.category : '',
imgControl: false,
imgControlZoom: 0,
imgControlMove: false,
imgControlX: 0,
imgControlY: 0,
sntValue: '0'
});
};
sntValue: '0',
})
}
const closeSubmit = state => {
return Object.assign({}, state, {
visible: false
});
};
visible: false,
})
}
const onInputName = (state, name) => {
return Object.assign({}, state, {
name
});
};
name,
})
}
const onInputDesc = (state, desc) => {
return Object.assign({}, state, {
desc
});
};
desc,
})
}
const onInputUrl = (state, url) => {
return Object.assign({}, state, {
url
});
};
url,
})
}
const onSelectCategory = (state, category) => {
return Object.assign({}, state, {
category
});
};
category,
})
}
const onImgRead = (state, imgBase64) => {
return Object.assign({}, state, {
@ -202,55 +250,55 @@ const onImgRead = (state, imgBase64) => {
imgControlZoom: 0,
imgControlMove: false,
imgControlX: 0,
imgControlY: 0
});
};
imgControlY: 0,
})
}
const onImgZoom = (state, zoom) => {
return Object.assign({}, state, {
imgControlZoom: zoom
});
};
imgControlZoom: zoom,
})
}
const onImgMoveControl = (state, move) => {
return Object.assign({}, state, {
imgControlMove: move
});
};
imgControlMove: move,
})
}
const onImgMove = (state, payload) => {
return Object.assign({}, state, {
imgControlX: payload.x,
imgControlY: payload.y
});
};
imgControlY: payload.y,
})
}
const onImgCancel = state => {
return Object.assign({}, state, {
img: '',
imgControl: false
});
};
imgControl: false,
})
}
const onImgDone = (state, imgBase64) => {
return Object.assign({}, state, {
img: imgBase64,
imgControl: false
});
};
imgControl: false,
})
}
const switchToRating = state => {
return Object.assign({}, state, {
visible_submit: false,
visible_rating: true
});
};
visible_rating: true,
})
}
const onInputSntValue = (state, sntValue) => {
return Object.assign({}, state, {
sntValue
});
};
sntValue,
})
}
const map = {
[SHOW_SUBMIT_AFTER_CHECK]: showSubmitAfterCheck,
@ -266,7 +314,7 @@ const map = {
[ON_IMG_CANCEL]: onImgCancel,
[ON_IMG_DONE]: onImgDone,
[SWITCH_TO_RATING]: switchToRating,
[ON_INPUT_SNT_VALUE]: onInputSntValue
};
[ON_INPUT_SNT_VALUE]: onInputSntValue,
}
export default reducerUtil(map, submitInitialState);
export default reducerUtil(map, submitInitialState)

View File

@ -38,11 +38,11 @@ class TransactionStatus extends React.Component {
<div className={styles.data}>
<div className={styles.name}>
<div className={styles.nameItself}>{dappName}</div>
{!progress && (
<div className={styles.close} onClick={hide}>
+
</div>
)}
{/* {!progress && ( */}
<div className={styles.close} onClick={hide}>
+
</div>
{/* )} */}
</div>
<div className={styles.info}>{txDesc}</div>
{published && <div className={styles.status}> Published</div>}

View File

@ -61,7 +61,7 @@ export const checkTransactionStatusAction = tx => {
transacationStatus.dappId,
)
dapp = Object.assign(dapp.metadata, {
id: dapp.id,
id: transacationStatus.dappId,
sntValue: parseInt(dapp.effectiveBalance, 10),
})
dispatch(onUpdateDappDataAction(dapp))
@ -72,6 +72,7 @@ export const checkTransactionStatusAction = tx => {
}),
)
}
transacationStatus.setTransactionInfo('', '')
break
case 2:
transacationStatus.setProgress(true)
@ -88,7 +89,6 @@ export const checkTransactionStatusAction = tx => {
const hide = state => {
const transacationStatus = transactionStatusFetchedInstance()
transacationStatus.setDappName('')
transacationStatus.setProgress(false)
transacationStatus.setType(TYPE_NONE)
return Object.assign({}, state, transacationStatus)
}

View File

@ -4,13 +4,15 @@ export const TYPE_NONE = 0
export const TYPE_SUBMIT = 1
export const TYPE_UPVOTE = 2
export const TYPE_DOWNVOTE = 3
export const TYPE_WITHDRAW = 4
export const TYPE_UPDATE = 5
class TransactionStatus {
constructor() {
this.dappId = ''
this.dappTx = ''
this.dappId = '' // responsible for background transaction check
this.dappTx = '' // responsible for background transaction check
this.txDesc = ''
this.dappName = ''
this.dappName = '' // responsible for UI visibility
this.dappImg = ''
this.type = TYPE_NONE
this.progress = false

View File

@ -9,10 +9,13 @@ import {
fetchVoteRatingAction,
upVoteAction,
downVoteAction,
learnMoreUpVoteAction,
learnMoreDownVoteAction,
closeLearnMoreAction,
} from './Vote.reducer'
const mapStateToProps = state =>
Object.assign(state.vote, { dapps: state.dapps.dapps })
Object.assign(state.vote, { dappState: state.dapps })
const mapDispatchToProps = dispatch => ({
onClickClose: () => dispatch(closeVoteAction()),
onClickUpvote: () => dispatch(switchToUpvoteAction()),
@ -23,6 +26,9 @@ const mapDispatchToProps = dispatch => ({
},
upVote: (dapp, amount) => dispatch(upVoteAction(dapp, amount)),
downVote: (dapp, amount) => dispatch(downVoteAction(dapp, amount)),
onClickLearnMoreUpVote: () => dispatch(learnMoreUpVoteAction()),
onClickLearnMoreDownVote: () => dispatch(learnMoreDownVoteAction()),
onClickCloseLearnMore: () => dispatch(closeLearnMoreAction()),
})
export default withRouter(

View File

@ -8,6 +8,7 @@ import Categories from '../../common/utils/categories'
import icon from '../../common/assets/images/icon.svg'
import Modal from '../../common/components/Modal'
import { DappModel } from '../../common/utils/models'
import { DappState } from '../../common/data/dapp'
const getCategoryName = category =>
Categories.find(x => x.key === category).value
@ -59,9 +60,14 @@ class Vote extends Component {
onClickClose,
isUpvote,
dapp,
dapps,
dappState,
sntValue,
afterVoteRating,
learnMoreUpVote,
learnMoreDownVote,
onClickLearnMoreUpVote,
onClickLearnMoreDownVote,
onClickCloseLearnMore,
} = this.props
if (dapp === null) {
@ -71,9 +77,7 @@ class Vote extends Component {
//const catPosition = dapp.categoryPosition
// const upvoteSNTcost = currentSNTamount + parseInt(sntValue, 10)
const currentSNTamount = dapp.sntValue
const dappsByCategory = dapps.filter(
dapp_ => dapp_.category === dapp.category,
)
const dappsByCategory = dappState.getDappsByCategory(dapp.category)
let catPosition = dappsByCategory.length
for (let i = 0; i < dappsByCategory.length; ++i) {
@ -100,123 +104,200 @@ class Vote extends Component {
windowClassName={styles.modalWindow}
contentClassName={styles.modalContent}
>
<div className={styles.tabs}>
<button
className={isUpvote ? styles.active : ''}
type="button"
onClick={this.onClickUpvote}
>
UPVOTE
</button>
<button
className={!isUpvote ? styles.active : ''}
type="button"
onClick={this.onClickDownvote}
>
DOWNVOTE
</button>
</div>
<div className={styles.dapp}>
<ReactImageFallback
className={styles.image}
src={dapp.image}
fallbackImage={icon}
alt="App icon"
width={24}
height={24}
/>
{dapp.name}
</div>
<div className={styles.items}>
<div className={styles.itemRow}>
<span className={styles.item}>
<img src={sntIcon} alt="SNT" width="24" height="24" />
{currentSNTamount.toLocaleString()}
</span>
{isUpvote && afterVoteRating !== null && (
<span className={styles.greenBadge}>
{`${(dapp.sntValue + afterVoteRating).toLocaleString()}`}
</span>
)}
{!isUpvote && afterVoteRating !== null && (
<span className={styles.redBadge}>
{`${(dapp.sntValue - afterVoteRating).toLocaleString()}`}
</span>
)}
</div>
<div className={styles.itemRow}>
<span className={styles.item}>
<img
src={CategoriesUtils(dapp.category)}
alt={getCategoryName(dapp.category)}
width="24"
height="24"
{!learnMoreUpVote && !learnMoreDownVote && (
<>
<div className={styles.tabs}>
<button
className={isUpvote ? styles.active : ''}
type="button"
onClick={this.onClickUpvote}
>
UPVOTE
</button>
<button
className={!isUpvote ? styles.active : ''}
type="button"
onClick={this.onClickDownvote}
>
DOWNVOTE
</button>
</div>
<div className={styles.dapp}>
<ReactImageFallback
className={styles.image}
src={dapp.image}
fallbackImage={icon}
alt="App icon"
width={24}
height={24}
/>
{`${getCategoryName(dapp.category)}${catPosition}`}
</span>
{isUpvote &&
afterVoteCategoryPosition !== null &&
afterVoteCategoryPosition !== catPosition && (
<span className={styles.greenBadge}>
{`${afterVoteCategoryPosition}`}
{dapp.name}
</div>
<div className={styles.items}>
<div className={styles.itemRow}>
<span className={styles.item}>
<img src={sntIcon} alt="SNT" width="24" height="24" />
{currentSNTamount.toLocaleString()}
</span>
)}
{!isUpvote &&
afterVoteCategoryPosition !== null &&
afterVoteCategoryPosition !== catPosition && (
<span className={styles.redBadge}>
{`${afterVoteCategoryPosition}`}
{isUpvote && afterVoteRating !== null && (
<span className={styles.greenBadge}>
{`${(dapp.sntValue + afterVoteRating).toLocaleString()}`}
</span>
)}
{!isUpvote && afterVoteRating !== null && (
<span className={styles.redBadge}>
{`${(dapp.sntValue - afterVoteRating).toLocaleString()}`}
</span>
)}
</div>
<div className={styles.itemRow}>
<span className={styles.item}>
<img
src={CategoriesUtils(dapp.category)}
alt={getCategoryName(dapp.category)}
width="24"
height="24"
/>
{`${getCategoryName(dapp.category)}${catPosition}`}
</span>
)}
</div>
</div>
{!isUpvote && (
<div
className={styles.inputArea}
style={{ opacity: sntValue !== '0' ? 1 : 0 }}
>
<span>{sntValue}</span>
</div>
)}
{isUpvote && (
<div className={`${styles.inputArea} ${styles.inputAreaBorder}`}>
<input
type="text"
value={sntValue}
onChange={this.handleChange}
style={{ width: `${21 * Math.max(1, sntValue.length)}px` }}
/>
</div>
)}
{isUpvote &&
afterVoteCategoryPosition !== null &&
afterVoteCategoryPosition !== catPosition && (
<span className={styles.greenBadge}>
{`${afterVoteCategoryPosition}`}
</span>
)}
{!isUpvote &&
afterVoteCategoryPosition !== null &&
afterVoteCategoryPosition !== catPosition && (
<span className={styles.redBadge}>
{`${afterVoteCategoryPosition}`}
</span>
)}
</div>
</div>
{!isUpvote && (
<div
className={styles.inputArea}
style={{ opacity: sntValue !== '0' ? 1 : 0 }}
>
<span>{sntValue}</span>
</div>
)}
{isUpvote && (
<div className={`${styles.inputArea} ${styles.inputAreaBorder}`}>
<input
type="text"
value={sntValue}
onChange={this.handleChange}
style={{ width: `${21 * Math.max(1, sntValue.length)}px` }}
/>
</div>
)}
<div className={styles.footer}>
{isUpvote && (
<p className={styles.disclaimer}>
SNT you spend to upvote is locked in the contract and contributes
directly to {dapp.name}'s ranking.{' '}
<a href="#" target="_blank">
Learn more
</a>
</p>
)}
{!isUpvote && (
<p className={styles.disclaimer}>
SNT you spend to downvote goes directly back to {dapp.name}.
Downvoting moves their DApp down by 1% of the current ranking. The
cost is fixed by our unique bonded curve.{' '}
<a href="#" target="_blank">
Learn more
</a>
</p>
)}
<button
type="submit"
disabled={(!sntValue || sntValue === '0') && isUpvote}
onClick={this.onClickVote}
>
{isUpvote ? 'Upvote' : 'Downvote'}
</button>
</div>
<div className={styles.footer}>
{isUpvote && (
<p className={styles.disclaimer}>
SNT you spend to upvote is locked in the contract and
contributes directly to {dapp.name}'s ranking.{' '}
<a onClick={onClickLearnMoreUpVote}>Learn more</a>
</p>
)}
{!isUpvote && (
<p className={styles.disclaimer}>
SNT you spend to downvote goes directly back to {dapp.name}.
Downvoting moves their DApp down by 1% of the current ranking.
The cost is fixed by our unique bonded curve.{' '}
<a onClick={onClickLearnMoreDownVote}>Learn more</a>
</p>
)}
<button
type="submit"
disabled={(!sntValue || sntValue === '0') && isUpvote}
onClick={this.onClickVote}
>
{isUpvote ? 'Upvote' : 'Downvote'}
</button>
</div>
</>
)}
{learnMoreUpVote && (
<div className={styles.learnMoreCnt}>
<div className={styles.title}>How to submit a ÐApp</div>
<div className={styles.spacing}>
<img src="/images/learn-more-curve.png" />
<p>
This is what the curve you're using really looks like. The more
SNT staked on a DApp, the cheaper it becomes for anyone to
downvote it.
</p>
<p>
However, you can upvote this DApp by any amount of SNT you wish.
SNT you spend is sent directly to the contract and locked up
there. It does not
</p>
<p>
go to Status, the developer of the DApp, or any other middleman.
There are no fees, but once the SNT is spent, you cannot get it
back.
</p>
<p>
What you spend is added directly to the DApp's balance, and the
effect this will have on it's ranking is shown in the previous
screen.
</p>
<p>Welcome to the future of decentralised curation!</p>
</div>
<div className={styles.backButtonCnt}>
<button
type="submit"
onClick={onClickCloseLearnMore}
className={styles.backButton}
>
Back
</button>
</div>
</div>
)}
{learnMoreDownVote && (
<div className={styles.learnMoreCnt}>
<div className={styles.title}>How to submit a ÐApp</div>
<div className={styles.spacing}>
<img src="/images/learn-more-curve.png" />
<p>
This is what the curve you're using really looks like. The more
SNT staked on a DApp, the cheaper it becomes for anyone to
downvote it.
</p>
<p>
You can downvote this DApp, and each downvote will move it 1% of
its current value down the rankings.{' '}
</p>
<p>
SNT you spend is sent directly to the developer of the DApp, so
we can be sure you aren't just trolling them and that, even if
you are, you are required to pay for the privilege. It does not
go to Status or any other middleman. There are no fees, but once
the SNT is spent, you cannot get it back.
</p>
<p>
What you spend is dictated by how much SNT the DApp has already
staked, and the exact numerical effect of moving 1% down in the
rankings is shown in the previous screen.
</p>
<p>Welcome to the future of decentralised curation!</p>
</div>
<div className={styles.backButtonCnt}>
<button
type="submit"
onClick={onClickCloseLearnMore}
className={styles.backButton}
>
Back
</button>
</div>
</div>
)}
</Modal>
)
}
@ -233,6 +314,8 @@ Vote.propTypes = {
visible: PropTypes.bool.isRequired,
sntValue: PropTypes.string.isRequired,
afterVoteRating: PropTypes.number,
learnMoreDownVote: PropTypes.bool.isRequired,
learnMoreUpVote: PropTypes.bool.isRequired,
onClickClose: PropTypes.func.isRequired,
onClickUpvote: PropTypes.func.isRequired,
onClickDownvote: PropTypes.func.isRequired,
@ -240,6 +323,10 @@ Vote.propTypes = {
fetchVoteRating: PropTypes.func.isRequired,
upVote: PropTypes.func.isRequired,
downVote: PropTypes.func.isRequired,
dappState: PropTypes.instanceOf(DappState).isRequired,
onClickLearnMoreUpVote: PropTypes.func.isRequired,
onClickLearnMoreDownVote: PropTypes.func.isRequired,
onClickCloseLearnMore: PropTypes.func.isRequired,
}
export default Vote

View File

@ -213,4 +213,62 @@
.footer .disclaimer a {
text-decoration: none;
color: $link-color;
cursor: pointer;
}
.learnMoreCnt {
display: flex;
flex-direction: column;
font-family: $font;
.title {
line-height: 40px;
text-align: center;
text-transform: uppercase;
font-weight: 600;
font-size: 17px;
border-bottom: 1px solid #f7f7f7;
}
.spacing {
margin: 14px 16px;
img {
width: 100%;
flex: 0 0 auto;
}
p {
line-height: 24px;
text-indent: 32px;
margin: 0;
}
}
.backButtonCnt {
height: 64px;
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
border-top: 1px solid #f7f7f7;
margin-top: auto;
.backButton {
background: $link-color;
border-radius: 8px;
color: #fff;
border: none;
font-family: $font;
padding: calculateRem(11) calculateRem(38);
font-size: calculateRem(15);
cursor: pointer;
}
}
}
@media (min-width: $desktop) {
.learnMoreCnt {
min-height: 512px;
}
}

View File

@ -21,6 +21,9 @@ const SWITCH_TO_DOWNVOTE = 'VOTE_SWITCH_TO_DOWNVOTE'
const ON_INPUT_SNT_VALUE = 'VOTE_ON_INPUT_SNT_VALUE'
const UPDATE_AFTER_UP_VOTING_VALUES = 'VOTE_UPDATE_AFTER_UP_VOTING_VALUES'
const UPDATE_AFTER_DOWN_VOTING_VALUES = 'VOTE_UPDATE_AFTER_DOWN_VOTING_VALUES'
const LEARN_MORE_UPVOTE = 'VOTE_LEARN_MORE_UPVOTE'
const LEARN_MORE_DOWNVOTE = 'VOTE_LEARN_MORE_DOWNVOTE'
const CLOSE_LEARN_MORE = 'VOTE_CLOSE_LEARN_MORE'
export const showUpVoteActionAfterCheck = dapp => {
window.location.hash = 'vote'
@ -41,7 +44,10 @@ export const showDownVoteActionAfterCheck = dapp => {
export const showUpVoteAction = dapp => {
return (dispatch, getState) => {
const state = getState()
if (state.transactionStatus.progress) {
if (
state.transactionStatus.progress &&
state.transactionStatus.dappTx !== ''
) {
dispatch(
showAlertAction(
'There is an active transaction. Please wait for it to finish and then you could be able to vote',
@ -54,7 +60,10 @@ export const showUpVoteAction = dapp => {
export const showDownVoteAction = dapp => {
return (dispatch, getState) => {
const state = getState()
if (state.transactionStatus.progress) {
if (
state.transactionStatus.progress &&
state.transactionStatus.dappTx !== ''
) {
dispatch(
showAlertAction(
'There is an active transaction. Please wait for it to finish and then you could be able to vote',
@ -187,6 +196,21 @@ export const downVoteAction = (dapp, amount) => {
}
}
export const learnMoreUpVoteAction = () => ({
type: LEARN_MORE_UPVOTE,
payload: null,
})
export const learnMoreDownVoteAction = () => ({
type: LEARN_MORE_DOWNVOTE,
payload: null,
})
export const closeLearnMoreAction = () => ({
type: CLOSE_LEARN_MORE,
payload: null,
})
const showUpVoteAfterCheck = (state, dapp) => {
return Object.assign({}, state, {
visible: true,
@ -194,6 +218,8 @@ const showUpVoteAfterCheck = (state, dapp) => {
sntValue: '0',
isUpvote: true,
afterVoteRating: null,
learnMoreUpVote: false,
learnMoreDownVote: false,
})
}
@ -204,6 +230,8 @@ const showDownVoteAfterCheck = (state, dapp) => {
sntValue: '0',
isUpvote: false,
afterVoteRating: null,
learnMoreUpVote: false,
learnMoreDownVote: false,
})
}
@ -211,6 +239,8 @@ const closeVote = state => {
return Object.assign({}, state, {
visible: false,
dapp: null,
learnMoreUpVote: false,
learnMoreDownVote: false,
})
}
@ -250,6 +280,27 @@ const updateAfterDownVotingValues = (state, payload) => {
})
}
const learnMoreUpVote = state => {
return Object.assign({}, state, {
learnMoreUpVote: true,
learnMoreDownVote: false,
})
}
const learnMoreDownVote = state => {
return Object.assign({}, state, {
learnMoreUpVote: false,
learnMoreDownVote: true,
})
}
const closeLearnMore = state => {
return Object.assign({}, state, {
learnMoreUpVote: false,
learnMoreDownVote: false,
})
}
const map = {
[SHOW_UP_VOTE_AFTER_CHECK]: showUpVoteAfterCheck,
[SHOW_DOWN_VOTE_AFTER_CHEECK]: showDownVoteAfterCheck,
@ -259,6 +310,9 @@ const map = {
[ON_INPUT_SNT_VALUE]: onInputSntValue,
[UPDATE_AFTER_UP_VOTING_VALUES]: updateAfterUpVotingValues,
[UPDATE_AFTER_DOWN_VOTING_VALUES]: updateAfterDownVotingValues,
[LEARN_MORE_UPVOTE]: learnMoreUpVote,
[LEARN_MORE_DOWNVOTE]: learnMoreDownVote,
[CLOSE_LEARN_MORE]: closeLearnMore,
}
export default reducerUtil(map, voteInitialState)

View File

@ -0,0 +1,24 @@
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import Withdraw from './Withdraw'
import {
closeWithdrawAction,
onInputSntValueAction,
withdrawAction,
} from './Withdraw.reducer'
const mapStateToProps = state =>
Object.assign(state.withdraw, { dappState: state.dapps })
const mapDispatchToProps = dispatch => ({
onClickClose: () => dispatch(closeWithdrawAction()),
onInputSntValue: sntValue => dispatch(onInputSntValueAction(sntValue)),
onWithdraw: (dapp, sntValue) => dispatch(withdrawAction(dapp, sntValue)),
})
export default withRouter(
connect(
mapStateToProps,
mapDispatchToProps,
)(Withdraw),
)

View File

@ -0,0 +1,157 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactImageFallback from 'react-image-fallback'
import styles from './Withdraw.module.scss'
import Modal from '../../common/components/Modal'
import CategoriesUtils from '../Categories/Categories.utils'
import Categories from '../../common/utils/categories'
import icon from '../../common/assets/images/icon.svg'
import sntIcon from '../../common/assets/images/SNT.svg'
import 'rc-slider/assets/index.css'
import 'rc-tooltip/assets/bootstrap.css'
import DappModel from '../../common/data/dapp'
const getCategoryName = category =>
Categories.find(x => x.key === category).value
class Withdraw extends React.Component {
constructor(props) {
super(props)
this.onWithdraw = this.onWithdraw.bind(this)
this.handleSNTChange = this.handleSNTChange.bind(this)
}
onWithdraw() {
const { dapp, sntValue, onWithdraw } = this.props
onWithdraw(dapp, parseInt(sntValue, 10))
}
handleSNTChange(e) {
const { dapp } = this.props
const { value } = e.target
if (value !== '' && /^[1-9][0-9]*$/.test(value) === false) return
const intValue = value === '' ? 0 : parseInt(value, 10)
if (intValue > 1571296) return
if (intValue > dapp.sntValue) return
const { onInputSntValue } = this.props
onInputSntValue(value)
}
render() {
const { dappState, dapp, visible, onClickClose, sntValue } = this.props
if (dapp === null)
return <Modal visible={false} onClickClose={onClickClose} />
const currentSNTamount = dapp.sntValue
const dappsByCategory = dappState.getDappsByCategory(dapp.category)
const afterVoteRating =
dapp.sntValue - (sntValue !== '' ? parseInt(sntValue, 10) : 0)
let catPosition = dappsByCategory.length
for (let i = 0; i < dappsByCategory.length; ++i) {
if (dapp.id === dappsByCategory[i].id) {
catPosition = i + 1
break
}
}
let afterVoteCategoryPosition = 1
for (let i = 0; i < dappsByCategory.length; ++i) {
if (dappsByCategory[i].id === dapp.id) continue
if (dappsByCategory[i].sntValue < afterVoteRating) break
afterVoteCategoryPosition++
}
return (
<Modal
visible={visible && window.location.hash === '#withdraw'}
onClickClose={onClickClose}
windowClassName={styles.modalWindow}
>
<div className={styles.title}>Withdraw</div>
<div className={styles.dapp}>
<ReactImageFallback
className={styles.image}
src={dapp.image}
fallbackImage={icon}
alt="App icon"
width={24}
height={24}
/>
{dapp.name}
</div>
<div className={styles.items}>
<div className={styles.itemRow}>
<span className={styles.item}>
<img src={sntIcon} alt="SNT" width="24" height="24" />
{currentSNTamount.toLocaleString()}
</span>
{afterVoteRating !== null &&
afterVoteRating !== currentSNTamount && (
<span className={styles.redBadge}>
{`${afterVoteRating.toLocaleString()}`}
</span>
)}
</div>
<div className={styles.itemRow}>
<span className={styles.item}>
<img
src={CategoriesUtils(dapp.category)}
alt={getCategoryName(dapp.category)}
width="24"
height="24"
/>
{`${getCategoryName(dapp.category)}${catPosition}`}
</span>
{afterVoteCategoryPosition !== null &&
afterVoteCategoryPosition !== catPosition && (
<span className={styles.redBadge}>
{`${afterVoteCategoryPosition}`}
</span>
)}
</div>
</div>
<div className={`${styles.inputArea} ${styles.inputAreaBorder}`}>
<input
type="text"
value={sntValue}
onChange={this.handleSNTChange}
style={{ width: `${21 * Math.max(1, sntValue.length)}px` }}
/>
</div>
<div className={styles.footer}>
<p className={styles.disclaimer}>
SNT you spend to rank your DApp is locked in the store. You can earn
back through votes, or withdraw, the majority of this SNT at any
time.
</p>
<button
type="submit"
disabled={!sntValue || sntValue === '0'}
onClick={this.onWithdraw}
>
Withdraw
</button>
</div>
</Modal>
)
}
}
Withdraw.defaultProps = {
dapp: null,
}
Withdraw.propTypes = {
visible: PropTypes.bool.isRequired,
dapp: PropTypes.instanceOf(DappModel),
sntValue: PropTypes.string.isRequired,
onClickClose: PropTypes.func.isRequired,
onWithdraw: PropTypes.func.isRequired,
onInputSntValue: PropTypes.func.isRequired,
}
export default Withdraw

View File

@ -0,0 +1,412 @@
@import '../../common/styles/variables';
.modalWindow {
height: 100%;
display: flex;
flex-direction: column;
font-family: $font;
}
@media (min-width: $desktop) {
.modalWindow {
height: auto;
}
}
.modalContentFullScreen {
display: flex;
flex-direction: column;
flex: 1 1 auto;
}
@media (min-width: $desktop) {
.modalContentFullScreen {
height: 512px;
}
}
.cntWithImgControl {
flex: 1 1 auto;
display: flex;
flex-direction: column;
}
.title {
line-height: 40px;
text-align: center;
text-transform: uppercase;
font-size: 13px;
border-bottom: 1px solid #f7f7f7;
}
.withImgControl {
display: none;
}
.hasTransaction {
text-align: center;
padding: 16px 26px;
}
.block {
padding: 0 16px 16px 16px;
.labelRow {
height: 42px;
display: flex;
align-items: center;
span:nth-child(1) {
font-size: 15px;
}
span:nth-child(2) {
color: #939ba1;
font-size: 12px;
margin-left: auto;
}
}
.input {
width: 100%;
height: 52px;
box-sizing: border-box;
padding: 15px;
border: none;
border-radius: 8px;
margin: 0;
background: #eef2f5;
}
textarea.input {
height: 92px;
resize: none;
}
.imgCnt {
width: 140px;
height: 140px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
border-radius: 50%;
border: 1px dashed #939ba1;
margin: 16px auto;
background: #eef2f5;
span {
color: #939ba1;
font-size: 15px;
}
.imgHolder {
width: inherit;
height: inherit;
position: absolute;
left: 0;
top: 0;
border-radius: inherit;
background-size: cover;
background-position: center;
}
input.uploader {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
opacity: 0;
cursor: pointer;
}
}
.imgInfo {
line-height: 22px;
color: #939ba1;
font-size: 15px;
}
.categorySelector {
position: relative;
}
.categorySelector > div {
width: 100%;
top: auto;
bottom: 0;
margin: 0;
}
.categorySelector > button {
width: 100%;
margin: 0;
transition-property: background;
transition-duration: 0.4s;
}
.categorySelectorEmpty > button {
background: #939ba1;
}
}
.blockSubmit {
display: flex;
flex-direction: column;
margin: 40px 0 16px 0;
.terms {
line-height: 22px;
color: #939ba1;
font-size: 15px;
cursor: pointer;
a {
color: $link-color;
text-decoration: none;
}
}
.submitButton {
background: $link-color;
border-radius: 8px;
color: #fff;
margin: calculateRem(26) auto 0 auto;
border: none;
font-family: $font;
padding: calculateRem(11) calculateRem(38);
font-size: calculateRem(15);
cursor: pointer;
}
.submitButton:disabled,
.submitButton[disabled] {
background: $text-color;
}
}
.imgControl {
display: flex;
align-items: center;
flex-direction: column;
flex: 1 1 auto;
.imgCanvasCnt {
width: 100%;
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: center;
.imgCanvas {
width: 160px;
height: 160px;
border-radius: 50%;
pointer-events: none;
}
}
.controls {
width: 100%;
border-top: 1px solid #eef2f5;
.slider {
height: 74px;
display: flex;
align-items: center;
* {
flex: 1 1 auto;
}
.minZoom {
width: 10px;
height: 10px;
flex: 0 0 auto;
border: 1px solid #939ba1;
border-radius: 3px;
margin: auto 30px auto 15px;
}
.maxZoom {
width: 18px;
height: 18px;
flex: 0 0 auto;
border: 1px solid #939ba1;
border-radius: 3px;
margin: auto 11px auto 26px;
}
}
.actionsCnt {
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 0 16px;
.button {
height: 44px;
border-radius: 8px;
border: none;
border-radius: 22px;
font-family: $font;
padding: calculateRem(11) calculateRem(38);
font-size: calculateRem(15);
cursor: pointer;
}
.cancelButton {
color: $link-color;
background: #eceffc;
}
.doneButton {
color: #fff;
background: $link-color;
}
}
}
}
/* rating */
.dapp {
height: 48px;
display: flex;
align-items: center;
font-family: $font;
font-weight: 500;
padding: 0 calculateRem(12);
}
.dapp .image {
max-width: 24px;
max-height: 24px;
border-radius: 50%;
margin-right: calculateRem(12);
}
.items {
display: flex;
flex-direction: column;
font-family: $font;
}
.items .itemRow {
height: 32px;
display: flex;
align-items: center;
padding: 0 calculateRem(12);
}
.items .item {
display: flex;
align-items: center;
}
.items .item img {
margin-right: calculateRem(12);
}
.badge {
border-radius: 24px;
color: #ffffff;
font-family: $font;
font-size: calculateRem(15);
margin-right: calculateRem(16);
margin-left: auto;
padding: calculateRem(3) calculateRem(10);
}
.greenBadge {
@extend .badge;
background: #44d058;
}
.redBadge {
@extend .badge;
background: #f00;
}
.inputArea {
width: calc(100% - 2 * 16px);
height: 64px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
margin: auto;
}
.inputArea.inputAreaBorder {
border-bottom: 1px solid #eef2f5;
}
.inputArea input {
width: 19px;
border: none;
text-align: center;
font-size: calculateRem(32);
line-height: calculateRem(28);
font-family: $font;
margin-right: calculateRem(6);
}
.inputArea input:focus {
outline: none;
}
.inputArea::after {
transition: all 0.05s ease-in-out;
content: 'SNT';
color: #939ba1;
font-size: calculateRem(32);
line-height: calculateRem(28);
font-family: $font;
}
.inputArea span {
font-size: calculateRem(32);
line-height: calculateRem(28);
font-family: $font;
margin-right: calculateRem(6);
}
.footer {
text-align: center;
}
.footer button {
background: $link-color;
border-radius: 8px;
color: #fff;
margin: calculateRem(10) auto;
border: none;
font-family: $font;
padding: calculateRem(11) calculateRem(38);
font-size: calculateRem(15);
cursor: pointer;
}
.footer button:disabled,
.footer button[disabled] {
background: $text-color;
}
.footer .disclaimer {
font-size: calculateRem(15);
color: $text-color;
line-height: 22px;
font-family: $font;
padding: calculateRem(16);
border-bottom: 1px solid #eef2f5;
margin: 0;
}
.footer .disclaimer a {
text-decoration: none;
color: $link-color;
}

View File

@ -0,0 +1,105 @@
import withdrawInitialState from '../../common/data/withdraw'
import reducerUtil from '../../common/utils/reducer'
import {
onReceiveTransactionInfoAction,
checkTransactionStatusAction,
onStartProgressAction,
hideAction,
} from '../TransactionStatus/TransactionStatus.recuder'
import { TYPE_WITHDRAW } from '../TransactionStatus/TransactionStatus.utilities'
import { showAlertAction } from '../Alert/Alert.reducer'
import BlockchainSDK from '../../common/blockchain'
const SHOW_WITHDRAW_AFTER_CHECK = 'WITHDRAW_SHOW_WITHDRAW_AFTER_CHECK'
const CLOSE_WITHDRAW = 'WITHDRAW_CLOSE_WITHDRAW'
const ON_INPUT_SNT_VALUE = 'WITHDRAW_ON_INPUT_SNT_VALUE'
export const showWithdrawAfterCheckAction = dapp => {
window.location.hash = 'withdraw'
return {
type: SHOW_WITHDRAW_AFTER_CHECK,
payload: dapp,
}
}
export const showWithdrawAction = dapp => {
return (dispatch, getState) => {
const state = getState()
if (
state.transactionStatus.progress &&
state.transactionStatus.dappTx !== ''
) {
dispatch(
showAlertAction(
'There is an active transaction. Please wait for it to finish and then you could be able to create your Ðapp',
),
)
} else dispatch(showWithdrawAfterCheckAction(dapp))
}
}
export const closeWithdrawAction = () => {
window.history.back()
return {
type: CLOSE_WITHDRAW,
payload: null,
}
}
export const withdrawAction = (dapp, sntValue) => {
return async dispatch => {
dispatch(closeWithdrawAction())
dispatch(
onStartProgressAction(
dapp.name,
dapp.image,
'Status is an open source mobile DApp browser and messenger build for #Etherium',
TYPE_WITHDRAW,
),
)
try {
const blockchain = await BlockchainSDK.getInstance()
const tx = await blockchain.DiscoverService.withdraw(dapp.id, sntValue)
dispatch(onReceiveTransactionInfoAction(dapp.id, tx))
dispatch(checkTransactionStatusAction(tx))
} catch (e) {
dispatch(hideAction())
dispatch(showAlertAction(e.message))
}
}
}
export const onInputSntValueAction = sntValue => ({
type: ON_INPUT_SNT_VALUE,
payload: sntValue,
})
const showWithdrawAfterCheck = (state, dapp) => {
return Object.assign({}, state, {
visible: true,
dapp,
sntValue: dapp.sntValue.toString(),
})
}
const closeWithdraw = state => {
return Object.assign({}, state, {
visible: false,
dapp: null,
})
}
const onInputSntValue = (state, sntValue) => {
return Object.assign({}, state, {
sntValue,
})
}
const map = {
[SHOW_WITHDRAW_AFTER_CHECK]: showWithdrawAfterCheck,
[CLOSE_WITHDRAW]: closeWithdraw,
[ON_INPUT_SNT_VALUE]: onInputSntValue,
}
export default reducerUtil(map, withdrawInitialState)

View File

@ -0,0 +1,3 @@
import Withdraw from './Withdraw.container'
export default Withdraw