/ src / modules / Submit / Submit.jsx
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