Submit.jsx
1 import React from 'react' 2 import PropTypes from 'prop-types' 3 import ReactImageFallback from 'react-image-fallback' 4 import styles from './Submit.module.scss' 5 import Modal from '../../common/components/Modal' 6 import Network from '../../common/components/Network' 7 import CategorySelector from '../CategorySelector/CategorySelector.picker' 8 import Slider from '../../common/components/Slider/Slider' 9 import CategoriesUtils from '../Categories/Categories.utils' 10 import { checkNetwork } from '../../common/blockchain/utils' 11 import Categories from '../../common/utils/categories' 12 import icon from '../../common/assets/images/icon.svg' 13 import sntIcon from '../../common/assets/images/SNT.svg' 14 import 'rc-slider/assets/index.css' 15 import 'rc-tooltip/assets/bootstrap.css' 16 import { DappState } from '../../common/data/dapp' 17 import validator from 'validator' 18 import { emailValidation } from '../../common/utils/email-utils' 19 20 const getCategoryName = category => 21 Categories.find(x => x.key === category).value 22 23 class Submit extends React.Component { 24 constructor(props) { 25 super(props) 26 this.imgCanvas = React.createRef() 27 this.previousMoveX = 0 28 this.previousMoveY = 0 29 this.onInputEmail = this.onInputEmail.bind(this) 30 this.onInputName = this.onInputName.bind(this) 31 this.onInputDesc = this.onInputDesc.bind(this) 32 this.onInputUrl = this.onInputUrl.bind(this) 33 this.onChangeImage = this.onChangeImage.bind(this) 34 this.onChangeZoom = this.onChangeZoom.bind(this) 35 this.onStartMove = this.onStartMove.bind(this) 36 this.onMouseMove = this.onMouseMove.bind(this) 37 this.onTouchMove = this.onTouchMove.bind(this) 38 this.onEndMove = this.onEndMove.bind(this) 39 this.onImgDone = this.onImgDone.bind(this) 40 this.onSubmit = this.onSubmit.bind(this) 41 this.handleSNTChange = this.handleSNTChange.bind(this) 42 this.onClickSubmit = this.onClickSubmit.bind(this) 43 44 this.state = { 45 isTestnet: false, 46 } 47 } 48 49 componentDidMount() { 50 (async () => { 51 const isTestnet = await checkNetwork() 52 this.setState({ isTestnet }) 53 })() 54 } 55 56 componentDidUpdate() { 57 const { img, imgControlZoom, imgControlX, imgControlY } = this.props 58 if (img === '') return 59 60 const canvas = this.imgCanvas.current 61 if (canvas === null) return 62 63 const ctx = canvas.getContext('2d') 64 const imgNode = new Image() 65 imgNode.onload = () => { 66 const s = parseInt( 67 Math.min(imgNode.width, imgNode.height) / 68 ((imgControlZoom + 100) / 100), 69 10, 70 ) 71 const k = canvas.width / s 72 ctx.fillStyle = '#fff' 73 ctx.fillRect(0, 0, canvas.width, canvas.height) 74 ctx.drawImage( 75 imgNode, 76 0, 77 0, 78 imgNode.width, 79 imgNode.height, 80 imgControlX + (s - imgNode.width) * 0.5 * k, 81 imgControlY + (s - imgNode.height) * 0.5 * k, 82 imgNode.width * k, 83 imgNode.height * k, 84 ) 85 } 86 imgNode.src = img 87 } 88 89 onInputEmail(e) { 90 const { onInputEmail } = this.props 91 onInputEmail(e.target.value) 92 } 93 94 onInputName(e) { 95 const { onInputName } = this.props 96 onInputName(e.target.value) 97 } 98 99 onInputDesc(e) { 100 const { onInputDesc } = this.props 101 onInputDesc(e.target.value) 102 } 103 104 onInputUrl(e) { 105 const { onInputUrl } = this.props 106 onInputUrl(e.target.value) 107 } 108 109 onChangeImage(e) { 110 const input = e.target 111 const { files } = e.target 112 if (files === 0) return 113 114 const file = files[0] 115 const fileReader = new FileReader() 116 fileReader.onload = ev => { 117 const { onImgRead } = this.props 118 input.value = '' 119 onImgRead(ev.target.result) 120 } 121 fileReader.readAsDataURL(file, 'UTF-8') 122 } 123 124 onChangeZoom(value) { 125 const { onImgZoom } = this.props 126 onImgZoom(value) 127 } 128 129 onStartMove() { 130 const { onImgMoveControl } = this.props 131 this.previousMoveX = -1 132 this.previousMoveY = -1 133 onImgMoveControl(true) 134 } 135 136 onMouseMove(e) { 137 const { imgControlMove, imgControlX, imgControlY, onImgMove } = this.props 138 if (!imgControlMove) return 139 const x = imgControlX + e.movementX 140 const y = imgControlY + e.movementY 141 requestAnimationFrame(() => { 142 onImgMove(x, y) 143 }) 144 } 145 146 onTouchMove(e) { 147 const { imgControlMove, imgControlX, imgControlY, onImgMove } = this.props 148 if (!imgControlMove) return 149 150 const touch = e.touches[0] 151 if (this.previousMoveX !== -1 && this.previousMoveY !== -1) { 152 const x = imgControlX - (this.previousMoveX - touch.screenX) 153 const y = imgControlY - (this.previousMoveY - touch.screenY) 154 requestAnimationFrame(() => { 155 onImgMove(x, y) 156 }) 157 } 158 159 this.previousMoveX = touch.screenX 160 this.previousMoveY = touch.screenY 161 } 162 163 onEndMove() { 164 const { onImgMoveControl } = this.props 165 onImgMoveControl(false) 166 } 167 168 onImgDone() { 169 const { onImgDone } = this.props 170 const canvas = this.imgCanvas.current 171 const imgBase64 = canvas.toDataURL('image/jpg') 172 onImgDone(imgBase64) 173 } 174 175 onClickSubmit() { 176 const { id, url, showAlert, switchToRating } = this.props 177 178 if (!validator.isURL(url, { require_protocol: true })) { 179 showAlert('Invalid URL address') 180 return 181 } 182 183 const functor = id === '' ? switchToRating : this.onSubmit 184 functor() 185 } 186 187 onSubmit() { 188 const { 189 onSubmit, 190 onUpdate, 191 id, 192 email, 193 name, 194 desc, 195 url, 196 img, 197 category, 198 sntValue, 199 } = this.props 200 201 const metadata = { 202 email, 203 name, 204 url, 205 image: img, 206 category, 207 description: desc, 208 dateAdded: Date.now(), 209 } 210 211 if (id === '') onSubmit(metadata, parseInt(sntValue, 10)) 212 else onUpdate(id, metadata) 213 } 214 215 handleSNTChange(e) { 216 const { value } = e.target 217 if (value !== '' && /^[1-9][0-9]*$/.test(value) === false) return 218 219 const intValue = value === '' ? 0 : parseInt(value, 10) 220 if (intValue > 1571296) return 221 222 const { onInputSntValue } = this.props 223 onInputSntValue(value) 224 } 225 226 title() { 227 const { visible_submit, imgControl } = this.props 228 if (visible_submit) 229 return imgControl ? 'Position and size your image' : 'Submit a Ðapp' 230 return 'Ðapp rating' 231 } 232 233 render() { 234 const { isTestnet } = this.state 235 236 const { 237 dappState, 238 visible_submit, 239 visible_rating, 240 onClickClose, 241 name, 242 desc, 243 url, 244 img, 245 category, 246 email, 247 imgControl, 248 imgControlZoom, 249 onImgCancel, 250 onClickTerms, 251 sntValue, 252 } = this.props 253 254 const canSubmit = 255 name !== '' && 256 desc !== '' && 257 url !== '' && 258 img !== '' && 259 category !== '' && 260 emailValidation(email) 261 262 const visible = visible_submit || visible_rating 263 264 /* rating */ 265 let currentSNTamount = 0 266 let dappsByCategory = [] 267 let catPosition = 0 268 let afterVoteRating = null 269 let afterVoteCategoryPosition = null 270 271 if (visible_rating) { 272 dappsByCategory = dappState.getDappsByCategory(category) 273 274 catPosition = dappsByCategory.length + 1 275 if (sntValue !== '') { 276 afterVoteRating = parseInt(sntValue, 10) 277 afterVoteCategoryPosition = 1 278 for (let i = 0; i < dappsByCategory.length; ++i) { 279 if (dappsByCategory[i].sntValue < afterVoteRating) break 280 afterVoteCategoryPosition++ 281 } 282 } 283 } 284 285 return ( 286 <Modal 287 visible={visible && window.location.hash === '#submit'} 288 onClickClose={onClickClose} 289 windowClassName={styles.modalWindow} 290 contentClassName={ 291 imgControl || visible_rating ? styles.modalContentFullScreen : '' 292 } 293 > 294 <div className={styles.title}>{this.title()}</div> 295 {visible_submit && ( 296 <div className={imgControl ? styles.cntWithImgControl : ''}> 297 <div className={imgControl ? styles.withImgControl : ''}> 298 <div className={styles.block}> 299 <div className={styles.labelRow}> 300 <span>Name of your Ðapp</span> 301 </div> 302 <input 303 className={styles.input} 304 placeholder="Name" 305 value={name} 306 onChange={this.onInputName} 307 /> 308 </div> 309 <div className={styles.block}> 310 <div className={styles.labelRow}> 311 <span>A short description</span> 312 <span>{desc.length}/140</span> 313 </div> 314 <textarea 315 className={styles.input} 316 placeholder="Max 140 characters" 317 value={desc} 318 onChange={this.onInputDesc} 319 /> 320 </div> 321 <div className={styles.block}> 322 <div className={styles.labelRow}> 323 <span>URL</span> 324 </div> 325 <input 326 className={styles.input} 327 placeholder="https://your.dapp.cool" 328 value={url} 329 onChange={this.onInputUrl} 330 /> 331 </div> 332 <div className={styles.block}> 333 <div className={styles.labelRow}> 334 <span>Upload the logo or icon of your Ðapp</span> 335 </div> 336 <div className={styles.imgCnt}> 337 <span>Choose image</span> 338 <div 339 className={styles.imgHolder} 340 style={{ backgroundImage: `url(${img})` }} 341 /> 342 <input 343 className={styles.uploader} 344 type="file" 345 onChange={this.onChangeImage} 346 accept=".jpg, .png" 347 /> 348 </div> 349 <div className={styles.imgInfo}> 350 The image should be a square 1:1 ratio JPG or PNG file, 351 minimum size is 160 × 160 pixels. The image will be placed in 352 a circle 353 </div> 354 </div> 355 <div className={styles.block}> 356 <div className={styles.labelRow}> 357 <span>Category</span> 358 </div> 359 <CategorySelector 360 category={category === '' ? null : category} 361 className={`${styles.categorySelector} ${ 362 category === '' ? styles.categorySelectorEmpty : '' 363 }`} 364 /> 365 </div> 366 <div className={styles.block}> 367 <div className={styles.labelRow}> 368 <span>Your email</span> 369 </div> 370 <input 371 className={styles.input} 372 placeholder="email" 373 value={email} 374 onChange={this.onInputEmail} 375 /> 376 </div> 377 <div className={`${styles.block} ${styles.blockSubmit}`}> 378 <div className={styles.terms}> 379 By continuing you agree to our 380 <span onClick={onClickTerms} className="link"> 381 {' '} 382 Terms and Conditions. 383 </span> 384 </div> 385 <button 386 className={styles.submitButton} 387 type="submit" 388 disabled={!canSubmit} 389 onClick={this.onClickSubmit} 390 > 391 Continue 392 </button> 393 </div> 394 </div> 395 {imgControl && ( 396 <div className={styles.imgControl}> 397 <div 398 className={styles.imgCanvasCnt} 399 onMouseDown={this.onStartMove} 400 onMouseMove={this.onMouseMove} 401 onMouseUp={this.onEndMove} 402 onMouseLeave={this.onEndMove} 403 onTouchStart={this.onStartMove} 404 onTouchMove={this.onTouchMove} 405 onTouchEnd={this.onEndMove} 406 onTouchCancel={this.onEndMove} 407 > 408 <canvas 409 ref={this.imgCanvas} 410 className={styles.imgCanvas} 411 width="160" 412 height="160" 413 /> 414 </div> 415 <div className={styles.controls}> 416 <div className={styles.slider}> 417 <div className={styles.minZoom} /> 418 <Slider 419 min={0} 420 max={300} 421 value={imgControlZoom} 422 onChange={this.onChangeZoom} 423 /> 424 <div className={styles.maxZoom} /> 425 </div> 426 <div className={styles.actionsCnt}> 427 <button 428 className={`${styles.button} ${styles.cancelButton}`} 429 onClick={onImgCancel} 430 > 431 Cancel 432 </button> 433 <button 434 className={`${styles.button} ${styles.doneButton}`} 435 onClick={this.onImgDone} 436 > 437 Done 438 </button> 439 </div> 440 </div> 441 </div> 442 )} 443 </div> 444 )} 445 {visible_rating && ( 446 <> 447 <div className={styles.dapp}> 448 <ReactImageFallback 449 className={styles.image} 450 src={img} 451 fallbackImage={icon} 452 alt="App icon" 453 width={24} 454 height={24} 455 /> 456 {name} 457 </div> 458 <div className={styles.items}> 459 <div className={styles.itemRow}> 460 <span className={styles.item}> 461 <img src={sntIcon} alt="SNT" width="24" height="24" /> 462 {currentSNTamount.toLocaleString()} 463 </span> 464 {afterVoteRating !== null && 465 afterVoteRating !== currentSNTamount && ( 466 <span className={styles.greenBadge}> 467 {`${afterVoteRating.toLocaleString()} ↑`} 468 </span> 469 )} 470 </div> 471 <div className={styles.itemRow}> 472 <span className={styles.item}> 473 <img 474 src={CategoriesUtils(category)} 475 alt={getCategoryName(category)} 476 width="24" 477 height="24" 478 /> 479 {`${getCategoryName(category)} №${catPosition}`} 480 </span> 481 {afterVoteCategoryPosition !== null && 482 afterVoteCategoryPosition !== catPosition && ( 483 <span className={styles.greenBadge}> 484 {`№${afterVoteCategoryPosition} ↑`} 485 </span> 486 )} 487 </div> 488 </div> 489 <div className={`${styles.inputArea} ${styles.inputAreaBorder}`}> 490 <input 491 type="text" 492 value={sntValue} 493 onChange={this.handleSNTChange} 494 style={{ width: `${21 * Math.max(1, sntValue.length)}px` }} 495 /> 496 </div> 497 <div className={styles.footer}> 498 <p className={styles.disclaimer}> 499 SNT you spend to rank your DApp is locked in the store. You can 500 earn back through votes, or withdraw, the majority of this SNT 501 at any time. 502 </p> 503 {isTestnet && <Network />} 504 <button type="submit" onClick={this.onSubmit}> 505 {!sntValue || sntValue === '0' 506 ? 'Publish' 507 : 'Stake and Publish'} 508 </button> 509 </div> 510 </> 511 )} 512 </Modal> 513 ) 514 } 515 } 516 517 Submit.propTypes = { 518 visible_submit: PropTypes.bool.isRequired, 519 visible_rating: PropTypes.bool.isRequired, 520 email: PropTypes.string.isRequired, 521 name: PropTypes.string.isRequired, 522 desc: PropTypes.string.isRequired, 523 url: PropTypes.string.isRequired, 524 img: PropTypes.string.isRequired, 525 category: PropTypes.string.isRequired, 526 imgControl: PropTypes.bool.isRequired, 527 imgControlZoom: PropTypes.number.isRequired, 528 imgControlMove: PropTypes.bool.isRequired, 529 imgControlX: PropTypes.number.isRequired, 530 imgControlY: PropTypes.number.isRequired, 531 sntValue: PropTypes.string.isRequired, 532 onClickClose: PropTypes.func.isRequired, 533 onInputEmail: PropTypes.func.isRequired, 534 onInputName: PropTypes.func.isRequired, 535 onInputDesc: PropTypes.func.isRequired, 536 onInputUrl: PropTypes.func.isRequired, 537 onImgRead: PropTypes.func.isRequired, 538 onImgZoom: PropTypes.func.isRequired, 539 onImgMoveControl: PropTypes.func.isRequired, 540 onImgMove: PropTypes.func.isRequired, 541 onImgCancel: PropTypes.func.isRequired, 542 onImgDone: PropTypes.func.isRequired, 543 onSubmit: PropTypes.func.isRequired, 544 onUpdate: PropTypes.func.isRequired, 545 onInputSntValue: PropTypes.func.isRequired, 546 onClickTerms: PropTypes.func.isRequired, 547 switchToRating: PropTypes.func.isRequired, 548 dappState: PropTypes.instanceOf(DappState).isRequired, 549 showAlert: PropTypes.func.isRequired, 550 } 551 552 export default Submit