-
Notifications
You must be signed in to change notification settings - Fork 153
feat: add new step one for personal details [Onboarding-Form] #1083
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d0adec7
6c6687f
5ebca46
c9fe6f6
3cc8996
77f0031
aff01fa
e0e1d3b
a90a012
e5edda0
2a697ea
d656d03
1b8f477
9e622c5
677dc27
8b271fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| import { action } from '@ember/object'; | ||
| import { debounce } from '@ember/runloop'; | ||
| import Component from '@glimmer/component'; | ||
| import { tracked } from '@glimmer/tracking'; | ||
| import { JOIN_DEBOUNCE_TIME } from '../../constants/join'; | ||
| import { validateWordCount } from '../../utils/validator'; | ||
| import { scheduleOnce } from '@ember/runloop'; | ||
| import { getLocalStorageItem, setLocalStorageItem } from '../../utils/storage'; | ||
|
|
||
| export default class BaseStepComponent extends Component { | ||
| stepValidation = {}; | ||
|
|
||
| @tracked data = {}; | ||
| @tracked errorMessage = {}; | ||
| @tracked wordCount = {}; | ||
|
|
||
| get storageKey() { | ||
| return ''; | ||
| } | ||
|
|
||
| postLoadInitialize() {} | ||
|
|
||
| constructor(...args) { | ||
| super(...args); | ||
| scheduleOnce('afterRender', this, this.initializeFormState); | ||
| } | ||
|
|
||
| initializeFormState() { | ||
| let saved = {}; | ||
| try { | ||
| const stored = getLocalStorageItem(this.storageKey, '{}'); | ||
| saved = stored ? JSON.parse(stored) : {}; | ||
| } catch (e) { | ||
| console.warn('Failed to parse stored form data:', e); | ||
| saved = {}; | ||
| } | ||
| this.data = saved; | ||
|
|
||
| this.errorMessage = Object.fromEntries( | ||
| Object.keys(this.stepValidation).map((k) => [k, '']), | ||
| ); | ||
|
|
||
| this.wordCount = Object.fromEntries( | ||
| Object.keys(this.stepValidation).map((k) => { | ||
| let val = String(this.data[k] || ''); | ||
| return [k, val.trim().split(/\s+/).filter(Boolean).length || 0]; | ||
| }), | ||
| ); | ||
|
|
||
| this.postLoadInitialize(); | ||
|
|
||
| const valid = this.isDataValid(); | ||
| this.args.setIsPreValid(valid); | ||
| setLocalStorageItem('isValid', String(valid)); | ||
MayankBansal12 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| @action inputHandler(e) { | ||
| if (!e?.target) return; | ||
| this.args.setIsPreValid(false); | ||
MayankBansal12 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const field = e.target.name; | ||
| const value = e.target.value; | ||
| debounce(this, this.handleFieldUpdate, field, value, JOIN_DEBOUNCE_TIME); | ||
| } | ||
|
|
||
| validateField(field, value) { | ||
MayankBansal12 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const limits = this.stepValidation[field]; | ||
| const fieldType = limits?.type || 'text'; | ||
|
|
||
| if (fieldType === 'select' || fieldType === 'dropdown') { | ||
| const hasValue = value && String(value).trim().length > 0; | ||
| return { isValid: hasValue }; | ||
| } | ||
| return validateWordCount(value, limits); | ||
| } | ||
|
|
||
| isDataValid() { | ||
| for (const field of Object.keys(this.stepValidation)) { | ||
| const result = this.validateField(field, this.data[field]); | ||
| if (!result.isValid) return false; | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| handleFieldUpdate(field, value) { | ||
| this.updateFieldValue(field, value); | ||
| const result = this.validateField(field, value); | ||
| this.updateWordCount(field, result); | ||
| this.updateErrorMessage(field, result); | ||
| this.syncFormValidity(); | ||
| } | ||
|
|
||
| updateFieldValue(field, value) { | ||
| this.data = { ...this.data, [field]: value }; | ||
| setLocalStorageItem(this.storageKey, JSON.stringify(this.data)); | ||
| } | ||
|
|
||
| updateWordCount(field, result) { | ||
| const wordCount = result.wordCount ?? 0; | ||
| this.wordCount = { ...this.wordCount, [field]: wordCount }; | ||
| } | ||
|
|
||
| updateErrorMessage(field, result) { | ||
| this.errorMessage = { | ||
| ...this.errorMessage, | ||
| [field]: this.formatError(field, result), | ||
| }; | ||
| } | ||
|
|
||
| formatError(field, result) { | ||
| const limits = this.stepValidation[field]; | ||
| if (result.isValid) return ''; | ||
|
|
||
| const fieldType = limits?.type || 'text'; | ||
| if (fieldType === 'select' || fieldType === 'dropdown') { | ||
| return 'Please choose an option'; | ||
| } | ||
| if (result.remainingToMin) { | ||
| return `At least ${result.remainingToMin} more word(s) required`; | ||
| } | ||
| return `Maximum ${limits?.max ?? 'N/A'} words allowed`; | ||
| } | ||
|
|
||
| syncFormValidity() { | ||
| const allValid = this.isDataValid(); | ||
| this.args.setIsValid(allValid); | ||
| setLocalStorageItem('isValid', String(allValid)); | ||
MayankBansal12 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| <div class="step-container"> | ||
| <div class="form-header__text"> | ||
| <h1 class="section-heading">{{@heading}}</h1> | ||
| <p class="section-instruction">{{@subHeading}}</p> | ||
| </div> | ||
|
|
||
| <Reusables::TextAreaBox @field='Why you want to join Real Dev Squad?' @name='whyRds' | ||
| @placeHolder='Tell us why you want to join our community...' @required={{true}} @value={{this.data.whyRds}} | ||
| @onInput={{this.inputHandler}} /> | ||
| {{#if this.errorMessage.whyRds}} | ||
| <div class='error__message'>{{this.errorMessage.whyRds}}</div> | ||
| {{/if}} | ||
|
|
||
| <div class="form-grid form-grid--2"> | ||
| <div class="form-grid__item"> | ||
| <Reusables::InputBox @field='No of hours/week you are willing to contribute?' @name='numberOfHours' | ||
| @placeHolder='Enter value between 1-100.' @type='number' @required={{true}} @value={{this.data.numberOfHours}} | ||
| @onInput={{this.inputHandler}} @options={{this.heardFrom}} /> | ||
| {{#if this.errorMessage.numberOfHours}} | ||
| <div class='error__message'>{{this.errorMessage.numberOfHours}}</div> | ||
| {{/if}} | ||
| </div> | ||
|
|
||
| <div class="form-grid__item"> | ||
| <Reusables::Dropdown @field='How did you hear about us?' @name='foundFrom' | ||
| @placeHolder='Choose from below options' @required={{true}} @value={{this.data.foundFrom}} | ||
| @options={{this.heardFrom}} @onChange={{this.inputHandler}} /> | ||
| {{#if this.errorMessage.foundFrom}} | ||
| <div class='error__message'>{{this.errorMessage.foundFrom}}</div> | ||
| {{/if}} | ||
| </div> | ||
| </div> | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import BaseStepComponent from './base-step'; | ||
| import { | ||
| NEW_STEP_LIMITS, | ||
| STEP_DATA_STORAGE_KEY, | ||
| } from '../../constants/new-join-form'; | ||
| import { heardFrom } from '../../constants/social-data'; | ||
|
|
||
| export default class NewStepFiveComponent extends BaseStepComponent { | ||
| storageKey = STEP_DATA_STORAGE_KEY.stepFive; | ||
| heardFrom = heardFrom; | ||
|
|
||
| stepValidation = { | ||
| whyRds: NEW_STEP_LIMITS.stepFive.whyRds, | ||
| foundFrom: NEW_STEP_LIMITS.stepFive.foundFrom, | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| <div class="step-container"> | ||
| <div class="form-header__text"> | ||
| <h1 class="section-heading">{{@heading}}</h1> | ||
| <p class="section-instruction">{{@subHeading}}</p> | ||
| </div> | ||
|
|
||
| <Reusables::InputBox @field='Phone Number' @name='phoneNumber' @placeHolder='+91 80000 00000' @type='tel' | ||
| @required={{true}} @value={{this.data.phoneNumber}} @onInput={{this.inputHandler}} @variant='input--full-width' /> | ||
| {{#if this.errorMessage.phoneNumber}} | ||
| <div class='error__message'>{{this.errorMessage.phoneNumber}}</div> | ||
| {{/if}} | ||
|
|
||
| <Reusables::InputBox @field='Twitter' @name='twitter' @placeHolder='https://twitter.com/gangster-rishi' @type='text' | ||
| @required={{true}} @value={{this.data.twitter}} @onInput={{this.inputHandler}} @variant='input--full-width' /> | ||
| {{#if this.errorMessage.twitter}} | ||
| <div class='error__message'>{{this.errorMessage.twitter}}</div> | ||
| {{/if}} | ||
|
|
||
| {{#if this.showGitHub}} | ||
| <Reusables::InputBox @field='GitHub' @name='github' @placeHolder='https://github.com/codewithrishi' @type='text' | ||
| @required={{true}} @value={{this.data.github}} @onInput={{this.inputHandler}} @variant='input--full-width' /> | ||
| {{#if this.errorMessage.github}} | ||
| <div class='error__message'>{{this.errorMessage.github}}</div> | ||
| {{/if}} | ||
| {{/if}} | ||
|
|
||
| <Reusables::InputBox @field='LinkedIn' @name='linkedin' @placeHolder='https://linkedin.com/in/professional-rishi' | ||
| @type='text' @required={{true}} @value={{this.data.linkedin}} @onInput={{this.inputHandler}} | ||
| @variant='input--full-width' /> | ||
| {{#if this.errorMessage.linkedin}} | ||
| <div class='error__message'>{{this.errorMessage.linkedin}}</div> | ||
| {{/if}} | ||
|
|
||
| <Reusables::InputBox @field='Instagram' @name='instagram' @placeHolder='https://instagram.com/gangster-rishi' | ||
| @type='text' @required={{false}} @value={{this.data.instagram}} @onInput={{this.inputHandler}} | ||
| @variant='input--full-width' /> | ||
| {{#if this.errorMessage.instagram}} | ||
| <div class='error__message'>{{this.errorMessage.instagram}}</div> | ||
| {{/if}} | ||
|
|
||
| <Reusables::InputBox @field='Peerlist' @name='peerlist' @placeHolder='https://peerlist.io/richy-rishi' @type='text' | ||
| @required={{true}} @value={{this.data.peerlist}} @onInput={{this.inputHandler}} @variant='input--full-width' /> | ||
| {{#if this.errorMessage.peerlist}} | ||
| <div class='error__message'>{{this.errorMessage.peerlist}}</div> | ||
| {{/if}} | ||
|
|
||
| {{#if this.showBehance}} | ||
| <Reusables::InputBox @field='Behance' @name='behance' @placeHolder='https://behance.net/designer-rishi' @type='text' | ||
| @required={{true}} @value={{this.data.behance}} @onInput={{this.inputHandler}} @variant='input--full-width' /> | ||
| {{#if this.errorMessage.behance}} | ||
| <div class='error__message'>{{this.errorMessage.behance}}</div> | ||
| {{/if}} | ||
| {{/if}} | ||
|
|
||
| {{#if this.showDribble}} | ||
| <Reusables::InputBox @field='Dribble' @name='dribble' @placeHolder='https://dribbble.com/dribwithrishi' @type='text' | ||
| @required={{true}} @value={{this.data.dribble}} @onInput={{this.inputHandler}} @variant='input--full-width' /> | ||
| {{#if this.errorMessage.dribble}} | ||
| <div class='error__message'>{{this.errorMessage.dribble}}</div> | ||
| {{/if}} | ||
| {{/if}} | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import BaseStepComponent from './base-step'; | ||
| import { | ||
| NEW_STEP_LIMITS, | ||
| STEP_DATA_STORAGE_KEY, | ||
| } from '../../constants/new-join-form'; | ||
| import { phoneNumberRegex } from '../../constants/regex'; | ||
|
|
||
| export default class NewStepFourComponent extends BaseStepComponent { | ||
| storageKey = STEP_DATA_STORAGE_KEY.stepFour; | ||
|
|
||
| stepValidation = { | ||
| phoneNumber: NEW_STEP_LIMITS.stepFour.phoneNumber, | ||
| twitter: NEW_STEP_LIMITS.stepFour.twitter, | ||
| linkedin: NEW_STEP_LIMITS.stepFour.linkedin, | ||
| instagram: NEW_STEP_LIMITS.stepFour.instagram, | ||
| peerlist: NEW_STEP_LIMITS.stepFour.peerlist, | ||
| }; | ||
|
|
||
| get userRole() { | ||
| const stepOneData = JSON.parse( | ||
| localStorage.getItem('newStepOneData') || '{}', | ||
| ); | ||
| return stepOneData.role || ''; | ||
| } | ||
|
|
||
| postLoadInitialize() { | ||
| if (this.userRole === 'Developer') { | ||
| this.stepValidation.github = NEW_STEP_LIMITS.stepFour.github; | ||
| } | ||
|
|
||
| if (this.userRole === 'Designer') { | ||
| this.stepValidation.behance = NEW_STEP_LIMITS.stepFour.behance; | ||
| this.stepValidation.dribble = NEW_STEP_LIMITS.stepFour.dribble; | ||
| } | ||
|
|
||
| // re-calculate the errorMessage and wordCount for new input fields | ||
| this.errorMessage = Object.fromEntries( | ||
| Object.keys(this.stepValidation).map((k) => [k, '']), | ||
| ); | ||
|
|
||
| this.wordCount = Object.fromEntries( | ||
| Object.keys(this.stepValidation).map((k) => { | ||
| let val = this.data[k] || ''; | ||
| return [k, val.trim().split(/\s+/).filter(Boolean).length || 0]; | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| get showGitHub() { | ||
| return this.userRole === 'Developer'; | ||
| } | ||
|
|
||
| get showBehance() { | ||
| return this.userRole === 'Designer'; | ||
| } | ||
|
|
||
| get showDribble() { | ||
| return this.userRole === 'Designer'; | ||
| } | ||
|
|
||
| validateField(field, value) { | ||
| if (field === 'phoneNumber') { | ||
| const trimmedValue = value?.trim() || ''; | ||
| const isValid = trimmedValue && phoneNumberRegex.test(trimmedValue); | ||
| return { | ||
| isValid, | ||
| wordCount: 0, | ||
| }; | ||
| } | ||
| return super.validateField(field, value); | ||
| } | ||
|
|
||
| formatError(field, result) { | ||
| if (field === 'phoneNumber') { | ||
| if (result.isValid) return ''; | ||
| return 'Please enter a valid phone number (e.g., +91 80000 00000)'; | ||
| } | ||
| return super.formatError(field, result); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| <div class="two-column-layout"> | ||
| <div class="two-column-layout__left"> | ||
| <h3 class="section-heading">Profile Picture</h3> | ||
| {{#if this.isImageUploading}} | ||
| <div class="image-preview-container"> | ||
| <Spinner /> | ||
| <p>Processing image...</p> | ||
| </div> | ||
| {{else}} | ||
| {{#if this.imagePreview}} | ||
| <div class="image-preview-container"> | ||
| <img src={{this.imagePreview}} alt="Profile preview" class="image-preview" /> | ||
| <Reusables::Button @type="button" @text="Change Image" @variant="light" @onClick={{this.triggerFileInput}} /> | ||
| </div> | ||
| {{else}} | ||
| <button class="image-upload-box" type="button" {{on 'click' this.triggerFileInput}}> | ||
| <FaIcon @icon="upload" /> | ||
| <p class="image-upload-box__text">Upload Image</p> | ||
| </button> | ||
| {{/if}} | ||
| {{/if}} | ||
| <input type="file" accept="image/png,image/jpeg" class="image-form__input" hidden aria-label="Upload profile image" | ||
| {{did-insert this.setFileInputElement}} {{will-destroy this.clearFileInputElement}} {{on 'change' this.handleImageSelect}} /> | ||
| <div class="image-requirements"> | ||
| <h4>Image Requirements:</h4> | ||
| <ul> | ||
| <li>Must be a real, clear photograph (no anime, filters, or drawings)</li> | ||
| <li>Must contain exactly one face</li> | ||
| <li>Face must cover at least 60% of the image</li> | ||
| <li>Supported formats: JPG, PNG</li> | ||
| <li>Image will be validated before moving to next step</li> | ||
| </ul> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="two-column-layout__right"> | ||
| <h3 class="section-heading">Personal Details</h3> | ||
| <p class="section-instruction">Please provide correct details and choose your role carefully, it won't be changed | ||
| later.</p> | ||
|
|
||
| <Reusables::InputBox @field='Your Name' @name='fullName' @placeHolder='Full Name' @type='text' @disabled={{true}} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Field is disabled but has @onInput handler which will never fire.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the field is meant to be read only but passing onInput here since it was throwing an error if onInput is undefined
i didn't want to change in reusables input box directly since it's being used at multiple places and it might cause some issues |
||
| @required={{true}} @value={{this.data.fullName}} @onInput={{this.inputHandler}} /> | ||
|
|
||
| <Reusables::Dropdown @field='Your Country' @name='country' @placeHolder='Choose your country' @required={{true}} | ||
| @value={{this.data.country}} @options={{this.countries}} @onChange={{this.inputHandler}} /> | ||
|
|
||
| <Reusables::InputBox @field='State' @name='state' @placeHolder='Your State e.g. Karnataka' @type='text' | ||
| @required={{true}} @value={{this.data.state}} @onInput={{this.inputHandler}} /> | ||
|
|
||
| <Reusables::InputBox @field='City' @name='city' @placeHolder='Your City e.g. Bengaluru' @type='text' | ||
| @required={{true}} @value={{this.data.city}} @onInput={{this.inputHandler}} /> | ||
|
|
||
| <div class="role-selection"> | ||
| <p>Applying as</p> | ||
| <div class="role-buttons" role="radiogroup" aria-label="Select your role"> | ||
| {{#each this.roleOptions as |role|}} | ||
| <button type="button" class="role-button {{if (eq this.data.role role) 'role-button--selected'}}" role="radio" | ||
| aria-checked="{{if (eq this.data.role role) 'true' 'false'}}" {{on 'click' (fn this.selectRole role)}}> | ||
| {{role}} | ||
| </button> | ||
| {{/each}} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
Uh oh!
There was an error while loading. Please reload this page.