submit flow

This commit is contained in:
Kamen Stoykov 2019-05-21 10:16:25 +03:00
parent df750b6dbb
commit b2dd152fae
10 changed files with 482 additions and 141 deletions

View File

@ -1,5 +1,6 @@
const submit = {
visible: false,
visible_submit: false,
visible_rating: false,
id: '',
name: '',
desc: '',
@ -11,6 +12,7 @@ const submit = {
imgControlMove: false,
imgControlX: 0,
imgControlY: 0,
sntValue: '0',
}
export default submit

View File

@ -2,6 +2,7 @@ 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 { TYPE_SUBMIT } from '../TransactionStatus/TransactionStatus.utilities'
//import BlockchainTool from '../../common/blockchain'
const ON_FINISH_FETCH_ALL_DAPPS_ACTION =
@ -100,7 +101,9 @@ export const onFinishFetchByCategoryAction = (category, dapps) => ({
})
const fetchAllDappsInState = async (dispatch, getState) => {
const stateDapps = getState().dapps
const state = getState()
const { transactionStatus } = state
const stateDapps = state.dapps
if (stateDapps.dapps === null) {
try {
let dapps = await BlockchainSDK.DiscoverService.getDApps()
@ -112,6 +115,14 @@ const fetchAllDappsInState = async (dispatch, getState) => {
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

View File

@ -15,9 +15,12 @@ import {
onImgDoneAction,
onImgCancelAction,
submitAction,
switchToRatingAction,
onInputSntValueAction,
} from './Submit.reducer'
const mapStateToProps = state => state.submit
const mapStateToProps = state =>
Object.assign(state.submit, { dapps: state.dapps.dapps })
const mapDispatchToProps = dispatch => ({
onClickClose: () => dispatch(closeSubmitAction()),
onInputName: name => dispatch(onInputNameAction(name)),
@ -31,6 +34,8 @@ const mapDispatchToProps = dispatch => ({
onImgDone: imgBase64 => dispatch(onImgDoneAction(imgBase64)),
onSubmit: dapp => dispatch(submitAction(dapp)),
onClickTerms: () => dispatch(push('/terms')),
switchToRating: () => dispatch(switchToRatingAction()),
onInputSntValue: sntValue => dispatch(onInputSntValueAction(sntValue)),
})
export default withRouter(

View File

@ -1,12 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactImageFallback from 'react-image-fallback'
import styles from './Submit.module.scss'
import Modal from '../../common/components/Modal'
import CategorySelector from '../CategorySelector/CategorySelector.picker'
import Slider from '../../common/components/Slider/Slider'
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'
const getCategoryName = category =>
Categories.find(x => x.key === category).value
class Submit extends React.Component {
constructor(props) {
super(props)
@ -24,6 +32,7 @@ class Submit extends React.Component {
this.onEndMove = this.onEndMove.bind(this)
this.onImgDone = this.onImgDone.bind(this)
this.onSubmit = this.onSubmit.bind(this)
this.handleSNTChange = this.handleSNTChange.bind(this)
}
componentDidUpdate() {
@ -153,9 +162,29 @@ class Submit extends React.Component {
onSubmit(dapp)
}
handleSNTChange(e) {
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
const { dapp, onInputSntValue, fetchVoteRating } = this.props
onInputSntValue(value)
}
title() {
const { visible_submit, visible_rating, imgControl } = this.props
if (visible_submit)
return imgControl ? 'Position and size your image' : 'Submit a Ðapp'
return 'Ðapp rating'
}
render() {
const {
visible,
dapps,
visible_submit,
visible_rating,
onClickClose,
name,
desc,
@ -166,161 +195,256 @@ class Submit extends React.Component {
imgControlZoom,
onImgCancel,
onClickTerms,
switchToRating,
sntValue,
} = this.props
const canSubmit =
name !== '' && desc !== '' && url !== '' && img !== '' && category !== ''
const visible = visible_submit || visible_rating
/* rating */
let currentSNTamount = 0
let dappsByCategory = []
let catPosition = 0
let afterVoteRating = null
let afterVoteCategoryPosition = null
if (visible_rating) {
dappsByCategory = dapps.filter(dapp_ => dapp_.category === category)
catPosition = dappsByCategory.length + 1
if (sntValue !== '') {
afterVoteRating = parseInt(sntValue, 10)
afterVoteCategoryPosition = 1
for (let i = 0; i < dappsByCategory.length; ++i) {
if (dappsByCategory[i].sntValue < afterVoteRating) break
afterVoteCategoryPosition++
}
}
}
return (
<Modal
visible={visible && window.location.hash === '#submit'}
onClickClose={onClickClose}
windowClassName={styles.modalWindow}
contentClassName={imgControl ? styles.modalContentImgControl : ''}
contentClassName={
imgControl || visible_rating ? styles.modalContentFullScreen : ''
}
>
<div className={styles.title}>
{imgControl ? 'Position and size your image' : 'Submit a Ðapp'}
</div>
<div className={imgControl ? styles.cntWithImgControl : ''}>
<div className={imgControl ? styles.withImgControl : ''}>
<div className={styles.block}>
<div className={styles.labelRow}>
<span>Name of your Ðapp</span>
</div>
<input
className={styles.input}
placeholder="Name"
value={name}
onChange={this.onInputName}
/>
</div>
<div className={styles.block}>
<div className={styles.labelRow}>
<span>A short description</span>
<span>{desc.length}/140</span>
</div>
<textarea
className={styles.input}
placeholder="Max 140 characters"
value={desc}
onChange={this.onInputDesc}
/>
</div>
<div className={styles.block}>
<div className={styles.labelRow}>
<span>URL</span>
</div>
<input
className={styles.input}
placeholder="https://your.dapp.cool"
value={url}
onChange={this.onInputUrl}
/>
</div>
<div className={styles.block}>
<div className={styles.labelRow}>
<span>Upload the logo or icon of your Ðapp</span>
</div>
<div className={styles.imgCnt}>
<span>Choose image</span>
<div
className={styles.imgHolder}
style={{ backgroundImage: `url(${img})` }}
/>
<div className={styles.title}>{this.title()}</div>
{visible_submit && (
<div className={imgControl ? styles.cntWithImgControl : ''}>
<div className={imgControl ? styles.withImgControl : ''}>
<div className={styles.block}>
<div className={styles.labelRow}>
<span>Name of your Ðapp</span>
</div>
<input
className={styles.uploader}
type="file"
onChange={this.onChangeImage}
accept=".jpg, .png"
className={styles.input}
placeholder="Name"
value={name}
onChange={this.onInputName}
/>
</div>
<div className={styles.imgInfo}>
The image should be a square 1:1 ratio JPG or PNG file, minimum
size is 160 × 160 pixels. The image will be placed in a circle
<div className={styles.block}>
<div className={styles.labelRow}>
<span>A short description</span>
<span>{desc.length}/140</span>
</div>
<textarea
className={styles.input}
placeholder="Max 140 characters"
value={desc}
onChange={this.onInputDesc}
/>
</div>
<div className={styles.block}>
<div className={styles.labelRow}>
<span>URL</span>
</div>
<input
className={styles.input}
placeholder="https://your.dapp.cool"
value={url}
onChange={this.onInputUrl}
/>
</div>
<div className={styles.block}>
<div className={styles.labelRow}>
<span>Upload the logo or icon of your Ðapp</span>
</div>
<div className={styles.imgCnt}>
<span>Choose image</span>
<div
className={styles.imgHolder}
style={{ backgroundImage: `url(${img})` }}
/>
<input
className={styles.uploader}
type="file"
onChange={this.onChangeImage}
accept=".jpg, .png"
/>
</div>
<div className={styles.imgInfo}>
The image should be a square 1:1 ratio JPG or PNG file,
minimum size is 160 × 160 pixels. The image will be placed in
a circle
</div>
</div>
<div className={styles.block}>
<div className={styles.labelRow}>
<span>Category</span>
</div>
<CategorySelector
category={category === '' ? null : category}
className={`${styles.categorySelector} ${
category === '' ? styles.categorySelectorEmpty : ''
}`}
/>
</div>
<div className={`${styles.block} ${styles.blockSubmit}`}>
<div className={styles.terms}>
By continuing you agree to our
<a onClick={onClickTerms}> Terms and Conditions.</a>
</div>
<button
className={styles.submitButton}
type="submit"
disabled={!canSubmit}
onClick={switchToRating}
>
Continue
</button>
</div>
</div>
<div className={styles.block}>
<div className={styles.labelRow}>
<span>Category</span>
{imgControl && (
<div className={styles.imgControl}>
<div
className={styles.imgCanvasCnt}
onMouseDown={this.onStartMove}
onMouseMove={this.onMouseMove}
onMouseUp={this.onEndMove}
onMouseLeave={this.onEndMove}
onTouchStart={this.onStartMove}
onTouchMove={this.onTouchMove}
onTouchEnd={this.onEndMove}
onTouchCancel={this.onEndMove}
>
<canvas
ref={this.imgCanvas}
className={styles.imgCanvas}
width="160"
height="160"
/>
</div>
<div className={styles.controls}>
<div className={styles.slider}>
<div className={styles.minZoom} />
<Slider
min={0}
max={300}
value={imgControlZoom}
onChange={this.onChangeZoom}
/>
<div className={styles.maxZoom} />
</div>
<div className={styles.actionsCnt}>
<button
className={`${styles.button} ${styles.cancelButton}`}
onClick={onImgCancel}
>
Cancel
</button>
<button
className={`${styles.button} ${styles.doneButton}`}
onClick={this.onImgDone}
>
Done
</button>
</div>
</div>
</div>
<CategorySelector
category={category === '' ? null : category}
className={`${styles.categorySelector} ${
category === '' ? styles.categorySelectorEmpty : ''
}`}
)}
</div>
)}
{visible_rating && (
<>
<div className={styles.dapp}>
<ReactImageFallback
className={styles.image}
src={img}
fallbackImage={icon}
alt="App icon"
width={24}
height={24}
/>
{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.greenBadge}>
{`${afterVoteRating.toLocaleString()}`}
</span>
)}
</div>
<div className={styles.itemRow}>
<span className={styles.item}>
<img
src={CategoriesUtils(category)}
alt={getCategoryName(category)}
width="24"
height="24"
/>
{`${getCategoryName(category)}${catPosition}`}
</span>
{afterVoteCategoryPosition !== null &&
afterVoteCategoryPosition !== catPosition && (
<span className={styles.greenBadge}>
{`${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.block} ${styles.blockSubmit}`}>
<div className={styles.terms}>
By continuing you agree to our
<a onClick={onClickTerms}> Terms and Conditions.</a>
</div>
<button
className={styles.submitButton}
type="submit"
disabled={!canSubmit}
onClick={this.onSubmit}
>
Continue
<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" onClick={this.onSubmit}>
{!sntValue || sntValue === '0'
? 'Publish'
: 'Stake and Publish'}
</button>
</div>
</div>
{imgControl && (
<div className={styles.imgControl}>
<div
className={styles.imgCanvasCnt}
onMouseDown={this.onStartMove}
onMouseMove={this.onMouseMove}
onMouseUp={this.onEndMove}
onMouseLeave={this.onEndMove}
onTouchStart={this.onStartMove}
onTouchMove={this.onTouchMove}
onTouchEnd={this.onEndMove}
onTouchCancel={this.onEndMove}
>
<canvas
ref={this.imgCanvas}
className={styles.imgCanvas}
width="160"
height="160"
/>
</div>
<div className={styles.controls}>
<div className={styles.slider}>
<div className={styles.minZoom} />
<Slider
min={0}
max={300}
value={imgControlZoom}
onChange={this.onChangeZoom}
/>
<div className={styles.maxZoom} />
</div>
<div className={styles.actionsCnt}>
<button
className={`${styles.button} ${styles.cancelButton}`}
onClick={onImgCancel}
>
Cancel
</button>
<button
className={`${styles.button} ${styles.doneButton}`}
onClick={this.onImgDone}
>
Done
</button>
</div>
</div>
</div>
)}
</div>
</>
)}
</Modal>
)
}
}
Submit.propTypes = {
visible: PropTypes.bool.isRequired,
visible_submit: PropTypes.bool.isRequired,
visible_rating: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
desc: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
@ -331,6 +455,7 @@ Submit.propTypes = {
imgControlMove: PropTypes.bool.isRequired,
imgControlX: PropTypes.number.isRequired,
imgControlY: PropTypes.number.isRequired,
sntValue: PropTypes.string.isRequired,
onClickClose: PropTypes.func.isRequired,
onInputName: PropTypes.func.isRequired,
onInputDesc: PropTypes.func.isRequired,
@ -342,7 +467,9 @@ Submit.propTypes = {
onImgCancel: PropTypes.func.isRequired,
onImgDone: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onInputSntValue: PropTypes.func.isRequired,
onClickTerms: PropTypes.func.isRequired,
switchToRating: PropTypes.func.isRequired,
}
export default Submit

View File

@ -13,14 +13,14 @@
}
}
.modalContentImgControl {
.modalContentFullScreen {
display: flex;
flex-direction: column;
flex: 1 1 auto;
}
@media (min-width: $desktop) {
.modalContentImgControl {
.modalContentFullScreen {
height: 512px;
}
}
@ -271,3 +271,142 @@
}
}
}
/* 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

@ -6,6 +6,7 @@ import {
onStartProgressAction,
hideAction,
} from '../TransactionStatus/TransactionStatus.recuder'
import { TYPE_SUBMIT } from '../TransactionStatus/TransactionStatus.utilities'
import { showAlertAction } from '../Alert/Alert.reducer'
// import BlockchainTool from '../../common/blockchain'
@ -36,6 +37,9 @@ const ON_IMG_MOVE = 'SUBMIT_ON_IMG_MOVE'
const ON_IMG_CANCEL = 'SUBMIT_ON_IMG_CANCEL'
const ON_IMG_DONE = 'SUBMIT_ON_IMG_DONE'
const SWITCH_TO_RATING = 'SUBMIT_SWITCH_TO_RATING'
const ON_INPUT_SNT_VALUE = 'SUBMIT_ON_INPUT_SNT_VALUE'
export const showSubmitActionAfterCheck = () => {
window.location.hash = 'submit'
return {
@ -123,6 +127,7 @@ export const submitAction = dapp => {
dapp.name,
dapp.img,
'Status is an open source mobile DApp browser and messenger build for #Etherium',
TYPE_SUBMIT,
),
)
try {
@ -142,9 +147,20 @@ export const submitAction = dapp => {
}
}
export const switchToRatingAction = () => ({
type: SWITCH_TO_RATING,
paylaod: null,
})
export const onInputSntValueAction = sntValue => ({
type: ON_INPUT_SNT_VALUE,
payload: sntValue,
})
const showSubmitAfterCheck = state => {
return Object.assign({}, state, {
visible: true,
visible_submit: true,
visible_rating: false,
id: '',
name: '',
desc: '',
@ -156,6 +172,7 @@ const showSubmitAfterCheck = state => {
imgControlMove: false,
imgControlX: 0,
imgControlY: 0,
sntValue: '0',
})
}
@ -233,6 +250,19 @@ const onImgDone = (state, imgBase64) => {
})
}
const switchToRating = state => {
return Object.assign({}, state, {
visible_submit: false,
visible_rating: true,
})
}
const onInputSntValue = (state, sntValue) => {
return Object.assign({}, state, {
sntValue,
})
}
const map = {
[SHOW_SUBMIT_AFTER_CHECK]: showSubmitAfterCheck,
[CLOSE_SUBMIT]: closeSubmit,
@ -246,6 +276,8 @@ const map = {
[ON_IMG_MOVE]: onImgMove,
[ON_IMG_CANCEL]: onImgCancel,
[ON_IMG_DONE]: onImgDone,
[SWITCH_TO_RATING]: switchToRating,
[ON_INPUT_SNT_VALUE]: onInputSntValue,
}
export default reducerUtil(map, submitInitialState)

View File

@ -3,6 +3,7 @@ import reducerUtil from '../../common/utils/reducer'
import {
transactionStatusFetchedInstance,
transactionStatusInitInstance,
TYPE_NONE,
} from './TransactionStatus.utilities'
import { onUpdateDappDataAction } from '../Dapps/Dapps.reducer'
import { showAlertAction } from '../Alert/Alert.reducer'
@ -46,9 +47,9 @@ export const hideAction = () => ({
payload: null,
})
export const onStartProgressAction = (dappName, dappImg, desc) => ({
export const onStartProgressAction = (dappName, dappImg, desc, type) => ({
type: ON_START_PROGRESS,
payload: { dappName, dappImg, desc },
payload: { dappName, dappImg, desc, type },
})
export const onReceiveTransactionInfoAction = (id, tx) => ({
@ -114,15 +115,17 @@ const hide = state => {
const transacationStatus = transactionStatusFetchedInstance()
transacationStatus.setDappName('')
transacationStatus.setProgress(false)
transacationStatus.setType(TYPE_NONE)
return Object.assign({}, state, transacationStatus)
}
const onStartProgress = (state, payload) => {
const { dappName, dappImg, desc } = payload
const { dappName, dappImg, desc, type } = payload
const transacationStatus = transactionStatusInitInstance(
dappName,
dappImg,
desc,
type,
)
transacationStatus.persistTransactionData()
return Object.assign({}, state, transacationStatus)

View File

@ -1,5 +1,10 @@
const COOKIE_NAME = 'TRANSACTION_STATUS_COOKIE'
export const TYPE_NONE = 0
export const TYPE_SUBMIT = 1
export const TYPE_UPVOTE = 2
export const TYPE_DOWNVOTE = 3
class TransactionStatus {
constructor() {
this.dappId = ''
@ -7,6 +12,7 @@ class TransactionStatus {
this.txDesc = ''
this.dappName = ''
this.dappImg = ''
this.type = TYPE_NONE
this.progress = false
this.published = false
this.failed = false
@ -49,18 +55,24 @@ class TransactionStatus {
this.failed = failed
this.persistTransactionData()
}
setType(type) {
this.type = type
this.persistTransactionData()
}
}
const getTransactionData = () => {
return localStorage.getItem(COOKIE_NAME)
}
export const transactionStatusInitInstance = (name, img, desc) => {
export const transactionStatusInitInstance = (name, img, desc, type) => {
const model = new TransactionStatus()
model.dappName = name
model.dappImg = img
model.progress = true
model.txDesc = desc
model.type = type
return model
}

View File

@ -174,7 +174,7 @@ class Vote extends Component {
</div>
</div>
{!isUpvote && (
<div className={styles.inputArea}>
<div className={styles.inputArea} style={{ opacity: 0 }}>
<span>{downvoteSNTcost}</span>
</div>
)}

View File

@ -6,6 +6,10 @@ import {
onReceiveTransactionInfoAction,
checkTransactionStatusAction,
} from '../TransactionStatus/TransactionStatus.recuder'
import {
TYPE_UPVOTE,
TYPE_DOWNVOTE,
} from '../TransactionStatus/TransactionStatus.utilities'
const SHOW_UP_VOTE_AFTER_CHECK = 'VOTE_SHOW_UP_VOTE_AFTER_CHECK'
const SHOW_DOWN_VOTE_AFTER_CHEECK = 'VOTE_SHOW_DOWN_VOTE_AFTER_CHEECK'
@ -138,7 +142,12 @@ export const upVoteAction = (dapp, amount) => {
return async dispatch => {
dispatch(closeVoteAction())
dispatch(
onStartProgressAction(dapp.name, dapp.image, `↑ Upvote by ${amount} SNT`),
onStartProgressAction(
dapp.name,
dapp.image,
`↑ Upvote by ${amount} SNT`,
TYPE_UPVOTE,
),
)
try {
const tx = await BlockchainSDK.DiscoverService.upVote(dapp.id, amount)
@ -158,6 +167,7 @@ export const downVoteAction = (dapp, amount) => {
dapp.name,
dapp.image,
`↓ Downvote by ${amount} SNT`,
TYPE_DOWNVOTE,
),
)
try {