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