From 16a930699b8a5217623c2848adee6b4067c257d6 Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Sun, 10 Sep 2023 18:25:50 +0000 Subject: [PATCH] NFT UI --- app/ResolveRoute.js | 12 +- app/RootRoute.js | 2 + app/assets/images/nft.png | Bin 0 -> 17674 bytes app/components/all.scss | 6 + app/components/elements/DropdownMenu.jsx | 4 +- app/components/modules/Header.jsx | 20 +- .../modules/nft/CreateNFTCollection.jsx | 398 ++++++++++++++++++ .../modules/nft/CreateNFTCollection.scss | 34 ++ app/components/modules/nft/IssueNFTToken.jsx | 358 ++++++++++++++++ app/components/modules/nft/IssueNFTToken.scss | 31 ++ app/components/modules/nft/NFTCollections.jsx | 208 +++++++++ .../modules/nft/NFTCollections.scss | 32 ++ .../modules/nft/NFTTokenDetails.jsx | 97 +++++ .../modules/nft/NFTTokenDetails.scss | 26 ++ .../modules/nft/NFTTokenTransfer.jsx | 36 ++ .../modules/nft/NFTTokenTransfer.scss | 2 + app/components/modules/nft/NFTTokens.jsx | 180 ++++++++ app/components/modules/nft/NFTTokens.scss | 48 +++ app/components/pages/NFTMarketPage.jsx | 25 ++ app/components/pages/UserProfile.jsx | 31 +- app/locales/en.json | 57 +++ app/locales/ru-RU.json | 57 +++ app/redux/FetchDataSaga.js | 88 ++++ app/redux/GlobalReducer.js | 7 +- app/redux/Transaction_Error.js | 18 +- app/utils/NFTUtils.js | 12 + package.json | 2 +- yarn.lock | 53 +-- 28 files changed, 1800 insertions(+), 44 deletions(-) create mode 100644 app/assets/images/nft.png create mode 100644 app/components/modules/nft/CreateNFTCollection.jsx create mode 100644 app/components/modules/nft/CreateNFTCollection.scss create mode 100644 app/components/modules/nft/IssueNFTToken.jsx create mode 100644 app/components/modules/nft/IssueNFTToken.scss create mode 100644 app/components/modules/nft/NFTCollections.jsx create mode 100644 app/components/modules/nft/NFTCollections.scss create mode 100644 app/components/modules/nft/NFTTokenDetails.jsx create mode 100644 app/components/modules/nft/NFTTokenDetails.scss create mode 100644 app/components/modules/nft/NFTTokenTransfer.jsx create mode 100644 app/components/modules/nft/NFTTokenTransfer.scss create mode 100644 app/components/modules/nft/NFTTokens.jsx create mode 100644 app/components/modules/nft/NFTTokens.scss create mode 100644 app/components/pages/NFTMarketPage.jsx create mode 100644 app/utils/NFTUtils.js diff --git a/app/ResolveRoute.js b/app/ResolveRoute.js index fd9ec96..baed8c3 100644 --- a/app/ResolveRoute.js +++ b/app/ResolveRoute.js @@ -1,14 +1,15 @@ export const routeRegex = { UserProfile1: /^\/(@[\w\.\d-]+)\/?$/, - UserProfile2: /^\/(@[\w\.\d-]+)\/(transfers|assets|create-asset|invites|curation-rewards|author-rewards|donates-from|donates-to|filled-orders|permissions|created|password|witness|settings)\/??(?:&?[^=&]*=[^=&]*)*$/, + UserProfile2: /^\/(@[\w\.\d-]+)\/(transfers|assets|create-asset|invites|curation-rewards|author-rewards|donates-from|donates-to|nft|nft-collections|filled-orders|permissions|created|password|witness|settings)\/??(?:&?[^=&]*=[^=&]*)*$/, UserProfile3: /^\/(@[\w\.\d-]+)\/[\w\.\d-]+/, UserAssetEndPoints: /^\/(@[\w\.\d-]+)\/assets\/([\w\d.-]+)\/(update|transfer)$/, - UserEndPoints: /^(transfers|assets|create-asset|invites|curation-rewards|author-rewards|donates-from|donates-to|filled-orders|permissions|created|password|witness|settings)$/, + UserEndPoints: /^(transfers|assets|create-asset|invites|curation-rewards|author-rewards|donates-from|donates-to|nft|nft-collections|filled-orders|permissions|created|password|witness|settings)$/, WorkerSort: /^\/workers\/([\w\d\-]+)\/?($|\?)/, WorkerSearchByAuthor: /^\/workers\/([\w\d\-]+)\/(\@[\w\d.-]+)\/?($|\?)/, WorkerRequest: /^\/workers\/([\w\d\-]+)\/(\@[\w\d.-]+)\/([\w\d-]+)\/?($|\?)/, MarketPair: /^\/market\/([\w\d\.]+)\/([\w\d.]+)\/?($|\?)/, ConvertPair: /^\/convert\/([\w\d\.]+)\/([\w\d.]+)\/?($|\?)/, + NFTMarket: /^\/nft-market\/([\w\d\.]+)\/?($|\?)/, UserJson: /^\/(@[\w\.\d-]+)(\.json)$/, UserNameJson: /^.*(?=(\.json))/ }; @@ -89,5 +90,12 @@ export default function resolveRoute(path) if (match) { return {page: 'ConvertAssetsPage', params: match.slice(1)} } + if (path === '/nft-market') { + return {page: 'NFTMarketPage'} + } + match = path.match(routeRegex.NFTMarket); + if (match) { + return {page: 'NFTMarketPage', params: match.slice(1)} + } return {page: 'NotFound'}; } diff --git a/app/RootRoute.js b/app/RootRoute.js index bd2cad2..0d6f3e7 100644 --- a/app/RootRoute.js +++ b/app/RootRoute.js @@ -33,6 +33,8 @@ export default { cb(null, [require('@pages/UserProfile')]); } else if (route.page === 'ConvertAssetsPage') { cb(null, [require('@pages/ConvertAssetsPage')]); + } else if (route.page === 'NFTMarketPage') { + cb(null, [require('@pages/NFTMarketPage')]); } else if (route.page === 'Market') { cb(null, [require('@pages/MarketLoader')]); } else if (route.page === 'Rating') { diff --git a/app/assets/images/nft.png b/app/assets/images/nft.png new file mode 100644 index 0000000000000000000000000000000000000000..db3321a57d2f6b2427cf5219df4ec4606da20a4b GIT binary patch literal 17674 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 bbf{>(El2stGjZ@eCJ!& literal 0 HcmV?d00001 diff --git a/app/components/all.scss b/app/components/all.scss index d85eb42..8719d7f 100644 --- a/app/components/all.scss +++ b/app/components/all.scss @@ -67,6 +67,12 @@ @import "./modules/Powerdown.scss"; @import "./modules/QuickBuy.scss"; @import "./modules/Modals"; +@import "./modules/nft/NFTCollections"; +@import "./modules/nft/CreateNFTCollection"; +@import "./modules/nft/IssueNFTToken"; +@import "./modules/nft/NFTTokens"; +@import "./modules/nft/NFTTokenTransfer"; +@import "./modules/nft/NFTTokenDetails"; // pages @import "./pages/Exchanges"; diff --git a/app/components/elements/DropdownMenu.jsx b/app/components/elements/DropdownMenu.jsx index 521bf75..2173600 100644 --- a/app/components/elements/DropdownMenu.jsx +++ b/app/components/elements/DropdownMenu.jsx @@ -68,7 +68,7 @@ export default class DropdownMenu extends React.Component { } render() { - const {el, items, selected, children, className, title, href, noArrow} = this.props; + const {el, items, selected, children, className, title, href, onClick, noArrow} = this.props; const hasDropdown = items.length > 0 let entry = children || @@ -76,7 +76,7 @@ export default class DropdownMenu extends React.Component { {hasDropdown && !noArrow && } - if(hasDropdown) entry = {entry} + if(hasDropdown) entry = { onClick(e); this.toggle(e) } : this.toggle}>{entry} const menu = ; const cls = 'DropdownMenu' + (this.state.shown ? ' show' : '') + (className ? ` ${className}` : '') diff --git a/app/components/modules/Header.jsx b/app/components/modules/Header.jsx index 12bfd41..585a4ba 100644 --- a/app/components/modules/Header.jsx +++ b/app/components/modules/Header.jsx @@ -91,23 +91,25 @@ class Header extends React.Component { const name = acct_meta ? normalizeProfile(acct_meta.toJS()).name : null; const user_title = name ? `${name} (@${user_name})` : user_name; page_title = user_title; - if(route.params[1] === "curation-rewards"){ + if (route.params[1] === "curation-rewards"){ page_title = tt('header_jsx.curation_rewards_by') + " " + user_title; - } - if(route.params[1] === "author-rewards"){ + } else if (route.params[1] === "author-rewards"){ page_title = tt('header_jsx.author_rewards_by') + " " + user_title; - } - if(route.params[1] === "donates-from"){ + } else if (route.params[1] === "donates-from"){ page_title = tt('header_jsx.donates_from') + " " + user_title; - } - if(route.params[1] === "donates-to"){ + } else if (route.params[1] === "donates-to"){ page_title = tt('header_jsx.donates_to') + " " + user_title; - } - if(route.params[1] === "recent-replies"){ + } else if (route.params[1] === "recent-replies"){ page_title = tt('header_jsx.replies_to') + " " + user_title; + } else if (route.params[1] === "nft"){ + page_title = tt('header_jsx.nft_tokens') + " " + user_title + } else if (route.params[1] === "nft-collections"){ + page_title = tt('header_jsx.nft_collections') + " " + user_title } } else if (route.page === 'ConvertAssetsPage') { page_title = tt('g.convert_assets') + } else if (route.page === `NFTMarketPage`){ + page_title = tt('header_jsx.nft_market') } else { page_name = ''; //page_title = route.page.replace( /([a-z])([A-Z])/g, '$1 $2' ).toLowerCase(); } diff --git a/app/components/modules/nft/CreateNFTCollection.jsx b/app/components/modules/nft/CreateNFTCollection.jsx new file mode 100644 index 0000000..3432068 --- /dev/null +++ b/app/components/modules/nft/CreateNFTCollection.jsx @@ -0,0 +1,398 @@ +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 Expandable from 'app/components/elements/Expandable' +import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import transaction from 'app/redux/Transaction' + +const UINT32_MAX = '4294967295' + +class CreateNFTCollection extends Component { + state = { + collection: { + name: '', + title: '', + json_metadata: '{}', + max_token_count: UINT32_MAX, + infinity: true + } + } + + validate = (values) => { + const errors = {} + const { title, name } = values + if (!title.length) { + errors.title = tt('g.required') + } + if (name.length < 3) { + errors.name = tt('assets_jsx.symbol_too_short') + } else { + const parts = name.split('.') + if (parts[0] == 'GOLOS' || parts[0] == 'GBG' || parts[0] == 'GESTS') { + errors.name = tt('assets_jsx.top_symbol_not_your') + } else if (parts.length == 2 && parts[1].length < 3) { + errors.name = tt('assets_jsx.subsymbol_too_short') + } + } + let meta = values.json_metadata + try { + meta = JSON.parse(meta) + if (!meta || Array.isArray(meta)) throw new Error('JSON is array') + } catch (err) { + console.error('json_metadata', err) + //errors.json_metadata = tt('create_nft_collection_jsx.json_wrong') + meta = null + } + if (meta) { + const noFields = [] + if (!('title' in meta)) noFields.push('title') + if (values.image && !('image' in meta)) noFields.push('image') + if (values.description && !('description' in meta)) noFields.push('description') + if (noFields.length) { + errors.json_metadata = tt('create_nft_collection_jsx.json_no_fields') + errors.json_metadata += noFields.join(', ') + '. ' + } + } + ++this.validationTime + return errors + } + + setSubmitting = (submitting) => { + this.setState({ submitting }) + } + + _onSubmit = async (values) => { + this.setSubmitting(true) + this.setState({ + errorMessage: '' + }) + const { currentUser } = this.props + const username = currentUser.get('username') + await this.props.createCollection(values.name, values.json_metadata, values.max_token_count, currentUser, () => { + this.props.fetchState() + this.props.onClose() + this.setSubmitting(false) + }, (err) => { + console.error(err) + this.setSubmitting(false) + this.setState({ + errorMessage: err.toString() + }) + }) + } + + _renderSubmittingIndicator = () => { + const { submitting } = this.state + + return submitting ? + + : null + } + + onNameChange = (e, values, setFieldValue) => { + let newName = '' + let hasDot + for (let i = 0; i < e.target.value.length; ++i) { + const c = e.target.value[i] + if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && c !== '.') { + continue + } + if (c == '.') { + if (i < 3 || hasDot) { + continue + } + hasDot = true + } + newName += c.toUpperCase() + } + setFieldValue('name', newName) + } + + updateJSONMetadata = (values, setFieldValue, currentVals, force = false) => { + let { json_metadata } = values + try { + json_metadata = JSON.parse(json_metadata) + if (json_metadata === null || Array.isArray(json_metadata)) { + json_metadata = {} + } + } catch (err) { + console.error('updateJSONMetadata', err) + if (!force) { + return + } + json_metadata = {} + } + + const title = currentVals.title || values.title + json_metadata.title = title || '' + + const description = currentVals.description || values.description + if (description) { + json_metadata.description = description + } + + const image = currentVals.image || values.image + if (image) { + json_metadata.image = image + } + + setFieldValue('json_metadata', JSON.stringify(json_metadata, null, 2)) + } + + onTitleChange = (e, values, setFieldValue) => { + const title = e.target.value + setFieldValue('title', title) + this.updateJSONMetadata(values, setFieldValue, { title }) + } + + onDescriptionChange = (e, values, setFieldValue) => { + const description = e.target.value + setFieldValue('description', description) + this.updateJSONMetadata(values, setFieldValue, { description }) + } + + onImageChange = (e, values, setFieldValue) => { + const image = e.target.value + setFieldValue('image', image) + this.updateJSONMetadata(values, setFieldValue, { image }) + } + + onImageBlur = (e) => { + e.preventDefault() + this.setState({ + showImage: true + }) + } + + onMaxTokenCountChange = (e, setFieldValue) => { + let maxTokenCount = '' + for (let i = 0; i < e.target.value.length; ++i) { + const c = e.target.value[i] + if (c < '0' || c > '9') { + continue + } + maxTokenCount += c + } + if (maxTokenCount === UINT32_MAX) { + setFieldValue('infinity', true) + } + setFieldValue('max_token_count', maxTokenCount) + } + + onInfinityChange = (e, values, setFieldValue) => { + if (!values.infinity) { + setFieldValue('max_token_count', UINT32_MAX) + setFieldValue('infinity', !values.infinity) + } else { + setFieldValue('max_token_count', '') + setFieldValue('infinity', !values.infinity) + } + } + + restoreJson = (e, values, setFieldValue) => { + e.preventDefault() + this.updateJSONMetadata(values, setFieldValue, {}, true) + } + + fixJson = (e, values, setFieldValue) => { + e.preventDefault() + this.updateJSONMetadata(values, setFieldValue, {}) + } + + 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() + } + } + + render() { + const { onClose, } = this.props; + const { submitting, showImage, errorMessage, hideErrors } = this.state + + return (
+ +

+ {tt('create_nft_collection_jsx.title')} +

+ + {({ + handleSubmit, isValid, values, errors, touched, setFieldValue, handleChange, + }) => { + return ( +
+
+
+ {tt('create_nft_collection_jsx.name')} +
+ this.onNameChange(e, values, setFieldValue)} /> +
+
+
+ {tt('create_nft_collection_jsx.coll_title') + '*'} +
+ this.onTitleChange(e, values, setFieldValue)} /> +
+ {!errors.name && } +
+ {!hideErrors && } +
+
+ {tt('create_nft_collection_jsx.coll_descr')} + {' '} + {tt('create_nft_collection_jsx.not_required')} +
+
+
+
+ this.onDescriptionChange(e, values, setFieldValue)} /> +
+ +
+
+
+ {tt('create_nft_collection_jsx.image')} + {' '} + {tt('create_nft_collection_jsx.not_required')} +
+
+
+
+ this.onImageChange(e, values, setFieldValue)} /> + +
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+ + {(touched.json_metadata && errors.json_metadata) ? + (errors.json_metadata === tt('create_nft_collection_jsx.json_wrong') ? + this.restoreJson(e, values, setFieldValue)}>{tt('create_nft_collection_jsx.restore_json')} : + this.fixJson(e, values, setFieldValue)}>{tt('create_nft_collection_jsx.json_fix')}) + : null} +
+
+
+ {tt('create_nft_collection_jsx.token_count')} +
+
+
+
+ this.onMaxTokenCountChange(e, setFieldValue)} + /> +
+
+
+
+ +
+
+
+ {(errorMessage && errorMessage !== 'Canceled') ?
+
+
{errorMessage}
+
+
: null} +
+
+ + + {this._renderSubmittingIndicator()} +
+
+
+ )}}
+
) + } +} + +export default connect( + // mapStateToProps + (state, ownProps) => { + const {locationBeforeTransitions: {pathname}} = state.routing; + let currentUser = ownProps.currentUser || state.user.getIn(['current']) + if (!currentUser) { + const currentUserNameFromRoute = pathname.split(`/`)[1].substring(1); + currentUser = Map({username: currentUserNameFromRoute}); + } + const currentAccount = currentUser && state.global.getIn(['accounts', currentUser.get('username')]); + return { ...ownProps, currentUser, currentAccount, }; + }, + + dispatch => ({ + createCollection: ( + name, json_metadata, max_token_count, currentUser, successCallback, errorCallback + ) => { + const username = currentUser.get('username') + let json = JSON.parse(json_metadata) + json = JSON.stringify(json) + const operation = { + creator: username, + name, + json_metadata: json, + max_token_count: parseInt(max_token_count) + } + + dispatch(transaction.actions.broadcastOperation({ + type: 'nft_collection', + username, + operation, + successCallback, + errorCallback + })) + } + }) +)(CreateNFTCollection) diff --git a/app/components/modules/nft/CreateNFTCollection.scss b/app/components/modules/nft/CreateNFTCollection.scss new file mode 100644 index 0000000..3db9e28 --- /dev/null +++ b/app/components/modules/nft/CreateNFTCollection.scss @@ -0,0 +1,34 @@ +.CreateNFTCollection { + .column { + padding-left: 0rem; + padding-right: 0rem; + } + .padding-left { + padding-left: 0.75rem; + } + .image-preview { + max-width: 150px; + height: 40px; + margin-left: 0.75rem; + border: none; + } + .json_metadata { + min-width: 100%; + min-height: 120px; + font-family: monospace; + } + .json-error { + margin-bottom: 0px; + } + .Expandable { + margin-bottom: 0px; + padding-bottom: 0px; + width: 100%; + .Expander { + margin-bottom: 0px; + h5 { + font-size: 1rem; + } + } + } +} diff --git a/app/components/modules/nft/IssueNFTToken.jsx b/app/components/modules/nft/IssueNFTToken.jsx new file mode 100644 index 0000000..1115534 --- /dev/null +++ b/app/components/modules/nft/IssueNFTToken.jsx @@ -0,0 +1,358 @@ +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 { Asset, validateAccountName } from 'golos-lib-js/lib/utils' + +import Expandable from 'app/components/elements/Expandable' +import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import transaction from 'app/redux/Transaction' + +class IssueNFTToken extends Component { + state = { + token: { + name: '', + to: '', + title: '', + json_metadata: '{}', + } + } + + initTo = (currentUser) => { + if (!currentUser) return + const username = currentUser.get('username') + this.setState({ + token: { + ...this.state.token, + to: username + } + }) + } + + componentDidMount() { + this.initTo(this.props.currentUser) + } + + componentDidUpdate(prevProps) { + const { currentUser } = this.props + if (currentUser && !prevProps.currentUser) { + this.initTo(currentUser) + } + } + + validate = (values) => { + const errors = {} + const { title, to } = values + if (!title.length) { + errors.title = tt('g.required') + } + const accNameError = validateAccountName(values.to) + if (accNameError.error) { + errors.to = tt('account_name.' + accNameError.error) + } + let meta = values.json_metadata + try { + meta = JSON.parse(meta) + if (Array.isArray(meta)) throw new Error('JSON is array') + } catch (err) { + console.error('json_metadata', err) + errors.json_metadata = tt('create_nft_collection_jsx.json_wrong') + meta = null + } + if (meta) { + const noFields = [] + if (!('title' in meta)) noFields.push('title') + if (values.image && !('image' in meta)) noFields.push('image') + if (values.description && !('description' in meta)) noFields.push('description') + if (noFields.length) { + errors.json_metadata = tt('create_nft_collection_jsx.json_no_fields') + errors.json_metadata += noFields.join(', ') + '. ' + } + } + return errors + } + + setSubmitting = (submitting) => { + this.setState({ submitting }) + } + + _onSubmit = async (values) => { + this.setSubmitting(true) + this.setState({ + errorMessage: '' + }) + const { currentUser, issueName } = this.props + const username = currentUser.get('username') + await this.props.issueToken(issueName, values.to, values.json_metadata, currentUser, () => { + this.props.fetchState() + this.props.onClose() + this.setSubmitting(false) + }, (err) => { + console.error(err) + this.setSubmitting(false) + this.setState({ + errorMessage: err.toString() + }) + }) + } + + _renderSubmittingIndicator = () => { + const { submitting } = this.state + + return submitting ? + + : null + } + + updateJSONMetadata = (values, setFieldValue, currentVals, force = false) => { + let { json_metadata } = values + try { + json_metadata = JSON.parse(json_metadata) + if (Array.isArray(json_metadata)) { + json_metadata = {} + } + } catch (err) { + console.error('updateJSONMetadata', err) + if (!force) { + return + } + json_metadata = {} + } + + const title = currentVals.title || values.title + json_metadata.title = title || '' + + const description = currentVals.description || values.description + if (description) { + json_metadata.description = description + } + + const image = currentVals.image || values.image + if (image) { + json_metadata.image = image + } + + setFieldValue('json_metadata', JSON.stringify(json_metadata, null, 2)) + } + + onTitleChange = (e, values, setFieldValue) => { + const title = e.target.value + setFieldValue('title', title) + this.updateJSONMetadata(values, setFieldValue, { title }) + } + + onDescriptionChange = (e, values, setFieldValue) => { + const description = e.target.value + setFieldValue('description', description) + this.updateJSONMetadata(values, setFieldValue, { description }) + } + + onImageChange = (e, values, setFieldValue) => { + const image = e.target.value + setFieldValue('image', image) + this.updateJSONMetadata(values, setFieldValue, { image }) + } + + onImageBlur = (e) => { + e.preventDefault() + this.setState({ + showImage: true + }) + } + + restoreJson = (e, values, setFieldValue) => { + e.preventDefault() + this.updateJSONMetadata(values, setFieldValue, {}, true) + } + + fixJson = (e, values, setFieldValue) => { + e.preventDefault() + this.updateJSONMetadata(values, setFieldValue, {}) + } + + 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() + } + } + + render() { + const { issueName, issueNum, cprops, onClose, } = this.props; + const { submitting, showImage, errorMessage } = this.state + + let cost = null + if (cprops) { + cost =
+ {tt('issue_nft_token_jsx.issue_cost')}  + {Asset(cprops.get('nft_issue_cost')).floatString} + . +
+ } + + return (
+ +

+ {tt('issue_nft_token_jsx.title') + ' (' + issueName + ', #' + issueNum + ')'} +

+ + {({ + handleSubmit, isValid, values, errors, touched, setFieldValue, handleChange, + }) => { + return ( +
+
+
+ {tt('create_nft_collection_jsx.coll_title') + '*'} +
+ this.onTitleChange(e, values, setFieldValue)} /> +
+ +
+
+
+ {tt('assets_jsx.transfer_new_owner')} +
+
+
+
+ +
+ +
+
+
+ {tt('create_nft_collection_jsx.coll_descr')} + {' '} + {tt('create_nft_collection_jsx.not_required')} +
+
+
+
+ this.onDescriptionChange(e, values, setFieldValue)} /> +
+ +
+
+
+ {tt('create_nft_collection_jsx.image')} + {' '} + {tt('create_nft_collection_jsx.not_required')} +
+
+
+
+ this.onImageChange(e, values, setFieldValue)} /> + +
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+ + {(touched.json_metadata && errors.json_metadata) ? + (errors.json_metadata === tt('create_nft_collection_jsx.json_wrong') ? + this.restoreJson(e, values, setFieldValue)}>{tt('create_nft_collection_jsx.restore_json')} : + this.fixJson(e, values, setFieldValue)}>{tt('create_nft_collection_jsx.json_fix')}) + : null} +
+
+ {cost} + {(errorMessage && errorMessage !== 'Canceled') ?
+
+
{errorMessage}
+
+
: null} +
+
+ + + {this._renderSubmittingIndicator()} +
+
+
+ )}}
+
) + } +} + +export default connect( + // mapStateToProps + (state, ownProps) => { + const {locationBeforeTransitions: {pathname}} = state.routing; + let currentUser = ownProps.currentUser || state.user.getIn(['current']) + if (!currentUser) { + const currentUserNameFromRoute = pathname.split(`/`)[1].substring(1); + currentUser = Map({username: currentUserNameFromRoute}); + } + const currentAccount = currentUser && state.global.getIn(['accounts', currentUser.get('username')]); + const cprops = state.global.get('cprops') + return { ...ownProps, currentUser, currentAccount, cprops, } + }, + + dispatch => ({ + issueToken: ( + name, to, json_metadata, currentUser, successCallback, errorCallback + ) => { + const username = currentUser.get('username') + let json = JSON.parse(json_metadata) + json = JSON.stringify(json) + const operation = { + creator: username, + name, + to, + json_metadata: json, + } + + dispatch(transaction.actions.broadcastOperation({ + type: 'nft_issue', + username, + operation, + successCallback, + errorCallback + })) + } + }) +)(IssueNFTToken) diff --git a/app/components/modules/nft/IssueNFTToken.scss b/app/components/modules/nft/IssueNFTToken.scss new file mode 100644 index 0000000..ff33515 --- /dev/null +++ b/app/components/modules/nft/IssueNFTToken.scss @@ -0,0 +1,31 @@ +.IssueNFTToken { + .column { + padding-left: 0rem; + padding-right: 0rem; + } + .image-preview { + max-width: 150px; + height: 40px; + margin-left: 0.75rem; + border: none; + } + .json_metadata { + min-width: 100%; + min-height: 120px; + font-family: monospace; + } + .json-error { + margin-bottom: 0px; + } + .Expandable { + margin-bottom: 0px; + padding-bottom: 0px; + width: 100%; + .Expander { + margin-bottom: 0px; + h5 { + font-size: 1rem; + } + } + } +} diff --git a/app/components/modules/nft/NFTCollections.jsx b/app/components/modules/nft/NFTCollections.jsx new file mode 100644 index 0000000..451affe --- /dev/null +++ b/app/components/modules/nft/NFTCollections.jsx @@ -0,0 +1,208 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import {connect} from 'react-redux'; +import { Link } from 'react-router'; +import tt from 'counterpart'; +import { Asset } from 'golos-lib-js/lib/utils'; +import Reveal from 'react-foundation-components/lib/global/reveal'; + +import Icon from 'app/components/elements/Icon'; +import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import CreateNFTCollection from 'app/components/modules/nft/CreateNFTCollection' +import IssueNFTToken from 'app/components/modules/nft/IssueNFTToken' +import g from 'app/redux/GlobalReducer' +import user from 'app/redux/User' +import transaction from 'app/redux/Transaction' +import { getAssetMeta } from 'app/utils/market/utils' + +class NFTCollections extends Component { + state = {} + + constructor() { + super() + } + + showCreate = (e) => { + e.preventDefault() + this.setState({ + showCreate: true, + }) + } + + hideCreate = () => { + this.setState({ + showCreate: false, + }) + } + + showIssue = (e) => { + e.preventDefault() + this.setState({ + showIssue: true, + }) + } + + hideIssue = () => { + this.setState({ + showIssue: false, + }) + } + + render() { + const { account, isMyAccount, nft_collections, nft_assets, fetchState } = this.props + const accountName = account.get('name') + + const collections = nft_collections ? nft_collections.toJS() : null + const assets = nft_assets ? nft_assets.toJS() : null + + let items + if (!collections) { + items = + } else if (!collections.length) { + if (isMyAccount) { + items = {tt('nft_collections_jsx.not_yet')} + } else { + items = {tt('nft_collections_jsx.not_yet2') + accountName + tt('nft_collections_jsx.not_yet3')} + } + } else { + items = [] + let count = 0 + for (const collection of collections) { + const { name, token_count, json_metadata, image, market_volume, last_buy_price } = collection + + let data + if (json_metadata) { + data = JSON.parse(json_metadata) + } + data = data || {} // node allows to use '', null, object, or array + + const issueIt = (e) => { + e.preventDefault() + this.setState({ + showIssue: true, + issueName: name, + issueNum: token_count + 1 + }) + } + + const deleteIt = async (e) => { + e.preventDefault() + + await this.props.deleteCollection(name, accountName, () => { + this.props.fetchState() + }, (err) => { + console.error(err) + }) + } + + const price = Asset(last_buy_price) + const asset = assets[price.symbol] + let imageUrl + if (asset) { + imageUrl = getAssetMeta(asset).image_url + } + + items.push( + + + + + {name} + + + {tt('nft_tokens_jsx.token_count', { count: token_count })} + {isMyAccount ? : null} + + +
+ {tt('rating_jsx.volume') + ' ' + parseFloat(market_volume).toFixed(3)} +
+
+ {imageUrl && {''}} + {price.amountFloat} +
+ + + {isMyAccount ? : null} + + ) + + ++count + } + + items = + {items} +
+ } + + const { showCreate, showIssue, issueName, issueNum } = this.state + + return (
+
+
+

{tt('g.nft_collections')}

+ {isMyAccount && + {tt('nft_collections_jsx.create')} + } +
+
+
+
+ {items} +
+
+ + + + + + + + +
) + } +} + +export default connect( + (state, ownProps) => { + return {...ownProps, + nft_collections: state.global.get('nft_collections'), + nft_assets: state.global.get('nft_assets') + } + }, + dispatch => ({ + fetchState: () => { + const pathname = window.location.pathname + dispatch({type: 'FETCH_STATE', payload: {pathname}}) + }, + deleteCollection: ( + name, username, successCallback, errorCallback + ) => { + const operation = { + creator: username, + name, + } + + dispatch(transaction.actions.broadcastOperation({ + type: 'nft_collection_delete', + username, + operation, + successCallback, + errorCallback + })) + } + }) +)(NFTCollections) diff --git a/app/components/modules/nft/NFTCollections.scss b/app/components/modules/nft/NFTCollections.scss new file mode 100644 index 0000000..06982f1 --- /dev/null +++ b/app/components/modules/nft/NFTCollections.scss @@ -0,0 +1,32 @@ +.NFTCollections { + .image { + background-size: cover; + background-repeat: no-repeat; + background-position: 50% 50%; + border-radius: 50%; + + width: 3rem; + height: 3rem; + display: inline-block; + vertical-align: top; + } + + .market-stats { + font-size: 90%; + } + .price-icon { + width: 20px; + height: 20px; + margin-right: 0.25rem; + } + .price-val { + display: inline-block; + vertical-align: middle; + } + + .button { + margin-left: 0.5rem; + margin-right: 0rem; + margin-bottom: 0rem; + } +} diff --git a/app/components/modules/nft/NFTTokenDetails.jsx b/app/components/modules/nft/NFTTokenDetails.jsx new file mode 100644 index 0000000..d481f00 --- /dev/null +++ b/app/components/modules/nft/NFTTokenDetails.jsx @@ -0,0 +1,97 @@ +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 { Asset } from 'golos-lib-js/lib/utils' + +import { getAssetMeta } from 'app/utils/market/utils' + +class NFTTokenDetails extends Component { + state = { + } + + onTransferClick = (e) => { + const { tokenIdx } = this.props + this.props.onClose() + this.props.showTransfer(e, tokenIdx) + } + + render() { + const { nft_tokens, nft_assets, tokenIdx, onClose, } = this.props + + const token = nft_tokens.toJS()[tokenIdx] + + const assets = nft_assets.toJS() + + 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 + + let last_price + const last_buy_price = Asset('1.005 GOLOS')// Asset(token.last_buy_price) + if (last_buy_price.amount > 0) { + const asset = assets[last_buy_price.symbol] + let imageUrl + if (asset) { + imageUrl = getAssetMeta(asset).image_url + } + last_price = + {imageUrl && {''}} + {last_buy_price.amountFloat} + + } + + return
+
+
+ + + +
+
+ +

+ {data.title} +

+ {token.name} + {data.description ?
+ {data.description} +
: null} +
+ {last_price} + + +
+
+
+
+ } +} + +export default connect( + // mapStateToProps + (state, ownProps) => { + const {locationBeforeTransitions: {pathname}} = state.routing; + let currentUser = ownProps.currentUser || state.user.getIn(['current']) + if (!currentUser) { + const currentUserNameFromRoute = pathname.split(`/`)[1].substring(1); + currentUser = Map({username: currentUserNameFromRoute}); + } + const currentAccount = currentUser && state.global.getIn(['accounts', currentUser.get('username')]); + return { ...ownProps, currentUser, currentAccount, + nft_tokens: state.global.get('nft_tokens'), + nft_assets: state.global.get('nft_assets') + } + }, + + dispatch => ({ + }) +)(NFTTokenDetails) diff --git a/app/components/modules/nft/NFTTokenDetails.scss b/app/components/modules/nft/NFTTokenDetails.scss new file mode 100644 index 0000000..bf49dbf --- /dev/null +++ b/app/components/modules/nft/NFTTokenDetails.scss @@ -0,0 +1,26 @@ +.NFTTokenDetails { + img { + max-width: 320px; + max-height: 320px; + } + + .price-icon { + width: 20px; + height: 20px; + margin-right: 0.25rem; + } + + .button { + margin: 0px !important; + margin-left: 5px !important; + } + .button:hover { + background-color: #016aad !important; + } + .button.hollow { + border: 0px; + } + .button.hollow:hover { + background-color: transparent !important; + } +} diff --git a/app/components/modules/nft/NFTTokenTransfer.jsx b/app/components/modules/nft/NFTTokenTransfer.jsx new file mode 100644 index 0000000..1a3d137 --- /dev/null +++ b/app/components/modules/nft/NFTTokenTransfer.jsx @@ -0,0 +1,36 @@ +import React, { Component, } from 'react' +import tt from 'counterpart' +import { connect, } from 'react-redux' +import CloseButton from 'react-foundation-components/lib/global/close-button' + +class NFTTokenTransfer extends Component { + state = { + } + + render() { + const { onClose, } = this.props + return
+ +
+ } +} + +export default connect( + // mapStateToProps + (state, ownProps) => { + const {locationBeforeTransitions: {pathname}} = state.routing; + let currentUser = ownProps.currentUser || state.user.getIn(['current']) + if (!currentUser) { + const currentUserNameFromRoute = pathname.split(`/`)[1].substring(1); + currentUser = Map({username: currentUserNameFromRoute}); + } + const currentAccount = currentUser && state.global.getIn(['accounts', currentUser.get('username')]); + return { ...ownProps, currentUser, currentAccount, + nft_tokens: state.global.get('nft_tokens'), + nft_assets: state.global.get('nft_assets') + } + }, + + dispatch => ({ + }) +)(NFTTokenTransfer) diff --git a/app/components/modules/nft/NFTTokenTransfer.scss b/app/components/modules/nft/NFTTokenTransfer.scss new file mode 100644 index 0000000..c0ae000 --- /dev/null +++ b/app/components/modules/nft/NFTTokenTransfer.scss @@ -0,0 +1,2 @@ +.NFTTokenTransfer { +} diff --git a/app/components/modules/nft/NFTTokens.jsx b/app/components/modules/nft/NFTTokens.jsx new file mode 100644 index 0000000..bf5ea09 --- /dev/null +++ b/app/components/modules/nft/NFTTokens.jsx @@ -0,0 +1,180 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import {connect} from 'react-redux' +import { Link } from 'react-router' +import tt from 'counterpart' +import { Asset } from 'golos-lib-js/lib/utils' +import Reveal from 'react-foundation-components/lib/global/reveal' + +import g from 'app/redux/GlobalReducer' +import user from 'app/redux/User' +import DropdownMenu from 'app/components/elements/DropdownMenu' +import Icon from 'app/components/elements/Icon' +import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import NFTTokenDetails from 'app/components/modules/nft/NFTTokenDetails' +import NFTTokenTransfer from 'app/components/modules/nft/NFTTokenTransfer' +import { getAssetMeta } from 'app/utils/market/utils' + +class NFTTokens extends Component { + state = {} + + constructor() { + super() + } + + showTransfer = (e, tokenIdx) => { + e.preventDefault() + this.setState({ + showTransfer: true, + }) + } + + hideTransfer = () => { + this.setState({ + showTransfer: false, + }) + } + + showDetails = (e, tokenIdx) => { + e.preventDefault() + this.setState({ + showDetails: true, + tokenIdx, + }) + } + + hideDetails = () => { + this.setState({ + showDetails: false, + }) + } + + onClick = (e, tokenIdx) => { + e.preventDefault() + for (let node = e.target; node && node != e.currentTarget; node = node.parentNode) { + if (node.onclick) { + return + } + } + this.showDetails(e, tokenIdx) + } + + render() { + const { account, isMyAccount, nft_tokens, nft_assets, fetchState } = this.props + const accountName = account.get('name') + + const tokens = nft_tokens ? nft_tokens.toJS() : null + const assets = nft_assets ? nft_assets.toJS() : null + + let items = [] + if (!tokens) { + items = + } else if (!tokens.length) { + if (isMyAccount) { + items = {tt('nft_tokens_jsx.not_yet')} + } else { + items = {tt('nft_tokens_jsx.not_yet2') + accountName + tt('nft_tokens_jsx.not_yet3')} + } + } else { + for (let i = 0; i < tokens.length; ++i) { + const token = tokens[i] + 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 + + let last_price + const last_buy_price = Asset('1.005 GOLOS')// Asset(token.last_buy_price) + if (last_buy_price.amount > 0) { + const asset = assets[last_buy_price.symbol] + let imageUrl + if (asset) { + imageUrl = getAssetMeta(asset).image_url + } + last_price = + {imageUrl && {''}} + {last_buy_price.amountFloat} + + } + + const kebabItems = [ + { link: '#', onClick: e => { + this.showTransfer(e, i) + }, value: tt('g.transfer') }, + { link: '#', onClick: e => { + this.showDetails(e, i) + }, value: tt('g.more_hint') }, + ] + + items.push(
this.onClick(e, i)}> + +
+
{data.title}
+ {token.name} +
+ {last_price} + {/**/} + + + + +
+
+
) + } + } + + const { showTransfer, showDetails, tokenIdx } = this.state + + return (
+
+
+

{tt('g.nft_tokens')}

+ + {tt('g.buy')} + +
+
+
+
+ {items} +
+
+ + + + + + + this.showTransfer(e, tokenIdx)} + onClose={this.hideDetails} + tokenIdx={tokenIdx} + fetchState={fetchState} + /> + +
) + } +} + +export default connect( + (state, ownProps) => { + return {...ownProps, + nft_tokens: state.global.get('nft_tokens'), + nft_assets: state.global.get('nft_assets') + } + }, + dispatch => ({ + fetchState: () => { + const pathname = window.location.pathname + dispatch({type: 'FETCH_STATE', payload: {pathname}}) + }, + }) +)(NFTTokens) diff --git a/app/components/modules/nft/NFTTokens.scss b/app/components/modules/nft/NFTTokens.scss new file mode 100644 index 0000000..57df615 --- /dev/null +++ b/app/components/modules/nft/NFTTokens.scss @@ -0,0 +1,48 @@ +.NFTTokens { + .Token { + display: inline-block; + border: 1px solid rgba(128,128,128,0.45); + border-radius: 5px; + margin-right: 1rem; + margin-bottom: 1em; + padding: 0.5rem; + cursor: pointer; + + .token-image { + width: 200px; + height: 200px; + } + .price-icon { + width: 20px; + height: 20px; + margin-right: 0.25rem; + } + + .token-title { + margin-top: 0.25rem; + margin-bottom: 0.1rem; + } + .token-coll { + display: inline-block; + margin-bottom: 0.35rem; + } + + .button { + margin: 0px !important; + margin-left: 5px !important; + } + .button:hover { + background-color: #016aad; + } + .button.hollow { + border: 0px; + } + .button.hollow:hover { + background-color: transparent; + } + } + .Token:hover { + background-color: rgba(168,168,168,0.45); + border: 1px solid rgba(128,128,128,0.45); + } +} diff --git a/app/components/pages/NFTMarketPage.jsx b/app/components/pages/NFTMarketPage.jsx new file mode 100644 index 0000000..06d0a87 --- /dev/null +++ b/app/components/pages/NFTMarketPage.jsx @@ -0,0 +1,25 @@ +import React from 'react' +import { connect, } from 'react-redux' +import tt from 'counterpart' + +class NFTMarketPage extends React.Component { + render() { + const { currentAccount, routeParams } = this.props + } +} + +module.exports = { + path: '/nft-market(/:sym)', + component: connect( + (state, ownProps) => { + const currentUser = state.user.getIn(['current']) + const currentAccount = currentUser && state.global.getIn(['accounts', currentUser.get('username')]) + + return { + currentAccount, + } + }, + dispatch => ({ + }) + )(NFTMarketPage), +} diff --git a/app/components/pages/UserProfile.jsx b/app/components/pages/UserProfile.jsx index 14c99c1..282516d 100644 --- a/app/components/pages/UserProfile.jsx +++ b/app/components/pages/UserProfile.jsx @@ -14,6 +14,8 @@ import CreateAsset from 'app/components/modules/uia/CreateAsset'; import Assets from 'app/components/modules/uia/Assets'; import UpdateAsset from 'app/components/modules/uia/UpdateAsset'; import TransferAsset from 'app/components/modules/uia/TransferAsset'; +import NFTCollections from 'app/components/modules/nft/NFTCollections' +import NFTTokens from 'app/components/modules/nft/NFTTokens' import Invites from 'app/components/elements/Invites'; import PasswordReset from 'app/components/elements/PasswordReset'; import UserWallet from 'app/components/modules/UserWallet'; @@ -246,8 +248,15 @@ export default class UserProfile extends React.Component {
; - } - else if( section === 'curation-rewards' ) { + } else if( section === 'nft-collections' ) { + tab_content =
+ +
+ } else if( section === 'nft' ) { + tab_content =
+ +
+ } else if( section === 'curation-rewards' ) { rewardsClass = 'active'; tab_content = {tt('g.assets')} +
+ } + > + + {tt('g.nft')} + + + +
{isMyAccount ? {tt('navigation.market2')} : null} diff --git a/app/locales/en.json b/app/locales/en.json index d0d9054..3e519f9 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -133,6 +133,9 @@ "reset_password": "Password reset", "invites": "Invite checks", "assets": "UIA assets", + "nft": "NFT", + "nft_tokens": "NFT-tokens", + "nft_collections": "NFT-collections", "help_wallet": "Wallet functions", "phone": "phone", "post": "Post", @@ -511,6 +514,16 @@ "other": "%(count)s posts" } }, + "account_name": { + "account_name_should_be_shorter": "Account name should be shorter.", + "account_name_should_be_longer": "Account name should be longer.", + "account_name_should_not_be_empty": "Account name should not be empty.", + "each_account_segment_should_start_with_a_letter": "Each account name segment should start with a letter.", + "each_account_segment_should_have_only_letters_digits_or_dashes": "Each account name segment should have only letters, digits or dashes.", + "each_account_segment_should_have_only_one_dash_in_a_row": "Each account name segment should have only one dash in a row.", + "each_account_segment_should_end_with_a_letter_or_digit": "Each account name segment should end with a letter or digit.", + "each_account_segment_should_be_longer": "Each account name segment should be longer." + }, "authorrewards_jsx": { "estimated_author_rewards_last_week": "Estimated author rewards last week", "author_rewards_history": "Author Rewards History" @@ -715,6 +728,47 @@ "no_way_error": "If you set at leas one of these fields, you should also set at least 1 withdrawal method or Details field.", "no_to_error": "If you set withdraw via transfer with memo, please also set, to which account to transfer." }, + "nft_collections_jsx": { + "not_yet": "You haven't any own NFT-token collections.", + "not_yet2": "", + "not_yet3": " haven't any own NFT-token collections.", + "create": "Create collection", + "name_exists": "Collection with such name is already created by another author", + "tokens_exist": "Cannot delete collection which has tokens", + "volume_hint": "Market volume for all time", + "price_hint": "Last buy price" + }, + "create_nft_collection_jsx": { + "title": "Create NFT collection", + "name": "Token name", + "coll_title": "Title", + "coll_descr": "Description", + "image": "Image URL", + "json_metadata": "JSON metadata", + "json_wrong": "Wrong JSON. ", + "not_required": "(not required)", + "token_count": "Max token count", + "infinity": "Infinity", + "create": "Create", + "restore_json": "Restore default", + "json_no_fields": "In JSON missing fields: ", + "json_fix": "Add missing fields" + }, + "issue_nft_token_jsx": { + "title": "Issue NFT", + "issue_cost": "The issue cost is", + "max_token_count": "Max token count in collection is already reached." + }, + "nft_tokens_jsx": { + "not_yet": "You have no NFT-tokens.", + "not_yet2": "", + "not_yet3": " has no NFT-tokens.", + "token_count": { + "zero": "0 tokens", + "one": "1 token", + "other": "%(count)s tokens" + } + }, "invites_jsx": { "create_invite": "Create new invite check", "create_invite_info": "Cheques (invite codes) are a universal tool for transferring of GOLOS tokens to other people outside the blockchain. There are two ways to redeem the code: transfer its balance to your account or register a new account using it.", @@ -918,6 +972,9 @@ "author_rewards_by": "Author rewards by", "donates_from": "Donates from", "donates_to": "Donates to", + "nft_tokens": "NFT-tokens", + "nft_collections": "NFT-collections, created by", + "nft_market": "NFT-market", "replies_to": "Replies to", "comments_by": "Comments by" }, diff --git a/app/locales/ru-RU.json b/app/locales/ru-RU.json index d4a29b1..8c1b35b 100644 --- a/app/locales/ru-RU.json +++ b/app/locales/ru-RU.json @@ -12,6 +12,16 @@ "TIP_TOKEN": "TIP-баланс", "CLAIM_TOKEN": "Накопительный баланс" }, + "account_name": { + "account_name_should_be_shorter": "Имя аккаунта должно быть короче.", + "account_name_should_be_longer": "Имя аккаунта должно быть длиннее.", + "account_name_should_not_be_empty": "Имя аккаунта не должно быть пустым.", + "each_account_segment_should_start_with_a_letter": "Каждый сегмент имени аккаунта должен начинаться с буквы.", + "each_account_segment_should_have_only_letters_digits_or_dashes": "Сегмент имени аккаунта может содержать только буквы, цифры и дефисы.", + "each_account_segment_should_have_only_one_dash_in_a_row": "Каждый сегмент имени аккаунта может содержать только один дефис.", + "each_account_segment_should_end_with_a_letter_or_digit": "Каждый сегмент имени аккаунта должен заканчиваться буквой или цифрой.", + "each_account_segment_should_be_longer": "Сегмент имени аккаунта должен быть длиннее." + }, "authorrewards_jsx": { "estimated_author_rewards_last_week": "Оценочные авторские вознаграждения за неделю", "author_rewards_history": "История авторских наград" @@ -242,6 +252,9 @@ "reset_password": "Сброс пароля", "invites": "Инвайт-чеки", "assets": "Активы UIA", + "nft": "NFT", + "nft_tokens": "Токены NFT", + "nft_collections": "Коллекции NFT", "help_wallet": "Функции кошелька", "phone": "телефон", "post": "Пост", @@ -501,6 +514,9 @@ "author_rewards_by": "Автор награжден", "donates_from": "Донаты, отправленные", "donates_to": "Донаты, полученные", + "nft_tokens": "NFT-токены", + "nft_collections": "NFT-коллекции, созданные", + "nft_market": "Биржа NFT", "replies_to": "Ответы на", "comments_by": "Комментарии" }, @@ -1053,6 +1069,47 @@ "no_way_error": "Если вы заполнили одно из полей, то вы должны указать хотя бы один способ вывода или заполнить Дополнительно.", "no_to_error": "Если вы указали вывод через перевод с заметкой, то укажите, куда переводить." }, + "nft_collections_jsx": { + "not_yet": "У вас нет своих собственных коллекций NFT-токенов.", + "not_yet2": "У ", + "not_yet3": " нет своих собственных коллекций NFT-токенов.", + "create": "Создать коллекцию", + "name_exists": "Коллекция с таким именем уже создана другим автором", + "tokens_exist": "Нельзя удалить коллекцию, в которой уже есть токены", + "volume_hint": "Объем торгов за все время", + "price_hint": "Последняя цена покупки" + }, + "create_nft_collection_jsx": { + "title": "Создать коллекцию NFT", + "name": "Имя токена", + "coll_title": "Название", + "coll_descr": "Описание", + "image": "Ссылка на изображение", + "json_metadata": "JSON-метаданные", + "json_wrong": "Некорректный JSON. ", + "not_required": "(не обязательно)", + "token_count": "Количество токенов", + "infinity": "Бесконечное", + "create": "Создать", + "restore_json": "Вернуть по умолчанию", + "json_no_fields": "В JSON не хватает полей: ", + "json_fix": "Добавить недостающее" + }, + "issue_nft_token_jsx": { + "title": "Выпустить NFT", + "issue_cost": "Будет списана плата за выпуск -", + "max_token_count": "Достигнуто максимальное кол-во токенов в коллекции." + }, + "nft_tokens_jsx": { + "not_yet": "У вас пока нет NFT-токенов.", + "not_yet2": "У ", + "not_yet3": " пока нет NFT-токенов.", + "token_count": { + "zero": "0 токенов", + "one": "1 токен", + "other": "%(count)s токенов" + } + }, "invites_jsx": { "create_invite": "Создание чека", "create_invite_info": "Чеки (инвайт-коды) — инструмент для передачи токенов другим людям вне блокчейна. Использовать чек можно двумя способами: перевести его баланс на аккаунт (форма для этого ниже) или зарегистрировать с его помощью новый аккаунт.", diff --git a/app/redux/FetchDataSaga.js b/app/redux/FetchDataSaga.js index 6c48c16..43bd1ad 100644 --- a/app/redux/FetchDataSaga.js +++ b/app/redux/FetchDataSaga.js @@ -1,6 +1,7 @@ import { call, put, select, fork, cancelled, takeLatest, takeEvery } from 'redux-saga/effects'; import cookie from "react-cookie"; import {config, api} from 'golos-lib-js'; +import { Asset } from 'golos-lib-js/lib/utils' import { getPinnedPosts, getMutedInNew } from 'app/utils/NormalizeProfile'; import { getBlockings, listBlockings } from 'app/redux/BlockingSaga' @@ -11,6 +12,7 @@ import constants from './constants'; 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' export function* fetchDataWatches () { yield fork(watchLocationChange); @@ -124,6 +126,92 @@ export function* fetchState(location_change_action) { state.cprops = yield call([api, api.getChainPropertiesAsync]) break + case 'nft-collections': + state.nft_collections = (yield call([api, api.getNftCollectionsAsync], { + creator: uname, + limit: 100, + sort: 'by_created' + })) + + try { + const noImgColls = {} + for (let i = 0; i < state.nft_collections.length; ++i) { + const nco = state.nft_collections[i] + + nco.image = parseNFTImage(nco.json_metadata) + if (!nco.image) { + noImgColls[nco.name] = i + } + } + + const noImgKeys = Object.keys(noImgColls) + + const tokens = (yield call([api, api.getNftTokensAsync], { + select_collections: noImgKeys, + collection_limit: 1, + limit: 100, + collections: false, + orders: false, + })) + + for (const token of tokens) { + const idx = noImgColls[token.name] + const nco = state.nft_collections[idx] + nco.image = parseNFTImage(token.json_metadata) + } + + const syms = new Set() + + for (const nco of state.nft_collections) { + nco.image = nco.image || NFTImageStub() + + const price = Asset(nco.last_buy_price) + syms.add(price.symbol) + } + + state.nft_assets = {} + if (syms.size) { + const nft_assets = yield call([api, api.getAssets], '', [...syms]) + for (const a of nft_assets) { + const supply = Asset(a.supply) + state.nft_assets[supply.symbol] = a + } + } + } catch (err) { + console.error(err) + } + + state.cprops = yield call([api, api.getChainPropertiesAsync]) + break + + case 'nft': + state.nft_tokens = (yield call([api, api.getNftTokensAsync], { + owner: uname + })) + + try { + const syms = new Set() + + for (const no of state.nft_tokens) { + no.image = parseNFTImage(no.json_metadata) || NFTImageStub() + + const price = Asset(no.last_buy_price) + syms.add(price.symbol) + } + + state.nft_assets = {} + if (syms.size) { + const nft_assets = yield call([api, api.getAssets], '', [...syms]) + for (const a of nft_assets) { + const supply = Asset(a.supply) + state.nft_assets[supply.symbol] = a + } + } + } catch (err) { + console.error(err) + } + break + case 'invites': state.cprops = yield call([api, api.getChainPropertiesAsync]) break diff --git a/app/redux/GlobalReducer.js b/app/redux/GlobalReducer.js index ce156bd..76c49dd 100644 --- a/app/redux/GlobalReducer.js +++ b/app/redux/GlobalReducer.js @@ -52,7 +52,12 @@ export default createModule({ ); } } - let res = state.mergeDeep(payload) + let res = state + if (res.has('nft_collections')) + res = res.delete('nft_collections') + if (res.has('nft_tokens')) + res = res.delete('nft_tokens') + res = res.mergeDeep(payload) return res }, }, diff --git a/app/redux/Transaction_Error.js b/app/redux/Transaction_Error.js index 0ea51cd..e05e861 100644 --- a/app/redux/Transaction_Error.js +++ b/app/redux/Transaction_Error.js @@ -8,6 +8,8 @@ export default function transactionErrorReducer( let errorStr = error.toString(); let errorKey = 'Transaction broadcast error.'; + let handled = false + for (const [type] of operations) { switch (type) { case 'vote': @@ -42,6 +44,21 @@ export default function transactionErrorReducer( errorKey = errorStr = tt('invites_jsx.claim_wrong_secret_fatal'); } break; + case 'nft_collection': + if (errorStr.includes('Object already exist')) { + errorKey = errorStr = tt('nft_collections_jsx.name_exists') + handled = true + } + break; + case 'nft_issue': + if (errorStr.includes('Account does not have sufficient funds')) { + errorKey = errorStr = tt('transfer_jsx.insufficient_funds') + handled = true + } else if (errorStr.includes('Cannot issue more tokens')) { + errorKey = errorStr = tt('issue_nft_token_jsx.max_token_count') + handled = true + } + break; case 'withdraw_vesting': if ( errorStr.includes( @@ -64,7 +81,6 @@ export default function transactionErrorReducer( break; } - let handled = false if (errorStr.includes('You are blocked by user')) { errorKey = errorStr = tt('chain_errors.user_blocked_user') handled = true diff --git a/app/utils/NFTUtils.js b/app/utils/NFTUtils.js new file mode 100644 index 0000000..d5fb6bc --- /dev/null +++ b/app/utils/NFTUtils.js @@ -0,0 +1,12 @@ + +export function parseNFTImage(json_metadata) { + if (json_metadata) { + const meta = JSON.parse(json_metadata) + if (meta) return meta.image + } + return null +} + +export function NFTImageStub() { + return require('app/assets/images/nft.png') +} diff --git a/package.json b/package.json index 4aa7f8c..95faadd 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "foundation-sites": "^6.4.3", "fs-extra": "^10.0.1", "git-rev-sync": "^3.0.2", - "golos-lib-js": "^0.9.53", + "golos-lib-js": "^0.9.56", "history": "^2.0.0-rc2", "immutable": "^3.8.2", "intl": "^1.2.5", diff --git a/yarn.lock b/yarn.lock index ddbc36f..ecd2f83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2185,14 +2185,15 @@ assert@^1.4.1: util "0.10.3" assert@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-2.0.0.tgz#95fc1c616d48713510680f2eaf2d10dd22e02d32" - integrity sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A== + version "2.1.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd" + integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw== dependencies: - es6-object-assign "^1.1.0" - is-nan "^1.2.1" - object-is "^1.0.1" - util "^0.12.0" + call-bind "^1.0.2" + is-nan "^1.3.2" + object-is "^1.1.5" + object.assign "^4.1.4" + util "^0.12.5" assertion-error@^1.1.0: version "1.1.0" @@ -2970,9 +2971,9 @@ core-js@^2.4.0, core-js@^2.5.0: integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== core-js@^3.17.3: - version "3.30.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.0.tgz#64ac6f83bc7a49fd42807327051701d4b1478dea" - integrity sha512-hQotSSARoNh1mYPi9O2YaWeiq/cEB95kOrFb4NCrO4RIFt1qqNpKsaE+vy/L3oiqvND5cThqXzUU3r9F7Efztg== + version "3.32.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.32.2.tgz#172fb5949ef468f93b4be7841af6ab1f21992db7" + integrity sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ== core-js@^3.19.1, core-js@^3.6.0, core-js@^3.8.3: version "3.25.0" @@ -3605,11 +3606,6 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es6-object-assign@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c" - integrity sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw== - escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -4204,10 +4200,10 @@ globule@^1.0.0: lodash "^4.17.21" minimatch "~3.0.2" -golos-lib-js@^0.9.53: - version "0.9.53" - resolved "https://registry.yarnpkg.com/golos-lib-js/-/golos-lib-js-0.9.53.tgz#8469e0cfb5b0183bb0b161279ceb059907e5251e" - integrity sha512-ipjFLKOXhhI31JuQzYsgZtZbFJQem15PRwU0OG+ocnQtUHB1CsU02GGJV0la3gL+K2KvHZdmKqd8XzGx8jgA4g== +golos-lib-js@^0.9.56: + version "0.9.56" + resolved "https://registry.yarnpkg.com/golos-lib-js/-/golos-lib-js-0.9.56.tgz#3dfe8c0658fba2f50976ef49103ddc3f34109c19" + integrity sha512-h9ay0q2AuHiYL8aFXsCGoEFe6ojHt67FHMv8W6oWbqayl44JlRuuEysfE1MZQiiLwzBDFOO1SNMAtv5sE0bRcg== dependencies: abort-controller "^3.0.0" assert "^2.0.0" @@ -4849,7 +4845,7 @@ is-lambda@^1.0.1: resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== -is-nan@^1.2.1: +is-nan@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== @@ -6073,7 +6069,7 @@ object-inspect@^1.12.2: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== -object-is@^1.0.1: +object-is@^1.0.1, object-is@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== @@ -7242,11 +7238,16 @@ selfsigned@^2.1.1: dependencies: node-forge "^1" -"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0, semver@^5.7.0: +"semver@2 || 3 || 4 || 5", semver@^5.6.0, semver@^5.7.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@^5.5.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -8148,7 +8149,7 @@ util@0.10.3: safe-buffer "^5.1.2" which-typed-array "^1.1.2" -util@^0.12.0: +util@^0.12.5: version "0.12.5" resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== @@ -8531,9 +8532,9 @@ ws@^5.2.0: async-limiter "~1.0.0" ws@^8.2.3: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + version "8.14.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.1.tgz#4b9586b4f70f9e6534c7bb1d3dc0baa8b8cf01e0" + integrity sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A== ws@^8.4.2: version "8.11.0"