github.com/resonatecoop/id@v1.1.0-43/frontend/src/components/forms/basic-info.js (about)

     1  const html = require('choo/html')
     2  const Component = require('choo/component')
     3  const nanostate = require('nanostate')
     4  const isEmpty = require('validator/lib/isEmpty')
     5  const isLength = require('validator/lib/isLength')
     6  const isUUID = require('validator/lib/isUUID')
     7  const validateFormdata = require('validate-formdata')
     8  const icon = require('@resonate/icon-element')
     9  const morph = require('nanomorph')
    10  const isEqual = require('is-equal-shallow')
    11  
    12  const input = require('@resonate/input-element')
    13  const textarea = require('@resonate/textarea-element')
    14  const messages = require('./messages')
    15  
    16  const Uploader = require('../image-upload')
    17  
    18  const imagePlaceholder = require('../../lib/image-placeholder')
    19  const inputField = require('../../elements/input-field')
    20  
    21  const UserGroupTypeSwitcher = require('../../components/forms/userGroupTypeSwitcher')
    22  const ProfileSwitcher = require('../../components/forms/profileSwitcher')
    23  
    24  const SwaggerClient = require('swagger-client')
    25  
    26  // ProfileForm class
    27  class ProfileForm extends Component {
    28    constructor (id, state, emit) {
    29      super(id)
    30  
    31      this.emit = emit
    32      this.state = state
    33  
    34      this.local = state.components[id] = Object.create({
    35        machine: nanostate.parallel({
    36          form: nanostate('idle', {
    37            idle: { submit: 'submitted' },
    38            submitted: { valid: 'data', invalid: 'error' },
    39            data: { reset: 'idle', submit: 'submitted' },
    40            error: { reset: 'idle', submit: 'submitted', invalid: 'error' }
    41          }),
    42          request: nanostate('idle', {
    43            idle: { start: 'loading' },
    44            loading: { resolve: 'data', reject: 'error' },
    45            data: { start: 'loading' },
    46            error: { start: 'loading', stop: 'idle' }
    47          }),
    48          machine: nanostate('basicInfo', {
    49            basicInfo: { next: 'recap', end: 'recap' }, // allow adding more later
    50            recap: { prev: 'basicInfo' }
    51          })
    52        })
    53      })
    54  
    55      this.local.machine.on('machine:next', () => {
    56        if (!this.element) return
    57        this.rerender()
    58        window.scrollTo(0, 0)
    59      })
    60  
    61      this.local.machine.on('machine:end', () => {
    62        if (!this.element) return
    63        this.rerender()
    64        window.scrollTo(0, 0)
    65      })
    66  
    67      this.local.machine.on('machine:next', () => {
    68        if (!this.element) return
    69        this.rerender()
    70        window.scrollTo(0, 0)
    71      })
    72  
    73      this.local.machine.on('machine:prev', () => {
    74        if (!this.element) return
    75        this.rerender()
    76        window.scrollTo(0, 0)
    77      })
    78  
    79      this.local.machine.on('form:valid', async () => {
    80        try {
    81          this.local.machine.emit('request:start')
    82  
    83          await this.getClient(this.state.token)
    84  
    85          if (!this.local.usergroup.id) {
    86            const response = await this.swaggerClient.apis.Usergroups.ResonateUser_AddUserGroup({
    87              id: this.state.profile.id,
    88              body: {
    89                displayName: this.local.data.displayName,
    90                description: this.local.data.description,
    91                // groupEmail: this.local.data.groupEmail,
    92                shortBio: this.local.data.shortBio,
    93                address: this.local.data.location,
    94                avatar: this.local.data.avatar, // uuid
    95                banner: this.local.data.banner, // uuid
    96                groupType: 'persona'
    97              }
    98            })
    99  
   100            this.local.usergroup = response.body
   101          } else {
   102            await this.swaggerClient.apis.Usergroups.ResonateUser_UpdateUserGroup({
   103              id: this.local.usergroup.id, // should be usergroup id
   104              body: {
   105                displayName: this.local.data.displayName,
   106                description: this.local.data.description,
   107                address: this.local.data.address,
   108                shortBio: this.local.data.shortBio
   109              }
   110            })
   111          }
   112  
   113          this.local.machine.emit('machine:end')
   114  
   115          this.local.machine.emit('request:resolve')
   116        } catch (err) {
   117          this.local.machine.emit('request:reject')
   118          console.log(err)
   119          this.emit('notify', { message: `ERR${err.response.body.code}: Display name is taken` })
   120        } finally {
   121          this.local.machine.emit('form:reset')
   122        }
   123      })
   124  
   125      this.local.machine.on('form:invalid', () => {
   126        console.log('form is invalid')
   127  
   128        const invalidInput = this.element.querySelector('.invalid')
   129  
   130        if (invalidInput) {
   131          invalidInput.focus({ preventScroll: false }) // focus to first invalid input
   132        }
   133      })
   134  
   135      this.local.machine.on('form:submit', () => {
   136        console.log('form has been submitted')
   137  
   138        const form = this.element.querySelector('form')
   139  
   140        for (const field of form.elements) {
   141          const isRequired = field.required
   142          const name = field.name || ''
   143          const value = field.value || ''
   144  
   145          if (isRequired) {
   146            this.validator.validate(name, value)
   147          }
   148        }
   149  
   150        this.rerender()
   151  
   152        this.local.machine.emit(`form:${this.local.form.valid ? 'valid' : 'invalid'}`)
   153      })
   154  
   155      this.local.data = {}
   156      this.local.usergroup = {}
   157      this.local.profile = {
   158        avatar: {}
   159      }
   160  
   161      this.validator = validateFormdata()
   162      this.local.form = this.validator.state
   163  
   164      this.renderBasicInfoForm = this.renderBasicInfoForm.bind(this)
   165      this.renderRecap = this.renderRecap.bind(this)
   166  
   167      // cached swagger client
   168      this.swaggerClient = null
   169  
   170      this.getClient = this.getClient.bind(this)
   171      this.setUsergroup = this.setUsergroup.bind(this)
   172  
   173      this.local.sticky = false // sticky profile switcher
   174    }
   175  
   176    /**
   177     * Get swagger client
   178     */
   179    async getClient (token) {
   180      if (this.swaggerClient !== null) {
   181        return this.swaggerClient
   182      }
   183  
   184      const specUrl = new URL('/user/user.swagger.json', 'https://' + process.env.API_DOMAIN)
   185  
   186      this.swaggerClient = await new SwaggerClient({
   187        url: specUrl.href,
   188        authorizations: {
   189          bearer: 'Bearer ' + token
   190        }
   191      })
   192  
   193      return this.swaggerClient
   194    }
   195  
   196    /***
   197     * Create basic info form component element
   198     * @returns {HTMLElement}
   199     */
   200    createElement (props = {}) {
   201      this.local.profile = props.profile || {}
   202      this.local.role = props.profile.role
   203  
   204      // initial persona
   205      if (!this.local.usergroup.id) {
   206        if (this.local.profile.usergroups.length) {
   207          this.setUsergroup(this.local.profile.usergroups[0].id)
   208        } else {
   209          this.setUsergroup()
   210        }
   211      }
   212  
   213      const machine = {
   214        basicInfo: this.renderBasicInfoForm, // basic infos for everyone
   215        recap: this.renderRecap // recap
   216      }[this.local.machine.state.machine]
   217  
   218      return html`
   219        <div class="flex flex-column">
   220          ${machine()}
   221        </div>
   222      `
   223    }
   224  
   225    /**
   226     * Set current usergroup
   227     */
   228    setUsergroup (usergroupID) {
   229      const profile = Object.assign({}, this.local.profile)
   230      const avatar = profile.avatar || {}
   231  
   232      if (usergroupID) {
   233        const usergroup = profile.usergroups.find(usergroup => {
   234          if (usergroupID) return usergroup.id === usergroupID
   235          return false
   236        }) || {
   237          // fallback to older profile data for returning members
   238          displayName: profile.nickname,
   239          description: profile.description || '',
   240          avatar: avatar['profile_photo-m'] || avatar['profile_photo-l'] || imagePlaceholder(400, 400)
   241        }
   242  
   243        this.local.groupType = usergroup.groupType
   244        this.local.data.banner = usergroup.banner
   245        this.local.data.avatar = usergroup.avatar
   246        this.local.data.address = usergroup.address
   247        this.local.data.shortBio = usergroup.shortBio
   248  
   249        this.local.data.description = usergroup.description
   250        this.local.data.displayName = usergroup.displayName
   251  
   252        this.local.usergroup = usergroup
   253      } else {
   254        this.local.usergroup = {}
   255        this.local.data = {}
   256        this.local.form.values.displayName = ''
   257        this.local.form.values.description = ''
   258        this.local.form.values.shortBio = ''
   259      }
   260    }
   261  
   262    /**
   263     * Rerender only base form element
   264     */
   265    rerender () {
   266      const machine = {
   267        basicInfo: this.renderBasicInfoForm, // basic infos for everyone
   268        recap: this.renderRecap // recap
   269      }[this.local.machine.state.machine]
   270  
   271      morph(this.element.querySelector('.base-form'), machine())
   272    }
   273  
   274    /**
   275     * Basic info form
   276     */
   277    renderBasicInfoForm () {
   278      // form elements
   279      const elements = {
   280        /**
   281         * Display name, artist name, nickname for user
   282         * @param {Object} validator Form data validator
   283         * @param {Object} form Form data object
   284         */
   285        displayName: (validator, form) => {
   286          const { values, pristine, errors } = form
   287  
   288          const el = input({
   289            type: 'text',
   290            name: 'displayName',
   291            invalid: errors.displayName && !pristine.displayName,
   292            value: values.displayName,
   293            onchange: async (e) => {
   294              validator.validate(e.target.name, e.target.value)
   295              this.local.data.displayName = e.target.value
   296              this.local.usergroup.displayName = this.local.data.displayName
   297              this.rerender()
   298  
   299              if (!this.local.usergroup.id) return
   300  
   301              try {
   302                await this.getClient(this.state.token)
   303  
   304                await this.swaggerClient.apis.Usergroups.ResonateUser_UpdateUserGroup({
   305                  id: this.local.usergroup.id, // should be usergroup id
   306                  body: {
   307                    displayName: this.local.data.displayName
   308                  }
   309                })
   310  
   311                this.emit('notify', { message: 'Display name saved' })
   312              } catch (err) {
   313                console.log(err)
   314                this.emit('notify', { message: 'Failed saving display name' })
   315              }
   316            }
   317          })
   318  
   319          const helpText = this.local.role && this.local.role !== 'user'
   320            ? `Your ${this.local.role} name`
   321            : 'Your username'
   322  
   323          const labelOpts = {
   324            labelText: 'Name',
   325            inputName: 'displayName',
   326            helpText: helpText,
   327            displayErrors: true
   328          }
   329  
   330          return inputField(el, form)(labelOpts)
   331        },
   332        /**
   333         * Secondary email
   334         */
   335        /*
   336        groupEmail: (validator, form) => {
   337          const { values, pristine, errors } = form
   338  
   339          const el = input({
   340            type: 'email',
   341            name: 'groupEmail',
   342            required: false,
   343            invalid: errors.groupEmail && !pristine.groupEmail,
   344            value: values.groupEmail,
   345            onchange: async (e) => {
   346              validator.validate(e.target.name, e.target.value)
   347              this.local.data.groupEmail = e.target.value
   348              this.rerender()
   349  
   350              if (!this.local.usergroup.id) return
   351  
   352              try {
   353                await this.getClient(this.state.token)
   354  
   355                await this.swaggerClient.apis.Usergroups.ResonateUser_UpdateUserGroup({
   356                  id: this.local.usergroup.id, // should be usergroup id
   357                  body: {
   358                    groupEmail: this.local.data.groupEmail
   359                  }
   360                })
   361  
   362                this.emit('notify', { message: 'Secondary email saved' })
   363              } catch (err) {
   364                console.log(err)
   365                this.emit('notify', { message: 'Failed saving secondary email' })
   366              }
   367            }
   368          })
   369  
   370          const helpText = 'A secondary email address for your profile'
   371  
   372          const labelOpts = {
   373            labelText: 'E-mail',
   374            inputName: 'groupEmail',
   375            helpText: helpText,
   376            displayErrors: true
   377          }
   378  
   379          return inputField(el, form)(labelOpts)
   380        },
   381        */
   382        /**
   383         * Description/bio for user
   384         * @param {Object} validator Form data validator
   385         * @param {Object} form Form data object
   386         */
   387        description: (validator, form) => {
   388          const { values, pristine, errors } = form
   389  
   390          return html`
   391            <div class="mb5">
   392              <div class="mb1">
   393                ${textarea({
   394                  name: 'description',
   395                  maxlength: 2000,
   396                  invalid: errors.description && !pristine.description,
   397                  placeholder: 'Bio',
   398                  required: false,
   399                  text: values.description,
   400                  onchange: async (e) => {
   401                    validator.validate(e.target.name, e.target.value)
   402                    this.local.data.description = e.target.value
   403                    this.rerender()
   404  
   405                    if (!this.local.usergroup.id) return
   406  
   407                    try {
   408                      await this.getClient(this.state.token)
   409  
   410                      await this.swaggerClient.apis.Usergroups.ResonateUser_UpdateUserGroup({
   411                        id: this.local.usergroup.id, // should be usergroup id
   412                        body: {
   413                          description: this.local.data.description
   414                        }
   415                      })
   416  
   417                      this.emit('notify', { message: 'Description saved' })
   418                    } catch (err) {
   419                      console.log(err)
   420                      this.emit('notify', { message: 'Failed saving description' })
   421                    }
   422                  }
   423                })}
   424              </div>
   425              <p class="ma0 pa0 message warning">${errors.description && !pristine.description ? errors.description.message : ''}</p>
   426              <p class="ma0 pa0 f5 dark-gray">${values.description ? 2000 - values.description.length : 2000} characters remaining</p>
   427            </div>
   428          `
   429        },
   430        /**
   431         * Short bio
   432         * @param {Object} validator Form data validator
   433         * @param {Object} form Form data object
   434         */
   435        shortBio: (validator, form) => {
   436          const { values, pristine, errors } = form
   437  
   438          return html`
   439            <div class="mb5">
   440              <div class="mb1">
   441                ${textarea({
   442                  name: 'shortBio',
   443                  maxlength: 100,
   444                  invalid: errors.shortBio && !pristine.shortBio,
   445                  placeholder: 'Short bio',
   446                  required: false,
   447                  text: values.shortBio,
   448                  onchange: async (e) => {
   449                    validator.validate(e.target.name, e.target.value)
   450                    this.local.data.shortBio = e.target.value
   451                    this.rerender()
   452  
   453                    if (!this.local.usergroup.id) return
   454  
   455                    try {
   456                      await this.getClient(this.state.token)
   457  
   458                      await this.swaggerClient.apis.Usergroups.ResonateUser_UpdateUserGroup({
   459                        id: this.local.usergroup.id, // should be usergroup id
   460                        body: {
   461                          shortBio: this.local.data.shortBio
   462                        }
   463                      })
   464                      this.emit('notify', { message: 'Short bio saved' })
   465                    } catch (err) {
   466                      console.log(err)
   467                      this.emit('notify', { message: 'Failed saving short bio' })
   468                    }
   469                  }
   470                })}
   471              </div>
   472              <p class="ma0 pa0 message warning">${errors.shortBio && !pristine.shortBio ? errors.shortBio.message : ''}</p>
   473              <p class="ma0 pa0 f5 dark-gray">${values.shortBio ? 100 - values.shortBio.length : 100} characters remaining</p>
   474            </div>
   475          `
   476        },
   477        /**
   478         * Upload user profile image
   479         * @param {Object} validator Form data validator
   480         * @param {Object} form Form data object
   481         */
   482        profilePicture: (validator, form) => {
   483          const component = this.state.cache(Uploader, this._name + '-profile-picture')
   484          const el = component.render({
   485            name: 'profilePicture',
   486            form: form,
   487            config: 'avatar',
   488            required: false,
   489            validator: validator,
   490            format: { width: 300, height: 300 }, // minimum accepted format values
   491            src: `https://${process.env.STATIC_HOSTNAME}/images/${this.local.usergroup.avatar}-x600.jpg`,
   492            accept: 'image/jpeg,image/jpg,image/png',
   493            ratio: '1600x1600px',
   494            archive: this.state.profile.avatar['profile_photo-m'] || this.state.profile.avatar['profile_photo-l'], // last uploaded files, old wp cover photo...
   495            onFileUploaded: async (filename) => {
   496              this.local.data.avatar = filename
   497  
   498              if (!this.local.usergroup.id) return
   499  
   500              try {
   501                await this.getClient(this.state.token)
   502  
   503                await this.swaggerClient.apis.Usergroups.ResonateUser_UpdateUserGroup({
   504                  id: this.local.usergroup.id, // should be usergroup id
   505                  body: {
   506                    avatar: this.local.data.avatar
   507                  }
   508                })
   509  
   510                this.emit('notify', { message: 'Profile picture updated', type: 'success' })
   511              } catch (err) {
   512                console.log(err)
   513                this.emit('notify', { message: 'Profile picture failed to update', type: 'success' })
   514              }
   515            }
   516          })
   517  
   518          const labelOpts = {
   519            labelText: 'Profile picture',
   520            labelPrefix: 'f4 fw1 db mb2',
   521            columnReverse: true,
   522            inputName: 'profile-picture',
   523            displayErrors: true
   524          }
   525  
   526          return inputField(el, form)(labelOpts)
   527        },
   528        /**
   529         * Upload user header image
   530         * @param {Object} validator Form data validator
   531         * @param {Object} form Form data object
   532         */
   533        headerImage: (validator, form) => {
   534          const component = this.state.cache(Uploader, this._name + '-header-image')
   535          const el = component.render({
   536            name: 'headerImage',
   537            form: form,
   538            config: 'banner',
   539            required: false,
   540            validator: validator,
   541            src: `https://${process.env.STATIC_HOSTNAME}/images/${this.local.usergroup.banner}-x625.jpg`,
   542            format: { width: 625, height: 125 },
   543            accept: 'image/jpeg,image/jpg,image/png',
   544            ratio: '2500x500px',
   545            direction: 'column',
   546            archive: this.state.profile.avatar['cover_photo-m'], // last uploaded files, old wp cover photo...
   547            onFileUploaded: async (filename) => {
   548              this.local.data.banner = filename
   549  
   550              if (!this.local.usergroup.id) return
   551  
   552              try {
   553                // TODO upload tool should update usergroup once file has been processed
   554                // or we should check file status until the file is good?
   555                await this.getClient(this.state.token)
   556  
   557                await this.swaggerClient.apis.Usergroups.ResonateUser_UpdateUserGroup({
   558                  id: this.local.usergroup.id, // should be usergroup id
   559                  body: {
   560                    banner: this.local.data.banner
   561                  }
   562                })
   563  
   564                this.emit('notify', { message: 'Profile picture updated', type: 'success' })
   565              } catch (err) {
   566                console.log(err)
   567              }
   568            }
   569          })
   570  
   571          const labelOpts = {
   572            labelText: 'Header image',
   573            labelPrefix: 'f4 fw1 db mb2',
   574            columnReverse: true,
   575            inputName: 'header-image',
   576            displayErrors: true
   577          }
   578  
   579          return inputField(el, form)(labelOpts)
   580        }//,
   581        /**
   582         * Address for user (could be a place, city, anywhere, should enable this later, not supported by user-api yet)
   583         * @param {Object} validator Form data validator
   584         * @param {Object} form Form data object
   585         */
   586        /*
   587        address: (validator, form) => {
   588          const { values, pristine, errors } = form
   589  
   590          const el = input({
   591            type: 'text',
   592            name: 'address',
   593            invalid: errors.address && !pristine.address,
   594            placeholder: 'City',
   595            required: false,
   596            value: values.address,
   597            onchange: async (e) => {
   598              validator.validate(e.target.name, e.target.value)
   599              this.local.data.address = e.target.value
   600              this.rerender()
   601  
   602              if (!this.local.usergroup.id) return
   603  
   604              try {
   605                await this.getClient(this.state.token)
   606  
   607                await this.swaggerClient.apis.Usergroups.ResonateUser_UpdateUserGroup({
   608                  id: this.local.usergroup.id, // should be usergroup id
   609                  body: {
   610                    address: this.local.data.address
   611                  }
   612                })
   613  
   614                this.emit('notify', { message: 'Location updated', type: 'success' })
   615              } catch (err) {
   616                console.log(err)
   617              }
   618            }
   619          })
   620  
   621          const labelOpts = {
   622            labelText: 'Location',
   623            inputName: 'location'
   624          }
   625  
   626          return inputField(el, form)(labelOpts)
   627        },
   628        */
   629        /**
   630         * Links for usergroup (enable this later, not supported by user-api yet)
   631         * @param {Object} validator Form data validator
   632         * @param {Object} form Form data object
   633         */
   634        /*
   635        links: (validator, form) => {
   636          const { values } = form
   637          const component = this.state.cache(Links, 'links-input')
   638  
   639          const el = component.render({
   640            form: form,
   641            validator: validator,
   642            value: values.links
   643          })
   644  
   645          const labelOpts = {
   646            labelText: 'Links',
   647            inputName: 'links'
   648          }
   649  
   650          return inputField(el, form)(labelOpts)
   651        },
   652        /**
   653         * Tags for usergroup (enable this later, not supported by user-api yet)
   654         * @param {Object} validator Form data validator
   655         * @param {Object} form Form data object
   656         */
   657        /*
   658        tags: (validator, form) => {
   659          const { values } = form
   660          const component = this.state.cache(Tags, 'tags-input')
   661  
   662          const el = component.render({
   663            form: form,
   664            validator: validator,
   665            value: values.tags,
   666            items: ['test']
   667          })
   668  
   669          const labelOpts = {
   670            labelText: 'Links',
   671            inputName: 'links'
   672          }
   673  
   674          return inputField(el, form)(labelOpts)
   675        }
   676        */
   677      }
   678  
   679      const role = {
   680        user: 'Listener',
   681        artist: 'Artist',
   682        label: 'Label'
   683      }[this.local.role]
   684  
   685      // an artist, a label
   686      const article = {
   687        artist: 'an',
   688        label: 'a'
   689      }[this.local.role]
   690  
   691      const title = html`${!this.local.usergroup.id ? 'Create' : 'Update'} ${!this.local.usergroup.id
   692        ? `${article || 'your'} ${this.local.role ? `${role} ` : ''}`
   693          : html`<span class="i">${this.local.usergroup.displayName}</span>`} profile`
   694  
   695      return this.renderForm(title, elements)
   696    }
   697  
   698    /*
   699     * All done with setting up account profile
   700     */
   701    renderRecap () {
   702      return html`
   703        <div class="base-form flex flex-column">
   704          <div class="flex flex-auto flex-column center mw6 w-auto-l ph3">
   705            <h2 class="lh-title fw1 f2">Thank you for completing your profile!</h2>
   706  
   707            <p>
   708              <a style="outline:solid 1px var(--near-black);outline-offset:-1px" class="link bg-white near-black b pv3 ph5 flex-shrink-0 f5" href="${process.env.APP_HOST}/api/v3/user/connect/resonate">Listen</a>
   709            </p>
   710          </div>
   711        </div>
   712      `
   713    }
   714  
   715    renderProfileSwitcher () {
   716      if (!this.local.role || this.local.role === 'user' || this.local.machine.state.machine !== 'basicInfo') return
   717  
   718      return this.state.cache(ProfileSwitcher, 'profile-switcher').render({
   719        value: this.local.usergroup.id, // currently selected usergroup/persona
   720        usergroups: this.local.profile.usergroups,
   721        onChangeCallback: (usergroupId) => {
   722          this.setUsergroup(usergroupId)
   723          this.rerender()
   724        }
   725      })
   726    }
   727  
   728    /**
   729     * Dev only for role switching
   730     */
   731    renderRoleSwitcher () {
   732      if (process.env.NODE_ENV !== 'development') return
   733  
   734      // groupe type assign
   735      const groupType = {
   736        user: 'persona', // listener
   737        artist: 'persona',
   738        label: 'label'
   739      }[this.local.role]
   740  
   741      return this.state.cache(UserGroupTypeSwitcher, 'usergroup-type-switcher').render({
   742        value: this.local.usergroup.groupType || groupType,
   743        onChangeCallback: async (groupType) => {
   744          if (!this.local.usergroup.id) return
   745  
   746          this.local.usergroup.groupType = groupType
   747          this.local.profile.usergroups = this.local.profile.usergroups.map((usergroup) => {
   748            if (usergroup.id === this.local.usergroup.id) {
   749              usergroup.groupType = groupType
   750            }
   751            return usergroup
   752          })
   753  
   754          try {
   755            await this.getClient(this.state.token)
   756  
   757            await this.swaggerClient.apis.Usergroups.ResonateUser_UpdateUserGroup({
   758              id: this.local.usergroup.id, // should be usergroup id
   759              body: {
   760                groupType: groupType
   761              }
   762            })
   763  
   764            this.emit('notify', { message: `Usergroup type changed to: ${groupType}` })
   765          } catch (err) {
   766            console.log(err)
   767            this.emit('notify', { message: 'Failed setting group type' })
   768          }
   769        }
   770      })
   771    }
   772  
   773    /*
   774     * Render form
   775     */
   776    renderForm (title, elements) {
   777      // find first available persona or fallback to available legacy profile
   778      const values = this.local.form.values
   779  
   780      for (const [key, value] of Object.entries(this.local.data)) {
   781        values[key] = value
   782      }
   783  
   784      // form attrs
   785      const attrs = {
   786        novalidate: 'novalidate',
   787        onsubmit: this.handleSubmit.bind(this)
   788      }
   789  
   790      const submitButton = () => {
   791        // button attrs
   792        const attrs = {
   793          type: 'submit',
   794          class: 'bg-white near-black dib bn b pv3 ph5 flex-shrink-0 f5 grow',
   795          style: 'outline:solid 1px var(--near-black);outline-offset:-1px'
   796        }
   797        return html`
   798          <button ${attrs}>
   799            ${this.local.form.changed ? !this.local.usergroup.id ? 'Create' : 'Update' : 'Continue'}
   800          </button>
   801        `
   802      }
   803  
   804      return html`
   805        <div class="base-form flex flex-column">
   806          <div class=${this.local.sticky ? 'sticky z-2' : ''} style=${this.local.sticky ? 'top:3rem' : ''}>
   807            ${this.renderProfileSwitcher.bind(this)()}
   808          </div>
   809          <div class="flex flex-auto flex-column center mw6 w-auto-l ph3">
   810            ${messages(this.state, this.local.form)}
   811            <div class="relative flex items-center">
   812              <h2 class="lh-title f3 fw1">
   813                ${title}
   814              </h2>
   815              ${this.renderBackButton.bind(this)()}
   816            </div>
   817            <div>
   818              ${this.renderRoleSwitcher.bind(this)()}
   819            </div>
   820            <form ${attrs}>
   821              ${Object.entries(elements)
   822                .map(([name, el]) => {
   823                  // possibility to filter by name
   824                  return el(this.validator, this.local.form)
   825                })}
   826  
   827              ${submitButton()}
   828            </form>
   829            <div class="relative flex items-center">
   830              <h4>
   831                <br/>
   832                <a href="https://forms.gle/VZok9gA1FDzznewW9">
   833                  New Release Submission Form
   834                </a>
   835              </h4>
   836            </div>
   837          </div>
   838        </div>
   839      `
   840    }
   841  
   842    renderBackButton () {
   843      if (this.local.machine.state.machine === 'basicInfo') return
   844  
   845      const attrs = {
   846        class: 'bg-white dib bn b flex-shrink-0 grow absolute',
   847        style: 'top: 50%;left:-1rem;transform: translate3d(-100%, -50%, 0)',
   848        onclick: (e) => {
   849          e.preventDefault()
   850  
   851          this.local.machine.emit('machine:prev')
   852        }
   853      }
   854  
   855      return html`
   856        <button ${attrs}>
   857          ${icon('arrow')}
   858        </button>
   859      `
   860    }
   861  
   862    /**
   863     * Basic info form submit handler
   864     */
   865    handleSubmit (e) {
   866      e.preventDefault()
   867  
   868      if (!this.local.form.changed) {
   869        if (this.local.usergroup.groupType === 'label') {
   870          return this.local.machine.emit('machine:next')
   871        }
   872        return this.local.machine.emit('machine:end')
   873      }
   874  
   875      this.local.machine.emit('form:submit')
   876    }
   877  
   878    /**
   879     * Basic info load handler
   880     * @param {HTMLElement} el THe basic info form element
   881     */
   882    load (el) {
   883      this.validator.field('displayName', (data) => {
   884        if (isEmpty(data)) return new Error('Display name is required')
   885        if (!isLength(data, { min: 1, max: 100 })) return new Error('Name should be no more than 100 characters')
   886      })
   887      /*
   888      this.validator.field('groupEmail', { required: false }, (data) => {
   889        if (!isEmail(data)) return new Error('Email is invalid')
   890      })
   891      */
   892      this.validator.field('description', { required: false }, (data) => {
   893        if (!isLength(data, { min: 0, max: 2000 })) return new Error('Description should be no more than 2000 characters')
   894      })
   895      this.validator.field('shortBio', { required: false }, (data) => {
   896        if (!isLength(data, { min: 0, max: 100 })) return new Error('Short bio should be no more than 100 characters')
   897      })
   898      /*
   899      this.validator.field('address', { required: false }, (data) => {
   900        if (!isLength(data, { min: 0, max: 100 })) return new Error('Location should be no more than 100 characters')
   901      })
   902      */
   903      this.validator.field('profilePicture', { required: false }, (data) => {
   904        if (!isEmpty(data) && !isUUID(data, 4)) return new Error('Profile picture ref is invalid')
   905      })
   906      this.validator.field('headerImage', { required: false }, (data) => {
   907        if (!isEmpty(data) && !isUUID(data, 4)) return new Error('Header image ref is invalid')
   908      })
   909    }
   910  
   911    /**
   912     * Basic info form update handler
   913     * @returns {Boolean}
   914     */
   915    update (props) {
   916      return !isEqual(props.profile, this.local.profile)
   917    }
   918  }
   919  
   920  module.exports = ProfileForm