From 252d9c055f0debedc882c6ec92761ac2540fd926 Mon Sep 17 00:00:00 2001 From: Kamen Stoykov Date: Fri, 31 May 2019 14:01:31 +0300 Subject: [PATCH 1/3] cache + withdraw --- .prettierrc | 5 + package.json | 3 +- .../discover-service/discover-service.js | 13 + src/common/components/DappList/DappList.jsx | 68 ++- .../DappListItem/DappListItem.container.js | 2 - .../components/DappListItem/DappListItem.jsx | 10 +- .../DappListItem/DappListItem.module.scss | 10 + src/common/data/dapp.js | 126 ++++++ src/common/data/database.js | 35 ++ src/common/data/withdraw.js | 7 + src/common/redux/reducers.js | 2 + src/common/utils/models.js | 4 +- src/modules/App/Router.jsx | 5 +- src/modules/Dapps/Dapps.container.js | 14 +- src/modules/Dapps/Dapps.jsx | 44 +- src/modules/Dapps/Dapps.module.scss | 12 +- src/modules/Dapps/Dapps.reducer.js | 362 +++------------ src/modules/Filtered/Filtered.container.js | 13 +- src/modules/Filtered/Filtered.jsx | 64 +-- .../HighestRanked/HighestRanked.container.js | 2 +- .../HighestRanked/HighestRanked.module.scss | 6 +- src/modules/Home/Home.jsx | 3 +- src/modules/Profile/Profile.container.js | 17 +- src/modules/Profile/Profile.jsx | 127 +++--- src/modules/Profile/Profile.module.scss | 35 ++ src/modules/Profile/Profile.reducer.js | 32 +- .../RecentlyAdded/RecentlyAdded.container.js | 2 +- .../RecentlyAdded/RecentlyAdded.module.scss | 6 +- src/modules/Submit/Submit.container.js | 2 +- src/modules/Submit/Submit.jsx | 6 +- src/modules/Submit/Submit.reducer.js | 5 +- .../TransactionStatus/TransactionStatus.jsx | 10 +- .../TransactionStatus.recuder.js | 4 +- .../TransactionStatus.utilities.js | 7 +- src/modules/Vote/Vote.container.js | 2 +- src/modules/Vote/Vote.jsx | 8 +- src/modules/Vote/Vote.reducer.js | 5 +- src/modules/Withdraw/Withdraw.container.js | 20 + src/modules/Withdraw/Withdraw.jsx | 161 +++++++ src/modules/Withdraw/Withdraw.module.scss | 412 ++++++++++++++++++ src/modules/Withdraw/Withdraw.reducer.js | 105 +++++ src/modules/Withdraw/index.js | 3 + 42 files changed, 1197 insertions(+), 582 deletions(-) create mode 100644 .prettierrc create mode 100644 src/common/data/dapp.js create mode 100644 src/common/data/database.js create mode 100644 src/common/data/withdraw.js create mode 100644 src/modules/Withdraw/Withdraw.container.js create mode 100644 src/modules/Withdraw/Withdraw.jsx create mode 100644 src/modules/Withdraw/Withdraw.module.scss create mode 100644 src/modules/Withdraw/Withdraw.reducer.js create mode 100644 src/modules/Withdraw/index.js diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a933e43 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "trailingComma": "all", + "semi": false, + "singleQuote": true +} diff --git a/package.json b/package.json index b30cd20..adb1f4e 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,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", diff --git a/src/common/blockchain/services/contracts-services/discover-service/discover-service.js b/src/common/blockchain/services/contracts-services/discover-service/discover-service.js index e2f1d99..762082b 100644 --- a/src/common/blockchain/services/contracts-services/discover-service/discover-service.js +++ b/src/common/blockchain/services/contracts-services/discover-service/discover-service.js @@ -53,6 +53,19 @@ class DiscoverService extends BlockchainService { } } + async getDAppByIndexWithMetadata(index) { + try { + const dapp = await DiscoverContract.methods + .dapps(index) + .call({ from: this.sharedContext.account }) + + dapp.metadata = await ipfsSDK.retrieveDAppMetadataByHash(dapp.metadata) + return dapp + } catch (error) { + throw new Error(`Error fetching dapps. Details: ${error.message}`) + } + } + async getDAppById(id) { let dapp try { diff --git a/src/common/components/DappList/DappList.jsx b/src/common/components/DappList/DappList.jsx index 9d0f778..43c6b75 100644 --- a/src/common/components/DappList/DappList.jsx +++ b/src/common/components/DappList/DappList.jsx @@ -3,20 +3,60 @@ 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) => ( - - )) - ) +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) => ( + + )) + ) + } } DappList.defaultProps = { diff --git a/src/common/components/DappListItem/DappListItem.container.js b/src/common/components/DappListItem/DappListItem.container.js index 2999999..62a47a1 100644 --- a/src/common/components/DappListItem/DappListItem.container.js +++ b/src/common/components/DappListItem/DappListItem.container.js @@ -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)) diff --git a/src/common/components/DappListItem/DappListItem.jsx b/src/common/components/DappListItem/DappListItem.jsx index 65a2694..9143aee 100644 --- a/src/common/components/DappListItem/DappListItem.jsx +++ b/src/common/components/DappListItem/DappListItem.jsx @@ -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,7 @@ const DappListItem = props => { } return ( -
+
{isRanked &&
{position}
}
{ className={styles.description} style={{ WebkitBoxOrient: 'vertical' }} > - {description} + {desc}

@@ -84,12 +84,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, diff --git a/src/common/components/DappListItem/DappListItem.module.scss b/src/common/components/DappListItem/DappListItem.module.scss index 68bd085..06a73f1 100644 --- a/src/common/components/DappListItem/DappListItem.module.scss +++ b/src/common/components/DappListItem/DappListItem.module.scss @@ -1,5 +1,14 @@ @import '../../styles/variables'; +.dappListItem { + transition-property: opacity; + transition-duration: 1s; +} + +.dappListItem.transparent { + opacity: 0; +} + .listItem { font-family: $font; background: $background; @@ -7,6 +16,7 @@ height: calculateRem(145); margin: 0 calculateRem(16) calculateRem(11) calculateRem(16); position: relative; + } .rankedListItem { diff --git a/src/common/data/dapp.js b/src/common/data/dapp.js new file mode 100644 index 0000000..4c728bb --- /dev/null +++ b/src/common/data/dapp.js @@ -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 } diff --git a/src/common/data/database.js b/src/common/data/database.js new file mode 100644 index 0000000..45dae44 --- /dev/null +++ b/src/common/data/database.js @@ -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) + } +} diff --git a/src/common/data/withdraw.js b/src/common/data/withdraw.js new file mode 100644 index 0000000..cb12a53 --- /dev/null +++ b/src/common/data/withdraw.js @@ -0,0 +1,7 @@ +const withdraw = { + visible: false, + dapp: null, + sntValue: '0', +} + +export default withdraw diff --git a/src/common/redux/reducers.js b/src/common/redux/reducers.js index 6eba451..94523dc 100644 --- a/src/common/redux/reducers.js +++ b/src/common/redux/reducers.js @@ -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, }) diff --git a/src/common/utils/models.js b/src/common/utils/models.js index b4cd760..7e9ad44 100644 --- a/src/common/utils/models.js +++ b/src/common/utils/models.js @@ -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, } diff --git a/src/modules/App/Router.jsx b/src/modules/App/Router.jsx index 8a13b60..720b316 100644 --- a/src/modules/App/Router.jsx +++ b/src/modules/App/Router.jsx @@ -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 { - + , , , , , , + , + ] } } diff --git a/src/modules/Dapps/Dapps.container.js b/src/modules/Dapps/Dapps.container.js index 152f04f..222eb74 100644 --- a/src/modules/Dapps/Dapps.container.js +++ b/src/modules/Dapps/Dapps.container.js @@ -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) diff --git a/src/modules/Dapps/Dapps.jsx b/src/modules/Dapps/Dapps.jsx index 879b0e0..f517c49 100644 --- a/src/modules/Dapps/Dapps.jsx +++ b/src/modules/Dapps/Dapps.jsx @@ -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)} />
- - {dappsCategoryMap.get(category).canFetch() && ( -
- Load more dApps from {category}{' '} -
- )} +
))} @@ -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, } diff --git a/src/modules/Dapps/Dapps.module.scss b/src/modules/Dapps/Dapps.module.scss index f960085..fbd9829 100644 --- a/src/modules/Dapps/Dapps.module.scss +++ b/src/modules/Dapps/Dapps.module.scss @@ -3,14 +3,4 @@ .list { 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; -} +} \ No newline at end of file diff --git a/src/modules/Dapps/Dapps.reducer.js b/src/modules/Dapps/Dapps.reducer.js index dba9995..c4956ee 100644 --- a/src/modules/Dapps/Dapps.reducer.js +++ b/src/modules/Dapps/Dapps.reducer.js @@ -1,165 +1,68 @@ -// 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 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_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_DAPPS = 'DAPPS_ON_UPDATE_DAPPS' 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 onUpdateDappsAction = dappState => ({ + type: ON_UPDATE_DAPPS, + payload: dappState, }) -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 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())) + } } } @@ -168,169 +71,18 @@ export const onUpdateDappDataAction = dapp => ({ 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 - } - } -} - 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, -} - -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, + [ON_UPDATE_DAPPS]: onUpdateDapps, } export default reducerUtil(map, dappsInitialState) diff --git a/src/modules/Filtered/Filtered.container.js b/src/modules/Filtered/Filtered.container.js index 69a84e5..0ddfd40 100644 --- a/src/modules/Filtered/Filtered.container.js +++ b/src/modules/Filtered/Filtered.container.js @@ -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) diff --git a/src/modules/Filtered/Filtered.jsx b/src/modules/Filtered/Filtered.jsx index f5c3e11..05ed930 100644 --- a/src/modules/Filtered/Filtered.jsx +++ b/src/modules/Filtered/Filtered.jsx @@ -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 ( <> - +
- +
) @@ -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, diff --git a/src/modules/HighestRanked/HighestRanked.container.js b/src/modules/HighestRanked/HighestRanked.container.js index 2a70388..b691d76 100644 --- a/src/modules/HighestRanked/HighestRanked.container.js +++ b/src/modules/HighestRanked/HighestRanked.container.js @@ -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) diff --git a/src/modules/HighestRanked/HighestRanked.module.scss b/src/modules/HighestRanked/HighestRanked.module.scss index b97272a..eff4d83 100644 --- a/src/modules/HighestRanked/HighestRanked.module.scss +++ b/src/modules/HighestRanked/HighestRanked.module.scss @@ -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; } diff --git a/src/modules/Home/Home.jsx b/src/modules/Home/Home.jsx index e108eb0..c037de7 100644 --- a/src/modules/Home/Home.jsx +++ b/src/modules/Home/Home.jsx @@ -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 ( <> diff --git a/src/modules/Profile/Profile.container.js b/src/modules/Profile/Profile.container.js index bbfa559..e14f6f7 100644 --- a/src/modules/Profile/Profile.container.js +++ b/src/modules/Profile/Profile.container.js @@ -1,15 +1,18 @@ 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' -const mapStateToProps = state => state +const mapStateToProps = state => ({ dappState: state.dapps }) const mapDispatchToProps = dispatch => ({ - openModal: dapp => dispatch(toggleProfileModalAction(dapp)), + onClickWithdraw: dapp => dispatch(showWithdrawAction(dapp)), }) -export default connect( - mapStateToProps, - mapDispatchToProps, -)(Profile) +export default withRouter( + connect( + mapStateToProps, + mapDispatchToProps, + )(Profile), +) diff --git a/src/modules/Profile/Profile.jsx b/src/modules/Profile/Profile.jsx index 4d85fd3..963d867 100644 --- a/src/modules/Profile/Profile.jsx +++ b/src/modules/Profile/Profile.jsx @@ -1,27 +1,22 @@ 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 {props.children} -} - -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, }) => { return ( <> @@ -37,13 +32,13 @@ const ProfileContent = ({

{name}

{category} -
+ Open
Description -

{description}

+

{desc}

Ranking
- {position} + {categoryPosition}
- {position} in {category} + {categoryPosition} in {category}
- {position} + {highestRankedPosition} - {position} in highest ranked DApps + {highestRankedPosition} in highest ranked DApps
+
+
+ Edit metadata +
+
+ Withdraw SNT +
+
) } 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) } 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 ? ( - - - - ) : ( - - - - ) + 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 ( + + + + ) } } -Profile.propTypes = { - visible: PropTypes.bool, - dapp: PropTypes.object, -} -Profile.defaultProps = { - // visible: false, +Profile.propTypes = { + dappState: PropTypes.instanceOf(DappState), + onClickWithdraw: PropTypes.func.isRequired, } export default Profile diff --git a/src/modules/Profile/Profile.module.scss b/src/modules/Profile/Profile.module.scss index aa1a20b..2345e75 100644 --- a/src/modules/Profile/Profile.module.scss +++ b/src/modules/Profile/Profile.module.scss @@ -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,28 @@ 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; + } +} + +} \ No newline at end of file diff --git a/src/modules/Profile/Profile.reducer.js b/src/modules/Profile/Profile.reducer.js index bb049cd..dfbca25 100644 --- a/src/modules/Profile/Profile.reducer.js +++ b/src/modules/Profile/Profile.reducer.js @@ -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) diff --git a/src/modules/RecentlyAdded/RecentlyAdded.container.js b/src/modules/RecentlyAdded/RecentlyAdded.container.js index a9479fa..a4912e0 100644 --- a/src/modules/RecentlyAdded/RecentlyAdded.container.js +++ b/src/modules/RecentlyAdded/RecentlyAdded.container.js @@ -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) diff --git a/src/modules/RecentlyAdded/RecentlyAdded.module.scss b/src/modules/RecentlyAdded/RecentlyAdded.module.scss index 9b2fa3a..eff4d83 100644 --- a/src/modules/RecentlyAdded/RecentlyAdded.module.scss +++ b/src/modules/RecentlyAdded/RecentlyAdded.module.scss @@ -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; } diff --git a/src/modules/Submit/Submit.container.js b/src/modules/Submit/Submit.container.js index 79060ae..65e2b1a 100644 --- a/src/modules/Submit/Submit.container.js +++ b/src/modules/Submit/Submit.container.js @@ -20,7 +20,7 @@ import { } 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)), diff --git a/src/modules/Submit/Submit.jsx b/src/modules/Submit/Submit.jsx index e2a152c..b175995 100644 --- a/src/modules/Submit/Submit.jsx +++ b/src/modules/Submit/Submit.jsx @@ -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 @@ -182,7 +183,7 @@ class Submit extends React.Component { render() { const { - dapps, + dappState, visible_submit, visible_rating, onClickClose, @@ -212,7 +213,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 !== '') { @@ -470,6 +471,7 @@ Submit.propTypes = { onInputSntValue: PropTypes.func.isRequired, onClickTerms: PropTypes.func.isRequired, switchToRating: PropTypes.func.isRequired, + dappState: PropTypes.instanceOf(DappState).isRequired, } export default Submit diff --git a/src/modules/Submit/Submit.reducer.js b/src/modules/Submit/Submit.reducer.js index e2030fc..17238bc 100644 --- a/src/modules/Submit/Submit.reducer.js +++ b/src/modules/Submit/Submit.reducer.js @@ -38,7 +38,10 @@ export const showSubmitActionAfterCheck = () => { export const showSubmitAction = () => { 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 create your Ðapp', diff --git a/src/modules/TransactionStatus/TransactionStatus.jsx b/src/modules/TransactionStatus/TransactionStatus.jsx index 2b4e3b0..01207de 100644 --- a/src/modules/TransactionStatus/TransactionStatus.jsx +++ b/src/modules/TransactionStatus/TransactionStatus.jsx @@ -38,11 +38,11 @@ class TransactionStatus extends React.Component {
{dappName}
- {!progress && ( -
- + -
- )} + {/* {!progress && ( */} +
+ + +
+ {/* )} */}
{txDesc}
{published &&
✓ Published
} diff --git a/src/modules/TransactionStatus/TransactionStatus.recuder.js b/src/modules/TransactionStatus/TransactionStatus.recuder.js index 606900d..3fcc915 100644 --- a/src/modules/TransactionStatus/TransactionStatus.recuder.js +++ b/src/modules/TransactionStatus/TransactionStatus.recuder.js @@ -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) } diff --git a/src/modules/TransactionStatus/TransactionStatus.utilities.js b/src/modules/TransactionStatus/TransactionStatus.utilities.js index feb85ca..4793246 100644 --- a/src/modules/TransactionStatus/TransactionStatus.utilities.js +++ b/src/modules/TransactionStatus/TransactionStatus.utilities.js @@ -4,13 +4,14 @@ 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 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 diff --git a/src/modules/Vote/Vote.container.js b/src/modules/Vote/Vote.container.js index aec62dc..d1ecfa7 100644 --- a/src/modules/Vote/Vote.container.js +++ b/src/modules/Vote/Vote.container.js @@ -12,7 +12,7 @@ import { } 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()), diff --git a/src/modules/Vote/Vote.jsx b/src/modules/Vote/Vote.jsx index d25d4ce..2122a74 100644 --- a/src/modules/Vote/Vote.jsx +++ b/src/modules/Vote/Vote.jsx @@ -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,7 +60,7 @@ class Vote extends Component { onClickClose, isUpvote, dapp, - dapps, + dappState, sntValue, afterVoteRating, } = this.props @@ -71,9 +72,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) { @@ -240,6 +239,7 @@ Vote.propTypes = { fetchVoteRating: PropTypes.func.isRequired, upVote: PropTypes.func.isRequired, downVote: PropTypes.func.isRequired, + dappState: PropTypes.instanceOf(DappState).isRequired, } export default Vote diff --git a/src/modules/Vote/Vote.reducer.js b/src/modules/Vote/Vote.reducer.js index aa560f3..7d41159 100644 --- a/src/modules/Vote/Vote.reducer.js +++ b/src/modules/Vote/Vote.reducer.js @@ -41,7 +41,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', diff --git a/src/modules/Withdraw/Withdraw.container.js b/src/modules/Withdraw/Withdraw.container.js new file mode 100644 index 0000000..c3c6091 --- /dev/null +++ b/src/modules/Withdraw/Withdraw.container.js @@ -0,0 +1,20 @@ +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), +) diff --git a/src/modules/Withdraw/Withdraw.jsx b/src/modules/Withdraw/Withdraw.jsx new file mode 100644 index 0000000..760bd46 --- /dev/null +++ b/src/modules/Withdraw/Withdraw.jsx @@ -0,0 +1,161 @@ +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 + + 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 ( + +
Withdraw
+
+ + {dapp.name} +
+
+
+ + SNT + {currentSNTamount.toLocaleString()} + + {afterVoteRating !== null && + afterVoteRating !== currentSNTamount && ( + + {`${afterVoteRating.toLocaleString()} ↓`} + + )} +
+
+ + {getCategoryName(dapp.category)} + {`${getCategoryName(dapp.category)} №${catPosition}`} + + {afterVoteCategoryPosition !== null && + afterVoteCategoryPosition !== catPosition && ( + + {`№${afterVoteCategoryPosition} ↓`} + + )} +
+
+
+ +
+
+

+ 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. +

+ +
+
+ ) + } +} + +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 diff --git a/src/modules/Withdraw/Withdraw.module.scss b/src/modules/Withdraw/Withdraw.module.scss new file mode 100644 index 0000000..91f17c1 --- /dev/null +++ b/src/modules/Withdraw/Withdraw.module.scss @@ -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; +} diff --git a/src/modules/Withdraw/Withdraw.reducer.js b/src/modules/Withdraw/Withdraw.reducer.js new file mode 100644 index 0000000..2dcfb10 --- /dev/null +++ b/src/modules/Withdraw/Withdraw.reducer.js @@ -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) diff --git a/src/modules/Withdraw/index.js b/src/modules/Withdraw/index.js new file mode 100644 index 0000000..db6bf75 --- /dev/null +++ b/src/modules/Withdraw/index.js @@ -0,0 +1,3 @@ +import Withdraw from './Withdraw.container' + +export default Withdraw From 7ba65c13e71b6a90309e6b50a13a0b5f3e704879 Mon Sep 17 00:00:00 2001 From: Kamen Stoykov Date: Fri, 31 May 2019 18:21:11 +0300 Subject: [PATCH 2/3] vote learn more links --- public/images/learn-more-curve.png | Bin 0 -> 71264 bytes src/common/data/dapps.js | 633 ------------------ src/common/data/vote.js | 2 + src/modules/Profile/Profile.container.js | 2 + src/modules/Profile/Profile.jsx | 15 +- src/modules/Submit/Submit.container.js | 2 + src/modules/Submit/Submit.jsx | 13 +- src/modules/Submit/Submit.reducer.js | 66 +- .../TransactionStatus.utilities.js | 1 + src/modules/Vote/Vote.container.js | 6 + src/modules/Vote/Vote.jsx | 272 ++++---- src/modules/Vote/Vote.module.scss | 56 ++ src/modules/Vote/Vote.reducer.js | 53 +- 13 files changed, 356 insertions(+), 765 deletions(-) create mode 100644 public/images/learn-more-curve.png delete mode 100644 src/common/data/dapps.js diff --git a/public/images/learn-more-curve.png b/public/images/learn-more-curve.png new file mode 100644 index 0000000000000000000000000000000000000000..e685be1da982af16914f503fd6da6a721bfaab58 GIT binary patch literal 71264 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 ({ dappState: state.dapps }) const mapDispatchToProps = dispatch => ({ onClickWithdraw: dapp => dispatch(showWithdrawAction(dapp)), + onClickUpdateMetadata: dapp => dispatch(showSubmitAction(dapp)), }) export default withRouter( diff --git a/src/modules/Profile/Profile.jsx b/src/modules/Profile/Profile.jsx index 963d867..daa7524 100644 --- a/src/modules/Profile/Profile.jsx +++ b/src/modules/Profile/Profile.jsx @@ -17,6 +17,7 @@ const ProfileContent = ({ highestRankedPosition, categoryPosition, onClickWithdraw, + onClickUpdateMetadata, }) => { return ( <> @@ -83,7 +84,7 @@ const ProfileContent = ({
-
+
Edit metadata
@@ -114,6 +115,14 @@ class Profile extends Component { }, 1) } + onClickUpdateMetadata(dapp) { + const { onClickUpdateMetadata } = this.props + this.onClickClose() + setTimeout(() => { + onClickUpdateMetadata(dapp) + }, 1) + } + render() { const { match, dappState } = this.props const { dapps } = dappState @@ -151,7 +160,8 @@ class Profile extends Component { + onClickWithdraw={this.onClickWithdraw.bind(this, dapp)} + onClickUpdateMetadata={this.onClickUpdateMetadata.bind(this, dapp)} /> ) } @@ -160,6 +170,7 @@ class Profile extends Component { Profile.propTypes = { dappState: PropTypes.instanceOf(DappState), onClickWithdraw: PropTypes.func.isRequired, + onClickUpdateMetadata: PropTypes.func.isRequired, } export default Profile diff --git a/src/modules/Submit/Submit.container.js b/src/modules/Submit/Submit.container.js index 65e2b1a..13b0bc7 100644 --- a/src/modules/Submit/Submit.container.js +++ b/src/modules/Submit/Submit.container.js @@ -17,6 +17,7 @@ import { submitAction, switchToRatingAction, onInputSntValueAction, + updateAction, } from './Submit.reducer' const mapStateToProps = state => @@ -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)), diff --git a/src/modules/Submit/Submit.jsx b/src/modules/Submit/Submit.jsx index b175995..8275128 100644 --- a/src/modules/Submit/Submit.jsx +++ b/src/modules/Submit/Submit.jsx @@ -151,8 +151,8 @@ 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, @@ -160,7 +160,10 @@ class Submit extends React.Component { desc, } - onSubmit(dapp, parseInt(sntValue, 10)) + if (id === '') + onSubmit(metadata, parseInt(sntValue, 10)) + else + onUpdate(id, metadata) } handleSNTChange(e) { @@ -187,6 +190,7 @@ class Submit extends React.Component { visible_submit, visible_rating, onClickClose, + id, name, desc, url, @@ -316,7 +320,7 @@ class Submit extends React.Component { className={styles.submitButton} type="submit" disabled={!canSubmit} - onClick={switchToRating} + onClick={id === '' ? switchToRating : this.onSubmit} > Continue @@ -468,6 +472,7 @@ 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, diff --git a/src/modules/Submit/Submit.reducer.js b/src/modules/Submit/Submit.reducer.js index 17238bc..3a37c63 100644 --- a/src/modules/Submit/Submit.reducer.js +++ b/src/modules/Submit/Submit.reducer.js @@ -6,7 +6,7 @@ import { onStartProgressAction, hideAction, } from '../TransactionStatus/TransactionStatus.recuder' -import { TYPE_SUBMIT } from '../TransactionStatus/TransactionStatus.utilities' +import { TYPE_SUBMIT, TYPE_UPDATE } from '../TransactionStatus/TransactionStatus.utilities' import { showAlertAction } from '../Alert/Alert.reducer' import BlockchainSDK from '../../common/blockchain' @@ -27,15 +27,15 @@ 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' -export const showSubmitActionAfterCheck = () => { +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 ( @@ -47,7 +47,26 @@ export const showSubmitAction = () => { 'There is an active transaction. Please wait for it to finish and then you could be able to create your Ðapp', ), ) - } else dispatch(showSubmitActionAfterCheck()) + } 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)) } } @@ -138,6 +157,29 @@ export const submitAction = (dapp, sntValue) => { } } +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, @@ -148,16 +190,16 @@ export const onInputSntValueAction = 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, diff --git a/src/modules/TransactionStatus/TransactionStatus.utilities.js b/src/modules/TransactionStatus/TransactionStatus.utilities.js index 4793246..4ce9d9b 100644 --- a/src/modules/TransactionStatus/TransactionStatus.utilities.js +++ b/src/modules/TransactionStatus/TransactionStatus.utilities.js @@ -5,6 +5,7 @@ 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() { diff --git a/src/modules/Vote/Vote.container.js b/src/modules/Vote/Vote.container.js index d1ecfa7..6d7d1a2 100644 --- a/src/modules/Vote/Vote.container.js +++ b/src/modules/Vote/Vote.container.js @@ -9,6 +9,9 @@ import { fetchVoteRatingAction, upVoteAction, downVoteAction, + learnMoreUpVoteAction, + learnMoreDownVoteAction, + closeLearnMoreAction, } from './Vote.reducer' const mapStateToProps = state => @@ -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( diff --git a/src/modules/Vote/Vote.jsx b/src/modules/Vote/Vote.jsx index 2122a74..5893196 100644 --- a/src/modules/Vote/Vote.jsx +++ b/src/modules/Vote/Vote.jsx @@ -63,6 +63,11 @@ class Vote extends Component { dappState, sntValue, afterVoteRating, + learnMoreUpVote, + learnMoreDownVote, + onClickLearnMoreUpVote, + onClickLearnMoreDownVote, + onClickCloseLearnMore, } = this.props if (dapp === null) { @@ -99,123 +104,159 @@ class Vote extends Component { windowClassName={styles.modalWindow} contentClassName={styles.modalContent} > -
- - -
-
- - {dapp.name} -
-
-
- - SNT - {currentSNTamount.toLocaleString()} - - {isUpvote && afterVoteRating !== null && ( - - {`${(dapp.sntValue + afterVoteRating).toLocaleString()} ↑`} - - )} - {!isUpvote && afterVoteRating !== null && ( - - {`${(dapp.sntValue - afterVoteRating).toLocaleString()} ↓`} - - )} + {(!learnMoreUpVote && !learnMoreDownVote && ( + <> +
+ +
-
- - {getCategoryName(dapp.category)} - {`${getCategoryName(dapp.category)} №${catPosition}`} - - {isUpvote && - afterVoteCategoryPosition !== null && - afterVoteCategoryPosition !== catPosition && ( - - {`№${afterVoteCategoryPosition} ↑`} - - )} - {!isUpvote && - afterVoteCategoryPosition !== null && - afterVoteCategoryPosition !== catPosition && ( - - {`№${afterVoteCategoryPosition} ↓`} - - )} -
-
- {!isUpvote && ( -
- {sntValue} -
- )} - {isUpvote && ( -
- + + {dapp.name} +
+
+
+ + SNT + {currentSNTamount.toLocaleString()} + + {isUpvote && afterVoteRating !== null && ( + + {`${(dapp.sntValue + afterVoteRating).toLocaleString()} ↑`} + + )} + {!isUpvote && afterVoteRating !== null && ( + + {`${(dapp.sntValue - afterVoteRating).toLocaleString()} ↓`} + + )} +
+
+ + {getCategoryName(dapp.category)} + {`${getCategoryName(dapp.category)} №${catPosition}`} + + {isUpvote && + afterVoteCategoryPosition !== null && + afterVoteCategoryPosition !== catPosition && ( + + {`№${afterVoteCategoryPosition} ↑`} + + )} + {!isUpvote && + afterVoteCategoryPosition !== null && + afterVoteCategoryPosition !== catPosition && ( + + {`№${afterVoteCategoryPosition} ↓`} + + )} +
+
+ {!isUpvote && ( +
+ {sntValue} +
+ )} + {isUpvote && ( +
+ +
+ )} + +
+ {isUpvote && ( +

+ SNT you spend to upvote is locked in the contract and contributes + directly to {dapp.name}'s ranking.{' '} + + Learn more↗ + +

+ )} + {!isUpvote && ( +

+ 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.{' '} + + Learn more↗ + +

+ )} + +
+ + ))} + {learnMoreUpVote && ( +
+
How to submit a ÐApp
+
+ +

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.

+

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

+

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.

+

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.

+

Welcome to the future of decentralised curation!

+
+
+ +
+
+ )} + {learnMoreDownVote && ( +
+
How to submit a ÐApp
+
+ +

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.

+

You can downvote this DApp, and each downvote will move it 1% of its current value down the rankings.

+

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.

+

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.

+

Welcome to the future of decentralised curation!

+
+
+ +
)} - -
- {isUpvote && ( -

- SNT you spend to upvote is locked in the contract and contributes - directly to {dapp.name}'s ranking.{' '} - - Learn more↗ - -

- )} - {!isUpvote && ( -

- 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.{' '} - - Learn more↗ - -

- )} - -
) } @@ -232,6 +273,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 +283,9 @@ Vote.propTypes = { 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 diff --git a/src/modules/Vote/Vote.module.scss b/src/modules/Vote/Vote.module.scss index c8ec269..6f1b95e 100644 --- a/src/modules/Vote/Vote.module.scss +++ b/src/modules/Vote/Vote.module.scss @@ -213,4 +213,60 @@ .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; + } +} \ No newline at end of file diff --git a/src/modules/Vote/Vote.reducer.js b/src/modules/Vote/Vote.reducer.js index 7d41159..4560806 100644 --- a/src/modules/Vote/Vote.reducer.js +++ b/src/modules/Vote/Vote.reducer.js @@ -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' @@ -57,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', @@ -190,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, @@ -197,6 +218,8 @@ const showUpVoteAfterCheck = (state, dapp) => { sntValue: '0', isUpvote: true, afterVoteRating: null, + learnMoreUpVote: false, + learnMoreDownVote: false, }) } @@ -207,6 +230,8 @@ const showDownVoteAfterCheck = (state, dapp) => { sntValue: '0', isUpvote: false, afterVoteRating: null, + learnMoreUpVote: false, + learnMoreDownVote: false, }) } @@ -214,6 +239,8 @@ const closeVote = state => { return Object.assign({}, state, { visible: false, dapp: null, + learnMoreUpVote: false, + learnMoreDownVote: false, }) } @@ -253,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, @@ -262,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) From 72805e69c487256ebcd6bc5f28234e86ca757d86 Mon Sep 17 00:00:00 2001 From: Kamen Stoykov Date: Mon, 3 Jun 2019 09:59:43 +0300 Subject: [PATCH 3/3] link --- src/modules/HowToSubmit/HowToSubmit.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/HowToSubmit/HowToSubmit.jsx b/src/modules/HowToSubmit/HowToSubmit.jsx index 32cacb3..2e5fb88 100644 --- a/src/modules/HowToSubmit/HowToSubmit.jsx +++ b/src/modules/HowToSubmit/HowToSubmit.jsx @@ -98,7 +98,7 @@ class HowToSubmit extends React.Component {
  1. Malicious code injection
  2. - Violation of Status' principles + Violation of Status' principles
  3. Lack of usability (especially on mobile)
  4. Vote manipulation.
  5. @@ -110,7 +110,7 @@ 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{' '} - Status’ principles, and to take a mobile-first approach + Status’ principles, and to take a mobile-first approach to development.