cache + withdraw

This commit is contained in:
Kamen Stoykov 2019-05-31 14:01:31 +03:00
parent 554e29a92e
commit 252d9c055f
42 changed files with 1197 additions and 582 deletions

5
.prettierrc Normal file
View File

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

View File

@ -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",

View File

@ -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 {

View File

@ -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) => (
<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,7 @@ 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 +51,7 @@ const DappListItem = props => {
className={styles.description}
style={{ WebkitBoxOrient: 'vertical' }}
>
{description}
{desc}
</p>
</div>
<a className={styles.url} href={url}>
@ -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,

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;
@ -7,6 +16,7 @@
height: calculateRem(145);
margin: 0 calculateRem(16) calculateRem(11) calculateRem(16);
position: relative;
}
.rankedListItem {

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

@ -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

@ -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

@ -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;
}
}

View File

@ -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)

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

@ -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),
)

View File

@ -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 <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,
}) => {
return (
<>
@ -37,13 +32,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,85 +65,101 @@ 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}>
Edit metadata
</div>
<div className={styles.button} onClick={onClickWithdraw}>
Withdraw SNT
</div>
</div>
</div>
</>
)
}
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 ? (
<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)} />
</Modal>
)
}
}
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

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,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;
}
}
}

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

@ -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)),

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
@ -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

View File

@ -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',

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,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

View File

@ -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()),

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,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

View File

@ -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',

View File

@ -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),
)

View File

@ -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 <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