diff --git a/app/components/all.scss b/app/components/all.scss index e48581c..f5a86f0 100644 --- a/app/components/all.scss +++ b/app/components/all.scss @@ -74,6 +74,7 @@ @import "./modules/nft/CreateNFTCollection"; @import "./modules/nft/IssueNFTToken"; @import "./modules/nft/NFTTokens"; +@import "./modules/nft/NFTPlaceBet"; @import "./modules/nft/NFTTokenSell"; @import "./modules/nft/NFTTokenTransfer"; diff --git a/app/components/elements/forms/AmountAssetField.jsx b/app/components/elements/forms/AmountAssetField.jsx index 4eb5782..449fbd1 100644 --- a/app/components/elements/forms/AmountAssetField.jsx +++ b/app/components/elements/forms/AmountAssetField.jsx @@ -8,23 +8,19 @@ class AmountAssetField extends React.Component { } onChange = (e) => { - const { amountField, values, setFieldValue, assets } = this.props + const { amountField, onChange, values, setFieldValue, assets } = this.props const value = e.target.value const asset = assets[value] if (asset) { const { supply } = asset - const oldValue = values[amountField].asset - setFieldValue(amountField, AssetEditor(oldValue.amount, - supply.precision, supply.symbol)) - - if (!values.author) { // if not edit mode - if (asset.allow_override_transfer || value === 'GBG') { - if (values.tip_cost) - setFieldValue('tip_cost', false) - setFieldValue('disable_tip', true) - } else { - setFieldValue('disable_tip', false) - } + if (amountField) { + const oldValue = values[amountField].asset + setFieldValue(amountField, AssetEditor(oldValue.amount, + supply.precision, supply.symbol)) + } + + if (onChange) { + onChange(e, asset) } } } diff --git a/app/components/elements/forms/AmountField.jsx b/app/components/elements/forms/AmountField.jsx index f9b5ef2..b91fc86 100644 --- a/app/components/elements/forms/AmountField.jsx +++ b/app/components/elements/forms/AmountField.jsx @@ -11,13 +11,13 @@ class AmountField extends React.Component { // TODO: is it right to pass all props to input const { placeholder, name, ...rest } = this.props const { value, } = field - const { values, setFieldValue } = form + const { values, setFieldValue, setFieldTouched } = form return this.onChange(e, values, setFieldValue)} + {...rest} onChange={(e) => this.onChange(e, values, setFieldValue, setFieldTouched)} /> } - onChange = (e, values, setFieldValue) => { + onChange = (e, values, setFieldValue, setFieldTouched) => { const { name } = this.props const newAmount = values[name].withChange(e.target.value) if (newAmount.hasChange && newAmount.asset.amount >= 0) { diff --git a/app/components/elements/nft/NFTTokenItem.jsx b/app/components/elements/nft/NFTTokenItem.jsx index d546e8f..b6a4db7 100644 --- a/app/components/elements/nft/NFTTokenItem.jsx +++ b/app/components/elements/nft/NFTTokenItem.jsx @@ -126,6 +126,12 @@ class NFTTokenItem extends Component { }, value: tt('g.transfer') }) } + // if (!isMy && last_price) { + // kebabItems.unshift({ link: '#', onClick: e => { + // this.props.showPlaceBet(e, tokenIdx) + // }, value: tt('nft_tokens_jsx.place_bet') }) + // } + const isCollection = page === 'collection' const isMarket = page === 'market' @@ -137,7 +143,7 @@ class NFTTokenItem extends Component { : null} {isMy && !selling && } - {isMy && selling && } {!isMy && selling && } - {isMy && } + {isMy && } {kebabItems.length > 1 ? : null} diff --git a/app/components/elements/nft/NFTTokenItem.scss b/app/components/elements/nft/NFTTokenItem.scss index 0782900..889be57 100644 --- a/app/components/elements/nft/NFTTokenItem.scss +++ b/app/components/elements/nft/NFTTokenItem.scss @@ -51,7 +51,7 @@ .button:hover { background-color: #016aad !important; } - .button.hollow { + .button.hollow.noborder { border: 0px; } .button.hollow:hover { diff --git a/app/components/modules/nft/NFTPlaceBet.jsx b/app/components/modules/nft/NFTPlaceBet.jsx new file mode 100644 index 0000000..4ec5a90 --- /dev/null +++ b/app/components/modules/nft/NFTPlaceBet.jsx @@ -0,0 +1,257 @@ +import React, { Component, } from 'react' +import tt from 'counterpart' +import { connect, } from 'react-redux' +import CloseButton from 'react-foundation-components/lib/global/close-button' +import { Formik, Form, Field, ErrorMessage, } from 'formik' +import { api } from 'golos-lib-js' +import { validateAccountName, Asset, AssetEditor } from 'golos-lib-js/lib/utils' + +import AssetBalance from 'app/components/elements/AssetBalance' +import AmountField from 'app/components/elements/forms/AmountField' +import AmountAssetField from 'app/components/elements/forms/AmountAssetField' +import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import NFTSmallIcon from 'app/components/elements/nft/NFTSmallIcon' +import transaction from 'app/redux/Transaction' +import { generateOrderID } from 'app/utils/market/utils' +import session from 'app/utils/session' + +class NFTPlaceBet extends Component { + state = { + order: { + price: AssetEditor('0.000 GOLOS') + } + } + + async componentDidMount() { + const isHidden = (sym) => { + return ($STM_Config.hidden_assets && $STM_Config.hidden_assets[sym]) + } + try { + let assets = {} + let currentBalance + const username = session.load().currentName + if (username) { + let bals = await api.getAccountsBalances([username], { system: true }) + bals = bals[0] + if (bals['GOLOS']) { + assets['GOLOS'] = { supply: Asset(bals['GOLOS'].balance) } + } + if (bals['GBG']) { + assets['GBG'] = { supply: Asset(bals['GBG'].balance) } + } + for (const [sym, obj] of Object.entries(bals)) { + if (!isHidden(sym) && sym !== 'GOLOS' && sym !== 'GBG') { + assets[sym] = { supply: Asset(obj.balance) } + } + } + for (const [sym, obj] of Object.entries(assets)) { + currentBalance = obj.supply.clone() + break + } + } + this.setState({ + assets, + currentBalance + }) + } catch (err) { + console.error(err) + } + } + + validate = (values) => { + const errors = {} + const { price } = values + const { currentBalance } = this.state + if (price.asset.eq(0)) { + errors.price = tt('nft_token_sell_jsx.fill_price') + } else if (currentBalance && price.asset.gt(currentBalance)) { + errors.price = tt('g.invalid_amount') + } + return errors + } + + setSubmitting = (submitting) => { + this.setState({ submitting }) + } + + getToken = () => { + const { nft_tokens, tokenIdx } = this.props + if (tokenIdx !== undefined) { + return nft_tokens.toJS().data[tokenIdx] + } + return this.props.token + } + + _onSubmit = async (values) => { + this.setSubmitting(true) + this.setState({ + errorMessage: '' + }) + + const { currentUser, onClose, } = this.props + const token = this.getToken() + const { token_id, name } = token + + const username = currentUser.get('username') + + await this.props.placeBet(token_id, values.price, name, username, () => { + this.props.onClose() + this.setSubmitting(false) + this.doNotRender = true + this.props.refetch() + }, (err) => { + console.error(err) + this.setSubmitting(false) + this.setState({ + errorMessage: err.toString() + }) + }) + } + + onCancelMouseDown = (e) => { + e.preventDefault() + this.setState({ + cancelMouseDown: true + }) + } + + onCancelMouseUp = (e) => { + e.preventDefault() + if (this.state.cancelMouseDown) { + this.props.onClose() + this.setState({ + cancelMouseDown: false + }) + } + } + + onMouseUp = (e) => { + e.preventDefault() + if (this.state.cancelMouseDown) { + this.props.onClose() + } + } + + onAssetChange = (e, asset) => { + this.setState({ + currentBalance: asset.supply.clone() + }) + } + + _renderSubmittingIndicator = () => { + const { submitting } = this.state + + return submitting ? + + : null + } + + render() { + const { assets } = this.state + + if (this.doNotRender || !assets) { + return + } + + const { onClose, } = this.props + + const token = this.getToken() + + const { json_metadata, image } = token + + let data + if (json_metadata) { + data = JSON.parse(json_metadata) + } + data = data || {} // node allows to use '', null, object, or array + + const { errorMessage, submitting, currentBalance, } = this.state + + return
+ +

{tt('nft_tokens_jsx.place_bet')}

+
+ + {data.title} +
+ + {({ + handleSubmit, isValid, values, errors, touched, setFieldValue, handleChange, + }) => { + return ( +
+
+ {tt('g.price')} +
+
+
+
+ + + + +
+ {errors.price &&
{errors.price}
} +
+
+ {currentBalance && } + {(errorMessage && errorMessage !== 'Canceled') ?
+
+
{errorMessage}
+
+
: null} +
+
+ + + {this._renderSubmittingIndicator()} +
+
+ + )}}
+
+ } +} + +export default connect( + // mapStateToProps + (state, ownProps) => { + return { ...ownProps, + nft_tokens: state.global.get('nft_tokens'), + } + }, + + dispatch => ({ + placeBet: ( + token_id, price, collectionName, username, successCallback, errorCallback + ) => { + const operation = { + buyer: username, + name: collectionName, + token_id, + order_id: generateOrderID(), + price: price.asset.toString() + } + + dispatch(transaction.actions.broadcastOperation({ + type: 'nft_buy', + username, + operation, + successCallback, + errorCallback + })) + } + }) +)(NFTPlaceBet) diff --git a/app/components/modules/nft/NFTPlaceBet.scss b/app/components/modules/nft/NFTPlaceBet.scss new file mode 100644 index 0000000..feff4b1 --- /dev/null +++ b/app/components/modules/nft/NFTPlaceBet.scss @@ -0,0 +1,9 @@ +.NFTPlaceBet { + .column { + padding-left: 0rem; + padding-right: 0rem; + } + .error { + margin-bottom: 0px; + } +} diff --git a/app/components/modules/nft/NFTTokens.jsx b/app/components/modules/nft/NFTTokens.jsx index 0186c7c..6e42937 100644 --- a/app/components/modules/nft/NFTTokens.jsx +++ b/app/components/modules/nft/NFTTokens.jsx @@ -12,6 +12,7 @@ import LoadingIndicator from 'app/components/elements/LoadingIndicator' import NFTTokenItem from 'app/components/elements/nft/NFTTokenItem' import NFTTokenTransfer from 'app/components/modules/nft/NFTTokenTransfer' import NFTTokenSell from 'app/components/modules/nft/NFTTokenSell' +import NFTPlaceBet from 'app/components/modules/nft/NFTPlaceBet' import g from 'app/redux/GlobalReducer' class NFTTokens extends Component { @@ -59,6 +60,20 @@ class NFTTokens extends Component { }) } + showPlaceBet = (e, tokenIdx) => { + e.preventDefault() + this.setState({ + showPlaceBet: true, + tokenIdx, + }) + } + + hidePlaceBet = () => { + this.setState({ + showPlaceBet: false, + }) + } + sortOrder = (e, sort, sortReversed) => { e.preventDefault() this.sort = sort @@ -91,11 +106,12 @@ class NFTTokens extends Component { assets={assets} showTransfer={this.showTransfer} showSell={this.showSell} + showPlaceBet={this.showPlaceBet} refetch={this.refetch} />) } } - const { showTransfer, showSell, tokenIdx } = this.state + const { showTransfer, showSell, showPlaceBet, tokenIdx } = this.state const sortItems = [ { link: '#', onClick: e => { @@ -169,6 +185,15 @@ class NFTTokens extends Component { refetch={this.refetch} /> + + + + ) } } diff --git a/app/components/pages/nft/NFTTokenPage.jsx b/app/components/pages/nft/NFTTokenPage.jsx index cd986c1..0d2eb44 100644 --- a/app/components/pages/nft/NFTTokenPage.jsx +++ b/app/components/pages/nft/NFTTokenPage.jsx @@ -11,6 +11,7 @@ import LoadingIndicator from 'app/components/elements/LoadingIndicator' import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper' import NFTTokenTransfer from 'app/components/modules/nft/NFTTokenTransfer' import NFTTokenSell from 'app/components/modules/nft/NFTTokenSell' +import NFTPlaceBet from 'app/components/modules/nft/NFTPlaceBet' import NotFound from 'app/components/pages/NotFound' import { msgsHost, msgsLink } from 'app/utils/ExtLinkUtils' import { getAssetMeta } from 'app/utils/market/utils' @@ -122,6 +123,19 @@ class NFTTokenPage extends Component { }) } + showPlaceBet = (e) => { + e.preventDefault() + this.setState({ + showPlaceBet: true, + }) + } + + hidePlaceBet = () => { + this.setState({ + showPlaceBet: false, + }) + } + _renderOp = (trx, i) => { const [ opType, op ] = trx.op @@ -261,6 +275,31 @@ class NFTTokenPage extends Component { const isMy = currentUser && currentUser.get('username') === token.owner + let my_bet = token.my_bet ? Asset(token.my_bet.price) : null + if (my_bet) { + const asset = assets[my_bet.symbol] + let imageUrl + if (asset) { + imageUrl = getAssetMeta(asset).image_url + } + const cancelBet = (e) => { + this.props.cancelBet(e, token.my_bet, () => { + this.props.fetchState() + }, (err) => { + if (!err || err.toString() === 'Canceled') return + console.error(err) + alert(err.toString()) + }) + } + my_bet =
+ {imageUrl && {''}} + {tt('nft_tokens_jsx.you_bet_is') + my_bet.floatString} + +
+ } + return
@@ -319,9 +358,13 @@ class NFTTokenPage extends Component { {selling && } + {!my_bet && } {!selling && msgsHost() && {tt('nft_token_page_jsx.msg')}}
} + {my_bet}
@@ -343,6 +386,15 @@ class NFTTokenPage extends Component { refetch={this.props.fetchState} /> + + + + } } @@ -425,6 +477,23 @@ module.exports = { successCallback, errorCallback })) + }, + cancelBet: (e, order, successCallback, errorCallback) => { + e.preventDefault() + + const operation = { + owner: order.owner, + order_id: order.order_id + } + + dispatch(transaction.actions.broadcastOperation({ + type: 'nft_cancel_order', + confirm: tt('g.are_you_sure'), + username: order.owner, + operation, + successCallback, + errorCallback + })) } }) )(NFTTokenPage) diff --git a/app/components/pages/nft/NFTTokenPage.scss b/app/components/pages/nft/NFTTokenPage.scss index f85c008..0f6e488 100644 --- a/app/components/pages/nft/NFTTokenPage.scss +++ b/app/components/pages/nft/NFTTokenPage.scss @@ -12,6 +12,16 @@ display: flex; width: 100%; } + .my_bet { + padding-left: 5px; + padding-top: 5px; + + span { + vertical-align: middle; + margin-left: 2px; + margin-right: 2px; + } + } img { max-width: 320px; diff --git a/app/locales/en.json b/app/locales/en.json index 924efce..7c5daa6 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -778,7 +778,9 @@ "sort_name": "By collection name", "cancel": "Cancel", "cancel_hint": "Cancel selling", + "place_bet": "Place a bet", "selling_for": "Selling for ", + "you_bet_is": "Your bet is ", "buy": "Buy", "buy2": "Buy for ", "buy_confirm": "Are you sure you want to buy ", diff --git a/app/locales/ru-RU.json b/app/locales/ru-RU.json index 9d9dcd7..c49c09d 100644 --- a/app/locales/ru-RU.json +++ b/app/locales/ru-RU.json @@ -1132,7 +1132,9 @@ "sort_name": "По имени коллекции", "cancel": "Отменить", "cancel_hint": "Отменить продажу", + "place_bet": "Сделать ставку", "selling_for": "Продается за ", + "you_bet_is": "Ваша ставка - ", "buy": "Купить", "buy2": "Купить за ", "buy_confirm": "Вы уверены, что хотите купить ", diff --git a/app/redux/FetchDataSaga.js b/app/redux/FetchDataSaga.js index eb463d0..ca74c3f 100644 --- a/app/redux/FetchDataSaga.js +++ b/app/redux/FetchDataSaga.js @@ -13,6 +13,7 @@ import { reveseTag, getFilterTags } from 'app/utils/tags'; import { CATEGORIES, SELECT_TAGS_KEY, DEBT_TOKEN_SHORT, LIQUID_TICKER } from 'app/client_config'; import { getAllPairs } from 'app/utils/market/utils' import { parseNFTImage, NFTImageStub } from 'app/utils/NFTUtils' +import session from 'app/utils/session' function* fillNftCollectionImages(nft_collections) { const noImgColls = {} @@ -54,6 +55,31 @@ function* loadNftAssets(nft_assets, syms) { } } +function* fillNftTokenOrders(select_token_ids, tokens_by_id) { + try { + if (!select_token_ids.length) return + + const acc = session.load().currentName + + if (acc) { + const nft_orders = yield call([api, api.getNftOrdersAsync], { + select_token_ids, + owner: acc, + limit: 100, + type: 'buying' + }) + for (const order of nft_orders) { + const { token_id } = order + const token = tokens_by_id[token_id] + if (!token) continue + token.my_bet = order + } + } + } catch (err) { + console.error(err) + } +} + export function* fetchDataWatches () { yield fork(watchLocationChange); yield fork(watchDataRequests); @@ -308,6 +334,20 @@ export function* fetchState(location_change_action) { state.nft_token = state.nft_token[0] state.nft_token_loaded = true + const select_token_ids = [] + const tokens_by_id = {} + + try { + if (state.nft_token) { + select_token_ids.push(state.nft_token.token_id) + tokens_by_id[state.nft_token.token_id] = state.nft_token + } + + yield fillNftTokenOrders(select_token_ids, tokens_by_id) + } catch (err) { + console.error(err) + } + try { const syms = new Set() @@ -318,6 +358,10 @@ export function* fetchState(location_change_action) { const price = Asset(state.nft_token.order.price) syms.add(price.symbol) } + if (state.nft_token.my_bet) { + const price = Asset(state.nft_token.my_bet.price) + syms.add(price.symbol) + } } state.nft_assets = {} @@ -555,6 +599,8 @@ export function* fetchNftTokens({ payload: { account, start_token_id, sort, reve } let nft_assets + const select_token_ids = [] + const tokens_by_id = {} try { const syms = new Set() @@ -566,6 +612,9 @@ export function* fetchNftTokens({ payload: { account, start_token_id, sort, reve const price = Asset(no.order.price) syms.add(price.symbol) } + + select_token_ids.push(no.token_id) + tokens_by_id[no.token_id] = no } nft_assets = {} @@ -574,6 +623,8 @@ export function* fetchNftTokens({ payload: { account, start_token_id, sort, reve console.error(err) } + yield fillNftTokenOrders(select_token_ids, tokens_by_id) + yield put(GlobalReducer.actions.receiveNftTokens({nft_tokens, start_token_id, next_from, nft_assets})) } catch (err) { console.error('fetchNftTokens', err) diff --git a/app/redux/GlobalReducer.js b/app/redux/GlobalReducer.js index e37e2fa..9c4e3c3 100644 --- a/app/redux/GlobalReducer.js +++ b/app/redux/GlobalReducer.js @@ -105,6 +105,8 @@ export default createModule({ let res = state if (res.has('nft_collections')) res = res.delete('nft_collections') + if (res.has('nft_token')) + res = res.deleteIn(['nft_token', 'my_bet']) res = res.mergeDeep(payload) if (!payload.has('nft_tokens')) { if (!window.location.pathname.endsWith('/nft-tokens')) {