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