/ 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 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