From d0adec74b598558ba00dce3e1e6a9d56fe893066 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Mon, 3 Nov 2025 20:24:05 +0530 Subject: [PATCH 01/25] feat: add step one for image upload and personal details --- .../new-join-steps/new-step-one.hbs | 79 +++++ app/components/new-join-steps/new-step-one.js | 81 +++++ .../new-join-steps/stepper-header.hbs | 14 + .../new-join-steps/stepper-header.js | 7 + app/components/new-stepper.hbs | 18 ++ app/constants/new-join-form.js | 23 ++ app/styles/new-stepper.module.css | 289 ++++++++++++++++++ 7 files changed, 511 insertions(+) create mode 100644 app/components/new-join-steps/new-step-one.hbs create mode 100644 app/components/new-join-steps/new-step-one.js create mode 100644 app/components/new-join-steps/stepper-header.hbs create mode 100644 app/components/new-join-steps/stepper-header.js create mode 100644 app/constants/new-join-form.js create mode 100644 app/styles/new-stepper.module.css diff --git a/app/components/new-join-steps/new-step-one.hbs b/app/components/new-join-steps/new-step-one.hbs new file mode 100644 index 00000000..dabc85a1 --- /dev/null +++ b/app/components/new-join-steps/new-step-one.hbs @@ -0,0 +1,79 @@ +
+
+

Profile Picture

+
+
+

Upload Image

+
+
+

Image Requirements:

+
    +
  • Must be a real, clear photograph (no anime, filters, or drawings)
  • +
  • Must contain exactly one face
  • +
  • Face must cover at least 60% of the image
  • +
  • Supported formats: JPG, PNG, GIF
  • +
  • Image will be validated during analysis
  • +
+
+
+ +
+

Personal Details

+

Please provide real name and details, any spam/incorrect details will lead to rejection.

+ + + + + + + + + +
+

Applying as:

+
+ {{#each this.roleOptions as |role|}} + + {{/each}} +
+
+
+
\ No newline at end of file diff --git a/app/components/new-join-steps/new-step-one.js b/app/components/new-join-steps/new-step-one.js new file mode 100644 index 00000000..00fe1c01 --- /dev/null +++ b/app/components/new-join-steps/new-step-one.js @@ -0,0 +1,81 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { validator } from '../../utils/validator'; +import { debounce } from '@ember/runloop'; +import { JOIN_DEBOUNCE_TIME } from '../../constants/join'; +import { NEW_STEP_LIMITS, ROLE_OPTIONS } from '../../constants/new-join-form'; +import { inject as service } from '@ember/service'; + +export default class NewStepOneComponent extends Component { + @service login; + + @tracked data = JSON.parse(localStorage.getItem('newStepOneData')) ?? { + fullName: '', + country: '', + state: '', + city: '', + role: '', + }; + + isValid; + setIsValid; + setIsPreValid; + roleOptions = ROLE_OPTIONS; + + constructor(...args) { + super(...args); + + this.isValid = this.args.isValid; + this.setIsValid = this.args.setIsValid; + this.setIsPreValid = this.args.setIsPreValid; + + if (this.login.userData) { + this.data.fullName = `${this.login.userData.first_name} ${this.login.userData.last_name}`; + } + + const validated = this.isDataValid(); + localStorage.setItem('isValid', validated); + this.setIsPreValid(validated); + } + + isDataValid() { + for (let field in this.data) { + if (field === 'role') { + if (!this.data[field]) return false; + } else { + const { isValid } = validator( + this.data[field], + NEW_STEP_LIMITS.stepOne[field], + ); + if (!isValid) { + return false; + } + } + } + return true; + } + + @action inputHandler(e) { + this.setIsPreValid(false); + const setValToLocalStorage = () => { + this.data = { ...this.data, [e.target.name]: e.target.value }; + localStorage.setItem('newStepOneData', JSON.stringify(this.data)); + localStorage.setItem('selectedRole', this.data.role); + const validated = this.isDataValid(); + this.setIsValid(validated); + localStorage.setItem('isValid', validated); + }; + debounce(this.data, setValToLocalStorage, JOIN_DEBOUNCE_TIME); + } + + @action selectRole(role) { + this.setIsPreValid(false); + this.data = { ...this.data, role }; + localStorage.setItem('newStepOneData', JSON.stringify(this.data)); + localStorage.setItem('selectedRole', role); + const validated = this.isDataValid(); + this.setIsValid(validated); + localStorage.setItem('isValid', validated); + } +} diff --git a/app/components/new-join-steps/stepper-header.hbs b/app/components/new-join-steps/stepper-header.hbs new file mode 100644 index 00000000..026c7c8b --- /dev/null +++ b/app/components/new-join-steps/stepper-header.hbs @@ -0,0 +1,14 @@ +
+
+
+

{{@heading}}

+

{{@subheading}}

+
+
+ Step {{@currentStep}} of {{@totalSteps}} +
+
+
+
+
+
\ No newline at end of file diff --git a/app/components/new-join-steps/stepper-header.js b/app/components/new-join-steps/stepper-header.js new file mode 100644 index 00000000..384da0c9 --- /dev/null +++ b/app/components/new-join-steps/stepper-header.js @@ -0,0 +1,7 @@ +import Component from '@glimmer/component'; + +export default class StepperHeaderComponent extends Component { + get progressPercentage() { + return Math.round((this.args.currentStep / this.args.totalSteps) * 100); + } +} diff --git a/app/components/new-stepper.hbs b/app/components/new-stepper.hbs index 598cf547..280dae94 100644 --- a/app/components/new-stepper.hbs +++ b/app/components/new-stepper.hbs @@ -1,4 +1,14 @@
+ + {{#if (not-eq this.currentStep 0)}} + + {{/if}} +
{{#if (eq this.currentStep 0)}} @@ -13,6 +23,14 @@ @disabled={{not this.joinApplicationTerms.hasUserAcceptedTerms}} /> + + {{else if (eq this.currentStep 1)}} + + {{/if}}
\ No newline at end of file diff --git a/app/constants/new-join-form.js b/app/constants/new-join-form.js new file mode 100644 index 00000000..0b6d57ea --- /dev/null +++ b/app/constants/new-join-form.js @@ -0,0 +1,23 @@ +export const NEW_FORM_STEPS = { + headings: ['Upload Professional HeadShot and fill up personal details'], + subheadings: [ + 'Please provide accurate information for verification purposes.', + ], +}; + +export const ROLE_OPTIONS = [ + 'Developer', + 'Designer', + 'Product Manager', + 'Project Manager', + 'QA', +]; + +export const NEW_STEP_LIMITS = { + stepOne: { + country: 1, + state: 1, + city: 1, + role: 1, + }, +}; diff --git a/app/styles/new-stepper.module.css b/app/styles/new-stepper.module.css new file mode 100644 index 00000000..e83fafc7 --- /dev/null +++ b/app/styles/new-stepper.module.css @@ -0,0 +1,289 @@ +.stepper__form { + max-width: 75%; + margin: 0 auto 3rem; + padding: 2rem; + background-color: var(--color-bgpink); + border-radius: 10px; +} + +.stepper-welcome { + display: flex; + flex-direction: column; + gap: 2rem; + justify-content: center; +} + +.stepper-welcome__logo { + display: flex; + justify-content: center; +} + +.stepper-welcome__info-item--bullet { + display: flex; + align-items: center; + margin-bottom: 1rem; + color: var(--color-black-light); + font-size: 1rem; + line-height: 1.5; +} + +.stepper-welcome__info-item--bullet::before { + content: "•"; + color: var(--color-pink); + font-size: 1.5rem; + font-weight: bold; + margin-right: 0.75rem; +} + +.stepper-welcome__checkbox { + display: flex; + align-items: center; + gap: 0.75rem; + cursor: pointer; +} + +.stepper-welcome__checkbox-text { + color: var(--color-black-light); + font-size: 0.9rem; + line-height: 1.4; +} + +.stepper-welcome__actions { + display: flex; + justify-content: center; +} + +.terms-modal__content { + background: var(--color-white); + border-radius: 10px; + padding: 2rem; + width: 90vw; + max-width: 500px; + max-height: 80vh; + overflow-y: scroll; + display: flex; + flex-direction: column; + gap: 2rem; +} + +.terms-modal__header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.terms-modal__header h2 { + font-size: 1.5rem; +} + +.terms-modal__close { + background-color: transparent !important; + margin: 0 !important; +} + +.terms-modal__close:hover { + background-color: var(--color-lightgrey) !important; + transition: all 0.3s ease-in-out; +} + +.terms-modal__section { + padding-block: 0.5rem; + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.terms-modal__footer { + display: flex; + justify-content: center; +} + +.form-header { + margin-bottom: 2rem; +} + +.form-header__content { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; +} + +.form-header__text { + flex: 1; +} + +.form-header__heading { + font-size: 1.5rem; + font-weight: 600; + margin: 0 0 0.5rem; + color: #333; +} + +.form-header__subheading { + font-size: 1rem; + color: #666; + margin: 0; +} + +.form-header__step-count { + font-size: 0.9rem; + color: #888; + font-weight: 500; +} + +.progress-bar { + width: 100%; + height: 8px; + background-color: #f0f0f0; + border-radius: 4px; + overflow: hidden; +} + +.progress-bar__fill { + height: 100%; + background: linear-gradient(90deg, #6366f1 0%, #ec4899 100%); + transition: width 0.3s ease; +} + +.two-column-layout { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + margin-bottom: 2rem; +} + +.two-column-layout__left, +.two-column-layout__right { + padding: 1rem; +} + +.section-heading { + font-size: 1.2rem; + font-weight: 600; + margin-bottom: 1rem; + color: #333; +} + +.section-instruction { + font-size: 0.9rem; + color: #666; + margin-bottom: 1rem; +} + +/* Image Upload Box */ +.image-upload-box { + border: 2px dashed #ccc; + border-radius: 8px; + padding: 2rem; + text-align: center; + background-color: #fafafa; + margin-bottom: 1rem; +} + +.image-upload-box__icon { + font-size: 2rem; + color: #999; + margin-bottom: 0.5rem; +} + +.image-upload-box__text { + color: #666; + margin: 0; +} + +.image-requirements { + font-size: 0.9rem; + color: #666; +} + +.image-requirements h4 { + margin: 0 0 0.5rem; + font-size: 1rem; + color: #333; +} + +.image-requirements ul { + margin: 0; + padding-left: 1.2rem; +} + +.image-requirements li { + margin-bottom: 0.25rem; +} + +.role-selection { + margin-top: 1rem; +} + +.role-selection h4 { + margin: 0 0 0.75rem; + font-size: 1rem; + color: #333; +} + +.role-buttons { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.role-button { + padding: 0.5rem 1rem; + border: 2px solid #e5e7eb; + background-color: white; + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.2s ease; +} + +.role-button:hover { + border-color: #6366f1; + background-color: #f8fafc; +} + +.role-button--selected { + border-color: #6366f1; + background-color: #6366f1; + color: white; +} + +@media screen and (width <= 768px) { + .stepper__form { + max-width: 85%; + } + + .stepper-welcome { + margin: 1rem auto 3rem; + padding: 1.5rem; + } + + .stepper-welcome img { + width: 100px; + } + + .terms-modal__content { + width: 95vw; + margin: 1rem; + } +} + +@media (width <= 480px) { + .stepper__form { + max-width: 95%; + } + + .stepper-welcome { + margin: 0.5rem auto 3rem; + padding: 1rem; + } + + .stepper-welcome img { + width: 80px; + } + + .stepper-welcome__checkbox-text { + font-size: 0.9rem; + } +} From 6c6687f194daa5e3fa598456e7cf693f8d22317d Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Mon, 3 Nov 2025 21:06:00 +0530 Subject: [PATCH 02/25] fix: styling for form header --- .../new-join-steps/new-step-one.hbs | 1 + .../new-join-steps/stepper-header.hbs | 12 +++--- .../new-join-steps/stepper-header.js | 9 ++++- app/components/new-stepper.hbs | 2 +- app/components/reusables/input-box.hbs | 1 + app/styles/new-stepper.module.css | 40 +++++++++++++------ 6 files changed, 44 insertions(+), 21 deletions(-) diff --git a/app/components/new-join-steps/new-step-one.hbs b/app/components/new-join-steps/new-step-one.hbs index dabc85a1..cbb19edd 100644 --- a/app/components/new-join-steps/new-step-one.hbs +++ b/app/components/new-join-steps/new-step-one.hbs @@ -26,6 +26,7 @@ @name='fullName' @placeHolder='Full Name' @type='text' + @disabled={{true}} @required={{true}} @value={{this.data.fullName}} @onInput={{this.inputHandler}} diff --git a/app/components/new-join-steps/stepper-header.hbs b/app/components/new-join-steps/stepper-header.hbs index 026c7c8b..7fcd9438 100644 --- a/app/components/new-join-steps/stepper-header.hbs +++ b/app/components/new-join-steps/stepper-header.hbs @@ -1,14 +1,14 @@
-

{{@heading}}

-

{{@subheading}}

+

RDS Application Form

-
- Step {{@currentStep}} of {{@totalSteps}} +
+ {{@currentStep}} + of {{@totalSteps}}
-
-
+
+
\ No newline at end of file diff --git a/app/components/new-join-steps/stepper-header.js b/app/components/new-join-steps/stepper-header.js index 384da0c9..3c6e20c7 100644 --- a/app/components/new-join-steps/stepper-header.js +++ b/app/components/new-join-steps/stepper-header.js @@ -1,7 +1,14 @@ import Component from '@glimmer/component'; +import { htmlSafe } from '@ember/template'; export default class StepperHeaderComponent extends Component { get progressPercentage() { - return Math.round((this.args.currentStep / this.args.totalSteps) * 100); + const totalSteps = +this.args.totalSteps ?? 1; + const currentStep = +this.args.currentStep ?? 0; + return Math.round((currentStep / totalSteps) * 100); + } + + get progressStyle() { + return htmlSafe(`width: ${this.progressPercentage}%`); } } diff --git a/app/components/new-stepper.hbs b/app/components/new-stepper.hbs index 280dae94..a31b32fc 100644 --- a/app/components/new-stepper.hbs +++ b/app/components/new-stepper.hbs @@ -5,7 +5,7 @@ @heading={{this.currentHeading}} @subheading={{this.currentSubheading}} @currentStep={{this.currentStep}} - @totalSteps={{6}} + @totalSteps={{this.MAX_STEP}} /> {{/if}} diff --git a/app/components/reusables/input-box.hbs b/app/components/reusables/input-box.hbs index 4d842bb4..30491d75 100644 --- a/app/components/reusables/input-box.hbs +++ b/app/components/reusables/input-box.hbs @@ -13,6 +13,7 @@ id={{@name}} placeholder={{@placeHolder}} required={{@required}} + disabled={{@disabled}} value={{@value}} {{on "input" @onInput}} /> diff --git a/app/styles/new-stepper.module.css b/app/styles/new-stepper.module.css index e83fafc7..7b203789 100644 --- a/app/styles/new-stepper.module.css +++ b/app/styles/new-stepper.module.css @@ -99,50 +99,56 @@ } .form-header { + width: 77.5%; margin-bottom: 2rem; } .form-header__content { display: flex; justify-content: space-between; - align-items: flex-start; + align-items: center; margin-bottom: 1rem; } -.form-header__text { - flex: 1; -} - .form-header__heading { font-size: 1.5rem; font-weight: 600; - margin: 0 0 0.5rem; - color: #333; } .form-header__subheading { - font-size: 1rem; - color: #666; + color: var(--color-lightgrey); margin: 0; } .form-header__step-count { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.form-header__step-current { + font-size: 1.75rem; + font-weight: 700; + color: var(--color-darkblack); +} + +.form-header__step-total { font-size: 0.9rem; - color: #888; - font-weight: 500; + color: var(--color-black-light); } .progress-bar { width: 100%; height: 8px; - background-color: #f0f0f0; + background-color: var(--color-pink-low-opacity); border-radius: 4px; overflow: hidden; } .progress-bar__fill { height: 100%; - background: linear-gradient(90deg, #6366f1 0%, #ec4899 100%); + background: var(--color-navyblue); + width: 0%; transition: width 0.3s ease; } @@ -250,6 +256,10 @@ } @media screen and (width <= 768px) { + .form-header { + width: 87.5%; + } + .stepper__form { max-width: 85%; } @@ -270,6 +280,10 @@ } @media (width <= 480px) { + .form-header { + width: 97.5%; + } + .stepper__form { max-width: 95%; } From 5ebca46b187fa855a370acad1980517f11d39da2 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Mon, 3 Nov 2025 23:49:53 +0530 Subject: [PATCH 03/25] fix: styling for image box and input boxes --- .../new-join-steps/new-step-one.hbs | 24 +++---- app/components/new-join-steps/new-step-one.js | 2 + app/styles/new-stepper.module.css | 71 ++++++++++--------- 3 files changed, 50 insertions(+), 47 deletions(-) diff --git a/app/components/new-join-steps/new-step-one.hbs b/app/components/new-join-steps/new-step-one.hbs index cbb19edd..f11399a1 100644 --- a/app/components/new-join-steps/new-step-one.hbs +++ b/app/components/new-join-steps/new-step-one.hbs @@ -2,7 +2,7 @@

Profile Picture

-
+

Upload Image

@@ -11,15 +11,15 @@
  • Must be a real, clear photograph (no anime, filters, or drawings)
  • Must contain exactly one face
  • Face must cover at least 60% of the image
  • -
  • Supported formats: JPG, PNG, GIF
  • -
  • Image will be validated during analysis
  • +
  • Supported formats: JPG, PNG
  • +
  • Image will be validated before moving to next step
  • Personal Details

    -

    Please provide real name and details, any spam/incorrect details will lead to rejection.

    +

    Please provide correct details and choose your role carefully, it won't be changed later.

    -
    -

    Applying as:

    +

    Applying as

    {{#each this.roleOptions as |role|}}
    {{/if}} From 77f0031f8990b7809afa5fd72c29f6256fac09a7 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Tue, 4 Nov 2025 18:17:11 +0530 Subject: [PATCH 06/25] feat: add base step component for all new steps --- app/components/new-join-steps/base-step.js | 102 ++++++++++++++++++ app/components/new-join-steps/new-step-one.js | 80 +++----------- .../new-join-steps/new-step-three.hbs | 38 +++++++ .../new-join-steps/new-step-three.js | 14 +++ .../new-join-steps/new-step-two.hbs | 54 ++++++++++ app/components/new-join-steps/new-step-two.js | 12 +++ app/components/new-stepper.js | 9 ++ 7 files changed, 246 insertions(+), 63 deletions(-) create mode 100644 app/components/new-join-steps/base-step.js create mode 100644 app/components/new-join-steps/new-step-three.hbs create mode 100644 app/components/new-join-steps/new-step-three.js create mode 100644 app/components/new-join-steps/new-step-two.hbs create mode 100644 app/components/new-join-steps/new-step-two.js diff --git a/app/components/new-join-steps/base-step.js b/app/components/new-join-steps/base-step.js new file mode 100644 index 00000000..25af01dd --- /dev/null +++ b/app/components/new-join-steps/base-step.js @@ -0,0 +1,102 @@ +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'; + +export default class BaseStepComponent extends Component { + storageKey = ''; + validationMap = {}; + + @tracked data = {}; + @tracked errorMessage = {}; + @tracked wordCount = {}; + + isValid; + setIsValid; + setIsPreValid; + + constructor(...args) { + super(...args); + this.isValid = this.args.isValid; + this.setIsValid = this.args.setIsValid; + this.setIsPreValid = this.args.setIsPreValid; + + const saved = JSON.parse(localStorage.getItem(this.storageKey) || 'null'); + this.data = saved ?? {}; + + this.errorMessage = Object.fromEntries( + Object.keys(this.validationMap).map((key) => [key, '']), + ); + + this.wordCount = Object.fromEntries( + Object.keys(this.validationMap).map((key) => { + const value = this?.data?.[key] ?? ''; + return [key, value?.trim()?.split(/\s+/).filter(Boolean).length ?? 0]; + }), + ); + + const validated = this.isDataValid(); + localStorage.setItem('isValid', validated); + this.setIsPreValid(validated); + } + + @action inputHandler(e) { + this.setIsPreValid(false); + const field = e.target.name; + const value = e.target.value; + debounce(this, this.handleFieldUpdate, field, value, JOIN_DEBOUNCE_TIME); + } + + validateField(field, value) { + const limits = this.validationMap[field]; + return validateWordCount(value, limits); + } + + isDataValid() { + for (let field of Object.keys(this.validationMap)) { + 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, value, result); + this.updateErrorMessage(field, result); + this.syncFormValidity(); + } + + updateFieldValue(field, value) { + this.data = { ...this.data, [field]: value }; + localStorage.setItem(this.storageKey, JSON.stringify(this.data)); + } + + updateWordCount(field, value, result) { + const wordCount = + result.wordCount ?? + value?.trim()?.split(/\s+/).filter(Boolean).length ?? + 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.validationMap[field]; + if (result.isValid) return ''; + if (result.remainingToMin) { + return `At least ${result.remainingToMin} more word(s) required`; + } + const max = limits.max; + return `Maximum ${max} words allowed`; + } +} diff --git a/app/components/new-join-steps/new-step-one.js b/app/components/new-join-steps/new-step-one.js index 18520bd4..035fbf02 100644 --- a/app/components/new-join-steps/new-step-one.js +++ b/app/components/new-join-steps/new-step-one.js @@ -1,83 +1,37 @@ -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; -import { validator } from '../../utils/validator'; -import { debounce } from '@ember/runloop'; -import { JOIN_DEBOUNCE_TIME } from '../../constants/join'; +import BaseStepComponent from './base-step'; import { NEW_STEP_LIMITS, ROLE_OPTIONS } from '../../constants/new-join-form'; import { countryList } from '../../constants/country-list'; import { inject as service } from '@ember/service'; -export default class NewStepOneComponent extends Component { +export default class NewStepOneComponent extends BaseStepComponent { @service login; - @tracked data = JSON.parse(localStorage.getItem('newStepOneData')) ?? { - fullName: '', - country: '', - state: '', - city: '', - role: '', + storageKey = 'newStepOneData'; + validationMap = { + country: NEW_STEP_LIMITS.stepOne.country, + state: NEW_STEP_LIMITS.stepOne.state, + city: NEW_STEP_LIMITS.stepOne.city, + role: NEW_STEP_LIMITS.stepOne.role, }; - isValid; - setIsValid; - setIsPreValid; roleOptions = ROLE_OPTIONS; countries = countryList; constructor(...args) { super(...args); - - this.isValid = this.args.isValid; - this.setIsValid = this.args.setIsValid; - this.setIsPreValid = this.args.setIsPreValid; - - if (this.login.userData) { - this.data.fullName = `${this.login.userData.first_name} ${this.login.userData.last_name}`; - } - - const validated = this.isDataValid(); - localStorage.setItem('isValid', validated); - this.setIsPreValid(validated); - } - - isDataValid() { - for (let field in this.data) { - if (field === 'role') { - if (!this.data[field]) return false; - } else { - const { isValid } = validator( - this.data[field], - NEW_STEP_LIMITS.stepOne[field], - ); - if (!isValid) { - return false; - } - } - } - return true; - } - - @action inputHandler(e) { - this.setIsPreValid(false); - const setValToLocalStorage = () => { - this.data = { ...this.data, [e.target.name]: e.target.value }; - localStorage.setItem('newStepOneData', JSON.stringify(this.data)); - localStorage.setItem('selectedRole', this.data.role); + if (this.login.userData && !this.data.fullName) { + this.data = { + ...this.data, + fullName: `${this.login.userData.first_name} ${this.login.userData.last_name}`, + }; + localStorage.setItem(this.storageKey, JSON.stringify(this.data)); const validated = this.isDataValid(); this.setIsValid(validated); localStorage.setItem('isValid', validated); - }; - debounce(this.data, setValToLocalStorage, JOIN_DEBOUNCE_TIME); + } } - @action selectRole(role) { - this.setIsPreValid(false); - this.data = { ...this.data, role }; - localStorage.setItem('newStepOneData', JSON.stringify(this.data)); - localStorage.setItem('selectedRole', role); - const validated = this.isDataValid(); - this.setIsValid(validated); - localStorage.setItem('isValid', validated); + selectRole(role) { + this.inputHandler({ target: { name: 'role', value: role } }); } } diff --git a/app/components/new-join-steps/new-step-three.hbs b/app/components/new-join-steps/new-step-three.hbs new file mode 100644 index 00000000..43d8c901 --- /dev/null +++ b/app/components/new-join-steps/new-step-three.hbs @@ -0,0 +1,38 @@ +
    +

    {{@heading}}

    +

    {{@subHeading}}

    +
    + +
    +
    +

    What do you do for fun? Your hobbies/interests?

    + +
    {{this.wordCount.hobbies}}/{{this.maxWords.hobbies}} words
    + {{#if this.errorMessage.hobbies}} +
    {{this.errorMessage.hobbies}}
    + {{/if}} +
    + +
    +

    Fun fact about you

    + +
    {{this.wordCount.funFact}}/{{this.maxWords.funFact}} words
    + {{#if this.errorMessage.funFact}} +
    {{this.errorMessage.funFact}}
    + {{/if}} +
    +
    \ No newline at end of file diff --git a/app/components/new-join-steps/new-step-three.js b/app/components/new-join-steps/new-step-three.js new file mode 100644 index 00000000..9b88406f --- /dev/null +++ b/app/components/new-join-steps/new-step-three.js @@ -0,0 +1,14 @@ +import BaseStepComponent from './base-step'; +import { NEW_STEP_LIMITS } from '../../constants/new-join-form'; + +export default class NewStepThreeComponent extends BaseStepComponent { + storageKey = 'newStepThreeData'; + validationMap = { + hobbies: NEW_STEP_LIMITS.stepThree.hobbies, + funFact: NEW_STEP_LIMITS.stepThree.funFact, + }; + maxWords = { + hobbies: NEW_STEP_LIMITS.stepThree.hobbies.max, + funFact: NEW_STEP_LIMITS.stepThree.funFact.max, + }; +} diff --git a/app/components/new-join-steps/new-step-two.hbs b/app/components/new-join-steps/new-step-two.hbs new file mode 100644 index 00000000..4d947d99 --- /dev/null +++ b/app/components/new-join-steps/new-step-two.hbs @@ -0,0 +1,54 @@ +
    +
    +

    {{@heading}}

    +

    {{@subHeading}}

    +
    + +
    +
    + + {{#if this.errorMessage.skills}} +
    {{this.errorMessage.skills}}
    + {{/if}} +
    + +
    + + {{#if this.errorMessage.company}} +
    {{this.errorMessage.company}}
    + {{/if}} +
    +
    + +

    Please introduce yourself

    + +
    {{this.wordCount.introduction}}/{{this.maxWords.introduction}} words
    +

    Share your background, experiences, and what drives you.

    + + {{#if this.errorMessage.introduction}} +
    {{this.errorMessage.introduction}}
    + {{/if}} +
    \ No newline at end of file diff --git a/app/components/new-join-steps/new-step-two.js b/app/components/new-join-steps/new-step-two.js new file mode 100644 index 00000000..813ed5a4 --- /dev/null +++ b/app/components/new-join-steps/new-step-two.js @@ -0,0 +1,12 @@ +import BaseStepComponent from './base-step'; +import { NEW_STEP_LIMITS } from '../../constants/new-join-form'; + +export default class NewStepTwoComponent extends BaseStepComponent { + storageKey = 'newStepTwoData'; + validationMap = { + skills: NEW_STEP_LIMITS.stepTwo.skills, + company: NEW_STEP_LIMITS.stepTwo.company, + introduction: NEW_STEP_LIMITS.stepTwo.introduction, + }; + maxWords = { introduction: NEW_STEP_LIMITS.stepTwo.introduction.max }; +} diff --git a/app/components/new-stepper.js b/app/components/new-stepper.js index 472c8e7f..17497728 100644 --- a/app/components/new-stepper.js +++ b/app/components/new-stepper.js @@ -2,6 +2,7 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { inject as service } from '@ember/service'; +import { NEW_FORM_STEPS } from '../constants/new-join-form'; export default class NewStepperComponent extends Component { MIN_STEP = 0; @@ -30,6 +31,14 @@ export default class NewStepperComponent extends Component { return this.currentStep > this.MIN_STEP + 1; } + get currentHeading() { + return NEW_FORM_STEPS.headings[this.currentStep - 1] ?? ''; + } + + get currentSubheading() { + return NEW_FORM_STEPS.subheadings[this.currentStep - 1] ?? ''; + } + @action incrementStep() { if (this.currentStep < this.MAX_STEP) { const nextStep = this.currentStep + 1; From aff01fa5d9f005e2ed253ec8cece7e5fd7a7d82e Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Tue, 4 Nov 2025 21:00:03 +0530 Subject: [PATCH 07/25] fix: input validation in new step one --- app/components/new-join-steps/base-step.js | 45 +++++++++++-------- app/components/new-join-steps/new-step-one.js | 35 +++++++-------- app/components/new-stepper.hbs | 1 - app/constants/new-join-form.js | 8 ++-- app/utils/validator.js | 17 +++++++ 5 files changed, 65 insertions(+), 41 deletions(-) diff --git a/app/components/new-join-steps/base-step.js b/app/components/new-join-steps/base-step.js index 25af01dd..f313305d 100644 --- a/app/components/new-join-steps/base-step.js +++ b/app/components/new-join-steps/base-step.js @@ -4,46 +4,50 @@ 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'; export default class BaseStepComponent extends Component { - storageKey = ''; validationMap = {}; @tracked data = {}; @tracked errorMessage = {}; @tracked wordCount = {}; - isValid; - setIsValid; - setIsPreValid; + get storageKey() { + return ''; + } + + postLoadInitialize() {} constructor(...args) { super(...args); - this.isValid = this.args.isValid; - this.setIsValid = this.args.setIsValid; - this.setIsPreValid = this.args.setIsPreValid; + scheduleOnce('afterRender', this, this.initializeFormState); + } - const saved = JSON.parse(localStorage.getItem(this.storageKey) || 'null'); + initializeFormState() { + const saved = JSON.parse(localStorage.getItem(this.storageKey) || '{}'); this.data = saved ?? {}; this.errorMessage = Object.fromEntries( - Object.keys(this.validationMap).map((key) => [key, '']), + Object.keys(this.validationMap).map((k) => [k, '']), ); this.wordCount = Object.fromEntries( - Object.keys(this.validationMap).map((key) => { - const value = this?.data?.[key] ?? ''; - return [key, value?.trim()?.split(/\s+/).filter(Boolean).length ?? 0]; + Object.keys(this.validationMap).map((k) => { + let val = this.data[k] || ''; + return [k, val.trim().split(/\s+/).filter(Boolean).length || 0]; }), ); - const validated = this.isDataValid(); - localStorage.setItem('isValid', validated); - this.setIsPreValid(validated); + this.postLoadInitialize(); + + const valid = this.isDataValid(); + this.args.setIsPreValid(valid); + localStorage.setItem('isValid', valid); } @action inputHandler(e) { - this.setIsPreValid(false); + this.args.setIsPreValid(false); const field = e.target.name; const value = e.target.value; debounce(this, this.handleFieldUpdate, field, value, JOIN_DEBOUNCE_TIME); @@ -96,7 +100,12 @@ export default class BaseStepComponent extends Component { if (result.remainingToMin) { return `At least ${result.remainingToMin} more word(s) required`; } - const max = limits.max; - return `Maximum ${max} words allowed`; + return `Maximum ${limits.max} words allowed`; + } + + syncFormValidity() { + const allValid = this.isDataValid(); + this.args.setIsValid(allValid); + localStorage.setItem('isValid', allValid); } } diff --git a/app/components/new-join-steps/new-step-one.js b/app/components/new-join-steps/new-step-one.js index 035fbf02..8b800c4f 100644 --- a/app/components/new-join-steps/new-step-one.js +++ b/app/components/new-join-steps/new-step-one.js @@ -1,12 +1,19 @@ -import BaseStepComponent from './base-step'; -import { NEW_STEP_LIMITS, ROLE_OPTIONS } from '../../constants/new-join-form'; -import { countryList } from '../../constants/country-list'; +import { action } from '@ember/object'; import { inject as service } from '@ember/service'; +import { countryList } from '../../constants/country-list'; +import { NEW_STEP_LIMITS, ROLE_OPTIONS } from '../../constants/new-join-form'; +import BaseStepComponent from './base-step'; export default class NewStepOneComponent extends BaseStepComponent { @service login; - storageKey = 'newStepOneData'; + roleOptions = ROLE_OPTIONS; + countries = countryList; + + get storageKey() { + return 'newStepOneData'; + } + validationMap = { country: NEW_STEP_LIMITS.stepOne.country, state: NEW_STEP_LIMITS.stepOne.state, @@ -14,24 +21,16 @@ export default class NewStepOneComponent extends BaseStepComponent { role: NEW_STEP_LIMITS.stepOne.role, }; - roleOptions = ROLE_OPTIONS; - countries = countryList; - - constructor(...args) { - super(...args); + postLoadInitialize() { if (this.login.userData && !this.data.fullName) { - this.data = { - ...this.data, - fullName: `${this.login.userData.first_name} ${this.login.userData.last_name}`, - }; - localStorage.setItem(this.storageKey, JSON.stringify(this.data)); - const validated = this.isDataValid(); - this.setIsValid(validated); - localStorage.setItem('isValid', validated); + this.updateFieldValue( + 'fullName', + `${this.login.userData.first_name} ${this.login.userData.last_name}`, + ); } } - selectRole(role) { + @action selectRole(role) { this.inputHandler({ target: { name: 'role', value: role } }); } } diff --git a/app/components/new-stepper.hbs b/app/components/new-stepper.hbs index aaf5c2bf..99fce813 100644 --- a/app/components/new-stepper.hbs +++ b/app/components/new-stepper.hbs @@ -31,7 +31,6 @@ @setIsValid={{this.setIsValid}} /> {{/if}} - {{#if (not-eq this.currentStep this.MIN_STEP)}} diff --git a/app/constants/new-join-form.js b/app/constants/new-join-form.js index 0b6d57ea..e7f47846 100644 --- a/app/constants/new-join-form.js +++ b/app/constants/new-join-form.js @@ -15,9 +15,9 @@ export const ROLE_OPTIONS = [ export const NEW_STEP_LIMITS = { stepOne: { - country: 1, - state: 1, - city: 1, - role: 1, + country: { min: 1 }, + state: { min: 1 }, + city: { min: 1 }, + role: { min: 1 }, }, }; diff --git a/app/utils/validator.js b/app/utils/validator.js index ec26bdea..13927746 100644 --- a/app/utils/validator.js +++ b/app/utils/validator.js @@ -13,3 +13,20 @@ export const validator = (value, words) => { remainingWords: remainingWords, }; }; + +export const validateWordCount = (text, wordLimits) => { + const trimmedText = text?.trim(); + const wordCount = trimmedText?.split(/\s+/)?.length ?? 0; + + const { min, max } = wordLimits; + if (!trimmedText) { + return { isValid: false, wordCount: 0, remainingToMin: min }; + } + + if (wordCount < min) { + return { isValid: false, wordCount, remainingToMin: min - wordCount }; + } else if (max && wordCount > max) { + return { isValid: false, wordCount, overByMax: wordCount - max }; + } + return { isValid: true, wordCount }; +}; From e0e1d3ba7fe32cd0acb30a0b4a2696d96039f086 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Thu, 6 Nov 2025 22:39:22 +0530 Subject: [PATCH 08/25] refactor: improve progress calculation and update header text --- app/components/new-join-steps/stepper-header.js | 7 +++++-- app/constants/new-join-form.js | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/components/new-join-steps/stepper-header.js b/app/components/new-join-steps/stepper-header.js index 3c6e20c7..a9cb4c1d 100644 --- a/app/components/new-join-steps/stepper-header.js +++ b/app/components/new-join-steps/stepper-header.js @@ -1,13 +1,16 @@ import Component from '@glimmer/component'; import { htmlSafe } from '@ember/template'; +import { cached } from '@glimmer/tracking'; export default class StepperHeaderComponent extends Component { + @cached get progressPercentage() { - const totalSteps = +this.args.totalSteps ?? 1; - const currentStep = +this.args.currentStep ?? 0; + const totalSteps = Number(this.args.totalSteps) ?? 1; + const currentStep = Number(this.args.currentStep) ?? 0; return Math.round((currentStep / totalSteps) * 100); } + @cached get progressStyle() { return htmlSafe(`width: ${this.progressPercentage}%`); } diff --git a/app/constants/new-join-form.js b/app/constants/new-join-form.js index e7f47846..1480c45a 100644 --- a/app/constants/new-join-form.js +++ b/app/constants/new-join-form.js @@ -1,5 +1,5 @@ export const NEW_FORM_STEPS = { - headings: ['Upload Professional HeadShot and fill up personal details'], + headings: ['Upload Professional Headshot and Complete Personal Details'], subheadings: [ 'Please provide accurate information for verification purposes.', ], From a90a012e6ca91ebe5c7b1dcfbd051cce69995202 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Sat, 8 Nov 2025 17:20:13 +0530 Subject: [PATCH 09/25] refactor: improve step navigation --- app/components/new-join-steps/base-step.js | 9 +++------ app/components/new-join-steps/stepper-header.js | 4 ++-- app/components/new-stepper.hbs | 4 ++-- app/components/new-stepper.js | 8 ++++++++ app/utils/validator.js | 2 +- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/components/new-join-steps/base-step.js b/app/components/new-join-steps/base-step.js index f313305d..07d073fe 100644 --- a/app/components/new-join-steps/base-step.js +++ b/app/components/new-join-steps/base-step.js @@ -69,7 +69,7 @@ export default class BaseStepComponent extends Component { handleFieldUpdate(field, value) { this.updateFieldValue(field, value); const result = this.validateField(field, value); - this.updateWordCount(field, value, result); + this.updateWordCount(field, result); this.updateErrorMessage(field, result); this.syncFormValidity(); } @@ -79,11 +79,8 @@ export default class BaseStepComponent extends Component { localStorage.setItem(this.storageKey, JSON.stringify(this.data)); } - updateWordCount(field, value, result) { - const wordCount = - result.wordCount ?? - value?.trim()?.split(/\s+/).filter(Boolean).length ?? - 0; + updateWordCount(field, result) { + const wordCount = result.wordCount ?? 0; this.wordCount = { ...this.wordCount, [field]: wordCount }; } diff --git a/app/components/new-join-steps/stepper-header.js b/app/components/new-join-steps/stepper-header.js index a9cb4c1d..9b81fd3c 100644 --- a/app/components/new-join-steps/stepper-header.js +++ b/app/components/new-join-steps/stepper-header.js @@ -5,8 +5,8 @@ import { cached } from '@glimmer/tracking'; export default class StepperHeaderComponent extends Component { @cached get progressPercentage() { - const totalSteps = Number(this.args.totalSteps) ?? 1; - const currentStep = Number(this.args.currentStep) ?? 0; + const totalSteps = Number(this.args.totalSteps) || 1; + const currentStep = Number(this.args.currentStep) || 0; return Math.round((currentStep / totalSteps) * 100); } diff --git a/app/components/new-stepper.hbs b/app/components/new-stepper.hbs index 99fce813..9c6ae4de 100644 --- a/app/components/new-stepper.hbs +++ b/app/components/new-stepper.hbs @@ -9,7 +9,7 @@ /> {{/if}} -
    + {{#if (eq this.currentStep this.MIN_STEP)}} @@ -34,7 +34,7 @@ {{#if (not-eq this.currentStep this.MIN_STEP)}} -
    +
    {{#if this.showPreviousButton}} (this.isValid = newVal); + setIsPreValid = (newVal) => (this.preValid = newVal); + updateQueryParam(step) { const existingQueryParams = this.router.currentRoute?.queryParams; @@ -43,6 +49,7 @@ export default class NewStepperComponent extends Component { if (this.currentStep < this.MAX_STEP) { const nextStep = this.currentStep + 1; localStorage.setItem('currentStep', String(nextStep)); + this.currentStep = nextStep; this.updateQueryParam(nextStep); } } @@ -51,6 +58,7 @@ export default class NewStepperComponent extends Component { if (this.currentStep > this.MIN_STEP) { const previousStep = this.currentStep - 1; localStorage.setItem('currentStep', String(previousStep)); + this.currentStep = previousStep; this.updateQueryParam(previousStep); } } diff --git a/app/utils/validator.js b/app/utils/validator.js index 13927746..d1cdd428 100644 --- a/app/utils/validator.js +++ b/app/utils/validator.js @@ -16,7 +16,7 @@ export const validator = (value, words) => { export const validateWordCount = (text, wordLimits) => { const trimmedText = text?.trim(); - const wordCount = trimmedText?.split(/\s+/)?.length ?? 0; + const wordCount = trimmedText?.split(/\s+/).filter(Boolean).length ?? 0; const { min, max } = wordLimits; if (!trimmedText) { From e5edda0e2436fbaa0315f8858abff00e8eb61a45 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Sat, 8 Nov 2025 18:41:13 +0530 Subject: [PATCH 10/25] fix: image upload preview and update storage keys --- app/components/new-join-steps/base-step.js | 12 +- .../new-join-steps/new-step-one.hbs | 31 +- app/components/new-join-steps/new-step-one.js | 43 ++- .../new-join-steps/new-step-three.hbs | 38 --- .../new-join-steps/new-step-three.js | 14 - .../new-join-steps/new-step-two.hbs | 54 --- app/components/new-join-steps/new-step-two.js | 12 - app/constants/new-join-form.js | 5 + app/styles/new-stepper.module.css | 321 ------------------ 9 files changed, 77 insertions(+), 453 deletions(-) delete mode 100644 app/components/new-join-steps/new-step-three.hbs delete mode 100644 app/components/new-join-steps/new-step-three.js delete mode 100644 app/components/new-join-steps/new-step-two.hbs delete mode 100644 app/components/new-join-steps/new-step-two.js delete mode 100644 app/styles/new-stepper.module.css diff --git a/app/components/new-join-steps/base-step.js b/app/components/new-join-steps/base-step.js index 07d073fe..a6935cae 100644 --- a/app/components/new-join-steps/base-step.js +++ b/app/components/new-join-steps/base-step.js @@ -7,7 +7,7 @@ import { validateWordCount } from '../../utils/validator'; import { scheduleOnce } from '@ember/runloop'; export default class BaseStepComponent extends Component { - validationMap = {}; + stepValidation = {}; @tracked data = {}; @tracked errorMessage = {}; @@ -29,11 +29,11 @@ export default class BaseStepComponent extends Component { this.data = saved ?? {}; this.errorMessage = Object.fromEntries( - Object.keys(this.validationMap).map((k) => [k, '']), + Object.keys(this.stepValidation).map((k) => [k, '']), ); this.wordCount = Object.fromEntries( - Object.keys(this.validationMap).map((k) => { + Object.keys(this.stepValidation).map((k) => { let val = this.data[k] || ''; return [k, val.trim().split(/\s+/).filter(Boolean).length || 0]; }), @@ -54,12 +54,12 @@ export default class BaseStepComponent extends Component { } validateField(field, value) { - const limits = this.validationMap[field]; + const limits = this.stepValidation[field]; return validateWordCount(value, limits); } isDataValid() { - for (let field of Object.keys(this.validationMap)) { + for (let field of Object.keys(this.stepValidation)) { const result = this.validateField(field, this.data[field]); if (!result.isValid) return false; } @@ -92,7 +92,7 @@ export default class BaseStepComponent extends Component { } formatError(field, result) { - const limits = this.validationMap[field]; + const limits = this.stepValidation[field]; if (result.isValid) return ''; if (result.remainingToMin) { return `At least ${result.remainingToMin} more word(s) required`; diff --git a/app/components/new-join-steps/new-step-one.hbs b/app/components/new-join-steps/new-step-one.hbs index f11399a1..c175fcf2 100644 --- a/app/components/new-join-steps/new-step-one.hbs +++ b/app/components/new-join-steps/new-step-one.hbs @@ -1,10 +1,33 @@

    Profile Picture

    -
    - -

    Upload Image

    -
    + {{#if this.imagePreview}} +
    + Profile preview + +
    + {{else}} + + {{/if}} +

    Image Requirements:

      diff --git a/app/components/new-join-steps/new-step-one.js b/app/components/new-join-steps/new-step-one.js index 8b800c4f..6e3864d2 100644 --- a/app/components/new-join-steps/new-step-one.js +++ b/app/components/new-join-steps/new-step-one.js @@ -1,7 +1,12 @@ import { action } from '@ember/object'; import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; import { countryList } from '../../constants/country-list'; -import { NEW_STEP_LIMITS, ROLE_OPTIONS } from '../../constants/new-join-form'; +import { + NEW_STEP_LIMITS, + ROLE_OPTIONS, + STEP_DATA_STORAGE_KEY, +} from '../../constants/new-join-form'; import BaseStepComponent from './base-step'; export default class NewStepOneComponent extends BaseStepComponent { @@ -10,11 +15,13 @@ export default class NewStepOneComponent extends BaseStepComponent { roleOptions = ROLE_OPTIONS; countries = countryList; + @tracked imagePreview = null; + get storageKey() { - return 'newStepOneData'; + return STEP_DATA_STORAGE_KEY.stepOne; } - validationMap = { + stepValidation = { country: NEW_STEP_LIMITS.stepOne.country, state: NEW_STEP_LIMITS.stepOne.state, city: NEW_STEP_LIMITS.stepOne.city, @@ -22,15 +29,43 @@ export default class NewStepOneComponent extends BaseStepComponent { }; postLoadInitialize() { - if (this.login.userData && !this.data.fullName) { + if (!this.data.fullName && this.login.userData) { this.updateFieldValue( 'fullName', `${this.login.userData.first_name} ${this.login.userData.last_name}`, ); } + if (this.data.profileImageBase64) { + this.imagePreview = this.data.profileImageBase64; + } } @action selectRole(role) { this.inputHandler({ target: { name: 'role', value: role } }); } + + @action triggerFileInput() { + const fileInput = document.getElementById('profile-image-input'); + if (fileInput) { + fileInput.click(); + } + } + + @action handleImageSelect(event) { + const file = event.target.files?.[0]; + if (!file) return; + + if (file.size > 2 * 1024 * 1024) { + alert('Image size must be less than 2MB'); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + const base64String = e.target.result; + this.imagePreview = base64String; + this.updateFieldValue('profileImageBase64', base64String); + }; + reader.readAsDataURL(file); + } } diff --git a/app/components/new-join-steps/new-step-three.hbs b/app/components/new-join-steps/new-step-three.hbs deleted file mode 100644 index 43d8c901..00000000 --- a/app/components/new-join-steps/new-step-three.hbs +++ /dev/null @@ -1,38 +0,0 @@ -
      -

      {{@heading}}

      -

      {{@subHeading}}

      -
      - -
      -
      -

      What do you do for fun? Your hobbies/interests?

      - -
      {{this.wordCount.hobbies}}/{{this.maxWords.hobbies}} words
      - {{#if this.errorMessage.hobbies}} -
      {{this.errorMessage.hobbies}}
      - {{/if}} -
      - -
      -

      Fun fact about you

      - -
      {{this.wordCount.funFact}}/{{this.maxWords.funFact}} words
      - {{#if this.errorMessage.funFact}} -
      {{this.errorMessage.funFact}}
      - {{/if}} -
      -
      \ No newline at end of file diff --git a/app/components/new-join-steps/new-step-three.js b/app/components/new-join-steps/new-step-three.js deleted file mode 100644 index 9b88406f..00000000 --- a/app/components/new-join-steps/new-step-three.js +++ /dev/null @@ -1,14 +0,0 @@ -import BaseStepComponent from './base-step'; -import { NEW_STEP_LIMITS } from '../../constants/new-join-form'; - -export default class NewStepThreeComponent extends BaseStepComponent { - storageKey = 'newStepThreeData'; - validationMap = { - hobbies: NEW_STEP_LIMITS.stepThree.hobbies, - funFact: NEW_STEP_LIMITS.stepThree.funFact, - }; - maxWords = { - hobbies: NEW_STEP_LIMITS.stepThree.hobbies.max, - funFact: NEW_STEP_LIMITS.stepThree.funFact.max, - }; -} diff --git a/app/components/new-join-steps/new-step-two.hbs b/app/components/new-join-steps/new-step-two.hbs deleted file mode 100644 index 4d947d99..00000000 --- a/app/components/new-join-steps/new-step-two.hbs +++ /dev/null @@ -1,54 +0,0 @@ -
      -
      -

      {{@heading}}

      -

      {{@subHeading}}

      -
      - -
      -
      - - {{#if this.errorMessage.skills}} -
      {{this.errorMessage.skills}}
      - {{/if}} -
      - -
      - - {{#if this.errorMessage.company}} -
      {{this.errorMessage.company}}
      - {{/if}} -
      -
      - -

      Please introduce yourself

      - -
      {{this.wordCount.introduction}}/{{this.maxWords.introduction}} words
      -

      Share your background, experiences, and what drives you.

      - - {{#if this.errorMessage.introduction}} -
      {{this.errorMessage.introduction}}
      - {{/if}} -
      \ No newline at end of file diff --git a/app/components/new-join-steps/new-step-two.js b/app/components/new-join-steps/new-step-two.js deleted file mode 100644 index 813ed5a4..00000000 --- a/app/components/new-join-steps/new-step-two.js +++ /dev/null @@ -1,12 +0,0 @@ -import BaseStepComponent from './base-step'; -import { NEW_STEP_LIMITS } from '../../constants/new-join-form'; - -export default class NewStepTwoComponent extends BaseStepComponent { - storageKey = 'newStepTwoData'; - validationMap = { - skills: NEW_STEP_LIMITS.stepTwo.skills, - company: NEW_STEP_LIMITS.stepTwo.company, - introduction: NEW_STEP_LIMITS.stepTwo.introduction, - }; - maxWords = { introduction: NEW_STEP_LIMITS.stepTwo.introduction.max }; -} diff --git a/app/constants/new-join-form.js b/app/constants/new-join-form.js index 1480c45a..6911a135 100644 --- a/app/constants/new-join-form.js +++ b/app/constants/new-join-form.js @@ -11,6 +11,7 @@ export const ROLE_OPTIONS = [ 'Product Manager', 'Project Manager', 'QA', + 'Social Media', ]; export const NEW_STEP_LIMITS = { @@ -21,3 +22,7 @@ export const NEW_STEP_LIMITS = { role: { min: 1 }, }, }; + +export const STEP_DATA_STORAGE_KEY = { + stepOne: 'newStepOneData', +}; diff --git a/app/styles/new-stepper.module.css b/app/styles/new-stepper.module.css deleted file mode 100644 index 3a48c261..00000000 --- a/app/styles/new-stepper.module.css +++ /dev/null @@ -1,321 +0,0 @@ -.stepper__form { - max-width: 75%; - margin: 0 auto 3rem; - padding: 2rem; - background-color: var(--color-bgpink); - border-radius: 10px; -} - -.stepper__buttons { - width: 77.5%; - display: flex; - justify-content: space-between; - align-items: center; - margin: 0; -} - -.stepper-welcome { - display: flex; - flex-direction: column; - gap: 2rem; - justify-content: center; -} - -.stepper-welcome__logo { - display: flex; - justify-content: center; -} - -.stepper-welcome__info-item--bullet { - display: flex; - align-items: center; - margin-bottom: 1rem; - color: var(--color-black-light); - font-size: 1rem; - line-height: 1.5; -} - -.stepper-welcome__info-item--bullet::before { - content: "•"; - color: var(--color-pink); - font-size: 1.5rem; - font-weight: bold; - margin-right: 0.75rem; -} - -.stepper-welcome__checkbox { - display: flex; - align-items: center; - gap: 0.75rem; - cursor: pointer; -} - -.stepper-welcome__checkbox-text { - color: var(--color-black-light); - font-size: 0.9rem; - line-height: 1.4; -} - -.stepper-welcome__actions { - margin-top: 2rem; - display: flex; - justify-content: center; -} - -.terms-modal__content { - background: var(--color-white); - border-radius: 10px; - padding: 2rem; - width: 90vw; - max-width: 500px; - max-height: 80vh; - overflow-y: scroll; - display: flex; - flex-direction: column; - gap: 2rem; -} - -.terms-modal__header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.terms-modal__header h2 { - font-size: 1.5rem; -} - -.terms-modal__close { - background-color: transparent !important; - margin: 0 !important; -} - -.terms-modal__close:hover { - background-color: var(--color-lightgrey) !important; - transition: all 0.3s ease-in-out; -} - -.terms-modal__section { - padding-block: 0.5rem; - display: flex; - flex-direction: column; - gap: 0.2rem; -} - -.terms-modal__footer { - display: flex; - justify-content: center; -} - -.form-header { - width: 77.5%; - margin-bottom: 2rem; -} - -.form-header__content { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; -} - -.form-header__heading { - font-size: 1.5rem; - font-weight: 600; -} - -.form-header__subheading { - color: var(--color-lightgrey); - margin: 0; -} - -.form-header__step-count { - display: flex; - flex-direction: column; - align-items: flex-end; -} - -.form-header__step-current { - font-size: 1.75rem; - font-weight: 700; - color: var(--color-darkblack); -} - -.form-header__step-total { - font-size: 0.9rem; - color: var(--color-black-light); -} - -.progress-bar { - width: 100%; - height: 8px; - background-color: var(--color-pink-low-opacity); - border-radius: 4px; - overflow: hidden; -} - -.progress-bar__fill { - height: 100%; - background: var(--color-navyblue); - width: 0%; - transition: width 0.3s ease; -} - -.two-column-layout { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; -} - -.two-column-layout__left { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.section-heading { - font-size: 1.2rem; - font-weight: 600; - margin-bottom: 0.25rem; - color: var(--color-black); -} - -.section-instruction { - font-size: 0.9rem; - color: var(--color-black-light); -} - -.image-upload-box { - border: 2px dashed var(--color-lightgrey); - border-radius: 10px; - padding: 2rem; - text-align: center; - background-color: var(--color-white); -} - -.image-upload-box:hover { - border-color: var(--color-navyblue); - cursor: pointer; - transition: all 0.3s ease-in-out; -} - -.image-requirements { - font-size: 0.9rem; - color: var(--color-black-light); -} - -.image-requirements h4 { - margin: 0 0 0.5rem; - font-size: 1rem; - color: var(--color-black-light); -} - -.image-requirements ul { - margin: 0; - padding-left: 1.2rem; -} - -.image-requirements li { - margin-bottom: 0.25rem; -} - -.role-selection { - margin-top: 1rem; -} - -.role-selection p { - margin: 0 0 0.5rem; - font-size: 1rem; - font-weight: 500; - color: var(--color-black); -} - -.role-buttons { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -.role-button { - padding: 0.5rem 1rem; - border: 2px solid var(--profile-field-input-border-clr); - background-color: var(--color-white); - border-radius: 6px; - cursor: pointer; - font-size: 0.9rem; -} - -.role-button:hover { - border-color: var(--color-navyblue); - background-color: var(--color-offwhite); - transition: all 0.3s ease; -} - -.role-button--selected { - border-color: var(--color-navyblue); - background-color: var(--color-navyblue); - color: var(--color-white); -} - -.role-button--selected:hover { - background-color: var(--color-navyblue); -} - -@media screen and (width <= 768px) { - .two-column-layout { - grid-template-columns: 1fr; - } - - .form-header { - width: 87.5%; - } - - .stepper__form { - max-width: 85%; - } - - .stepper__buttons { - width: 87.5%; - } - - .stepper-welcome { - margin: 1rem auto 3rem; - padding: 1.5rem; - } - - .stepper-welcome img { - width: 100px; - } - - .terms-modal__content { - width: 95vw; - margin: 1rem; - } -} - -@media (width <= 480px) { - .form-header { - width: 97.5%; - } - - .stepper__form { - max-width: 95%; - } - - .stepper__buttons { - width: 97.5%; - } - - .stepper-welcome { - margin: 0.5rem auto 3rem; - padding: 1rem; - } - - .stepper-welcome img { - width: 80px; - } - - .stepper-welcome__checkbox-text { - font-size: 0.9rem; - } -} From 2a697eaa00d09dfa59e534f18163f0cc1a8517a8 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Sat, 8 Nov 2025 20:33:18 +0530 Subject: [PATCH 11/25] refactor: use try catch in local storage actions --- app/components/new-join-steps/base-step.js | 9 +++++---- app/components/new-stepper.js | 9 +++++---- app/utils/storage.js | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 app/utils/storage.js diff --git a/app/components/new-join-steps/base-step.js b/app/components/new-join-steps/base-step.js index a6935cae..5e8e1a49 100644 --- a/app/components/new-join-steps/base-step.js +++ b/app/components/new-join-steps/base-step.js @@ -5,6 +5,7 @@ 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 = {}; @@ -25,7 +26,7 @@ export default class BaseStepComponent extends Component { } initializeFormState() { - const saved = JSON.parse(localStorage.getItem(this.storageKey) || '{}'); + const saved = JSON.parse(getLocalStorageItem(this.storageKey, '{}')); this.data = saved ?? {}; this.errorMessage = Object.fromEntries( @@ -43,7 +44,7 @@ export default class BaseStepComponent extends Component { const valid = this.isDataValid(); this.args.setIsPreValid(valid); - localStorage.setItem('isValid', valid); + setLocalStorageItem('isValid', String(valid)); } @action inputHandler(e) { @@ -76,7 +77,7 @@ export default class BaseStepComponent extends Component { updateFieldValue(field, value) { this.data = { ...this.data, [field]: value }; - localStorage.setItem(this.storageKey, JSON.stringify(this.data)); + setLocalStorageItem(this.storageKey, JSON.stringify(this.data)); } updateWordCount(field, result) { @@ -103,6 +104,6 @@ export default class BaseStepComponent extends Component { syncFormValidity() { const allValid = this.isDataValid(); this.args.setIsValid(allValid); - localStorage.setItem('isValid', allValid); + setLocalStorageItem('isValid', String(allValid)); } } diff --git a/app/components/new-stepper.js b/app/components/new-stepper.js index e957b389..c65366d6 100644 --- a/app/components/new-stepper.js +++ b/app/components/new-stepper.js @@ -3,6 +3,7 @@ import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { inject as service } from '@ember/service'; import { NEW_FORM_STEPS } from '../constants/new-join-form'; +import { getLocalStorageItem, setLocalStorageItem } from '../utils/storage'; export default class NewStepperComponent extends Component { MIN_STEP = 0; @@ -14,10 +15,10 @@ export default class NewStepperComponent extends Component { @service joinApplicationTerms; @tracked currentStep = - Number(localStorage.getItem('currentStep') ?? this.args.step) || 0; + Number(getLocalStorageItem('currentStep') ?? this.args.step) || 0; @tracked preValid = false; - @tracked isValid = localStorage.getItem('isValid') === 'true'; + @tracked isValid = getLocalStorageItem('isValid') === 'true'; setIsValid = (newVal) => (this.isValid = newVal); setIsPreValid = (newVal) => (this.preValid = newVal); @@ -48,7 +49,7 @@ export default class NewStepperComponent extends Component { @action incrementStep() { if (this.currentStep < this.MAX_STEP) { const nextStep = this.currentStep + 1; - localStorage.setItem('currentStep', String(nextStep)); + setLocalStorageItem('currentStep', String(nextStep)); this.currentStep = nextStep; this.updateQueryParam(nextStep); } @@ -57,7 +58,7 @@ export default class NewStepperComponent extends Component { @action decrementStep() { if (this.currentStep > this.MIN_STEP) { const previousStep = this.currentStep - 1; - localStorage.setItem('currentStep', String(previousStep)); + setLocalStorageItem('currentStep', String(previousStep)); this.currentStep = previousStep; this.updateQueryParam(previousStep); } diff --git a/app/utils/storage.js b/app/utils/storage.js new file mode 100644 index 00000000..31a65c78 --- /dev/null +++ b/app/utils/storage.js @@ -0,0 +1,18 @@ +export function getLocalStorageItem(key, defaultValue = null) { + try { + return localStorage.getItem(key) ?? defaultValue; + } catch (error) { + console.warn(`Failed to get localStorage item "${key}":`, error); + return defaultValue; + } +} + +export function setLocalStorageItem(key, value) { + try { + localStorage.setItem(key, value); + return true; + } catch (error) { + console.warn(`Failed to set localStorage item "${key}":`, error); + return false; + } +} From d656d030ab05f2b2c25d422f8879274a657c3716 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Sat, 8 Nov 2025 20:41:03 +0530 Subject: [PATCH 12/25] refactor: improve accessibility in new step one --- .../new-join-steps/new-step-one.hbs | 95 +++++-------------- 1 file changed, 25 insertions(+), 70 deletions(-) diff --git a/app/components/new-join-steps/new-step-one.hbs b/app/components/new-join-steps/new-step-one.hbs index c175fcf2..2bb38f03 100644 --- a/app/components/new-join-steps/new-step-one.hbs +++ b/app/components/new-join-steps/new-step-one.hbs @@ -2,32 +2,18 @@

      Profile Picture

      {{#if this.imagePreview}} -
      - Profile preview - -
      +
      + Profile preview + +
      {{else}} - + {{/if}} - +

      Image Requirements:

        @@ -42,60 +28,29 @@

        Personal Details

        -

        Please provide correct details and choose your role carefully, it won't be changed later.

        +

        Please provide correct details and choose your role carefully, it won't be changed + later.

        - + - + - + - +

        Applying as

        -
        +
        {{#each this.roleOptions as |role|}} - + {{/each}}
        From 3a70ecd82ea1e0fd9041bd3f2583e4f740e6d98e Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Tue, 4 Nov 2025 05:09:22 +0530 Subject: [PATCH 13/25] feat: add new step two for introduction --- .../new-join-steps/new-step-two.hbs | 48 ++++++++ app/components/new-join-steps/new-step-two.js | 108 ++++++++++++++++++ app/components/new-stepper.hbs | 7 ++ app/constants/new-join-form.js | 11 +- 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 app/components/new-join-steps/new-step-two.hbs create mode 100644 app/components/new-join-steps/new-step-two.js diff --git a/app/components/new-join-steps/new-step-two.hbs b/app/components/new-join-steps/new-step-two.hbs new file mode 100644 index 00000000..b91320a1 --- /dev/null +++ b/app/components/new-join-steps/new-step-two.hbs @@ -0,0 +1,48 @@ +
        +
        +

        {{this.heading}}

        +

        {{this.subHeading}}

        +
        + + + {{#if this.errorMessage.skills}} +
        {{this.errorMessage.skills}}
        + {{/if}} + + + {{#if this.errorMessage.company}} +
        {{this.errorMessage.company}}
        + {{/if}} + + + +
        0/500 words
        + {{!--

        Share your background, experiences, and what drives you (100-500 words)

        --}} + + {{#if this.errorMessage.introduction}} +
        {{this.errorMessage.introduction}}
        + {{/if}} +
        \ No newline at end of file diff --git a/app/components/new-join-steps/new-step-two.js b/app/components/new-join-steps/new-step-two.js new file mode 100644 index 00000000..fc5a8720 --- /dev/null +++ b/app/components/new-join-steps/new-step-two.js @@ -0,0 +1,108 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { validator } from '../../utils/validator'; +import { debounce } from '@ember/runloop'; +import { JOIN_DEBOUNCE_TIME } from '../../constants/join'; +import { NEW_FORM_STEPS, NEW_STEP_LIMITS } from '../../constants/new-join-form'; + +export default class NewStepTwoComponent extends Component { + @tracked data = JSON.parse(localStorage.getItem('newStepTwoData')) ?? { + skills: '', + company: '', + introduction: '', + }; + + @tracked errorMessage = { + skills: '', + company: '', + introduction: '', + }; + + heading = NEW_FORM_STEPS.headings[this.currentStep - 1]; + subHeading = NEW_FORM_STEPS.subheadings[this.currentStep - 1]; + + isValid; + setIsValid; + setIsPreValid; + + constructor(...args) { + super(...args); + this.isValid = this.args.isValid; + this.setIsValid = this.args.setIsValid; + this.setIsPreValid = this.args.setIsPreValid; + const validated = this.isDataValid(); + localStorage.setItem('isValid', validated); + this.setIsPreValid(validated); + } + + isDataValid() { + for (let field in this.data) { + if (field === 'introduction') { + const wordCount = this.data[field].trim().split(/\s+/).length; + if ( + wordCount < NEW_STEP_LIMITS.stepTwo.introduction.min || + wordCount > NEW_STEP_LIMITS.stepTwo.introduction.max + ) { + return false; + } + } else { + const { isValid } = validator( + this.data[field], + NEW_STEP_LIMITS.stepTwo[field], + ); + if (!isValid) { + return false; + } + } + } + return true; + } + + @action inputHandler(e) { + this.setIsPreValid(false); + + const setValToLocalStorage = () => { + this.data = { ...this.data, [e.target.name]: e.target.value }; + localStorage.setItem('newStepTwoData', JSON.stringify(this.data)); + + const field = e.target.name; + if (field === 'introduction') { + const wordCount = this.data[field].trim().split(/\s+/).length; + if (wordCount < NEW_STEP_LIMITS.stepTwo.introduction.min) { + this.errorMessage = { + ...this.errorMessage, + [field]: `At least ${NEW_STEP_LIMITS.stepTwo.introduction.min - wordCount} more word(s) required`, + }; + } else if (wordCount > NEW_STEP_LIMITS.stepTwo.introduction.max) { + this.errorMessage = { + ...this.errorMessage, + [field]: `Maximum ${NEW_STEP_LIMITS.stepTwo.introduction.max} words allowed`, + }; + } else { + this.errorMessage = { + ...this.errorMessage, + [field]: '', + }; + } + } else { + const { isValid, remainingWords } = validator( + this.data[field], + NEW_STEP_LIMITS.stepTwo[field], + ); + this.errorMessage = { + ...this.errorMessage, + [field]: isValid + ? '' + : `At least, ${remainingWords} more word(s) required`, + }; + } + + const isAllValid = this.isDataValid(); + this.setIsValid(isAllValid); + localStorage.setItem('isValid', isAllValid); + }; + + debounce(this.data, setValToLocalStorage, JOIN_DEBOUNCE_TIME); + } +} diff --git a/app/components/new-stepper.hbs b/app/components/new-stepper.hbs index 9c6ae4de..594581df 100644 --- a/app/components/new-stepper.hbs +++ b/app/components/new-stepper.hbs @@ -30,6 +30,13 @@ @isValid={{this.isValid}} @setIsValid={{this.setIsValid}} /> + + {{else if (eq this.currentStep 2)}} + {{/if}} diff --git a/app/constants/new-join-form.js b/app/constants/new-join-form.js index 6911a135..dd38e2d4 100644 --- a/app/constants/new-join-form.js +++ b/app/constants/new-join-form.js @@ -1,7 +1,11 @@ export const NEW_FORM_STEPS = { - headings: ['Upload Professional Headshot and Complete Personal Details'], + headings: [ + 'Upload Professional Headshot and Complete Personal Details', + 'More personal details please', + ], subheadings: [ 'Please provide accurate information for verification purposes.', + 'Introduce and help us get to know you better', ], }; @@ -21,6 +25,11 @@ export const NEW_STEP_LIMITS = { city: { min: 1 }, role: { min: 1 }, }, + stepTwo: { + skills: 5, + company: 1, + introduction: { min: 100, max: 500 }, + }, }; export const STEP_DATA_STORAGE_KEY = { From 4e9fe8a3b0d6d3ba6945a37a092002589916ba5b Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Tue, 4 Nov 2025 15:09:39 +0530 Subject: [PATCH 14/25] feat: add step three for interests --- .../new-join-steps/new-step-three.hbs | 38 ++++++++ .../new-join-steps/new-step-three.js | 90 +++++++++++++++++++ .../new-join-steps/new-step-two.hbs | 86 +++++++++--------- app/components/new-join-steps/new-step-two.js | 62 +++++++------ app/components/new-stepper.hbs | 7 ++ app/constants/new-join-form.js | 6 ++ 6 files changed, 220 insertions(+), 69 deletions(-) create mode 100644 app/components/new-join-steps/new-step-three.hbs create mode 100644 app/components/new-join-steps/new-step-three.js diff --git a/app/components/new-join-steps/new-step-three.hbs b/app/components/new-join-steps/new-step-three.hbs new file mode 100644 index 00000000..8ab34a1c --- /dev/null +++ b/app/components/new-join-steps/new-step-three.hbs @@ -0,0 +1,38 @@ +
        +

        {{this.heading}}

        +

        {{this.subHeading}}

        +
        + +
        +
        +

        What do you do for fun? Your hobbies/interests?

        + +
        {{this.wordCount.hobbies}}/{{this.maxWords.hobbies}} words
        + {{#if this.errorMessage.hobbies}} +
        {{this.errorMessage.hobbies}}
        + {{/if}} +
        + +
        +

        Fun fact about you

        + +
        {{this.wordCount.funFact}}/{{this.maxWords.funFact}} words
        + {{#if this.errorMessage.funFact}} +
        {{this.errorMessage.funFact}}
        + {{/if}} +
        +
        \ No newline at end of file diff --git a/app/components/new-join-steps/new-step-three.js b/app/components/new-join-steps/new-step-three.js new file mode 100644 index 00000000..e8f57710 --- /dev/null +++ b/app/components/new-join-steps/new-step-three.js @@ -0,0 +1,90 @@ +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 { NEW_FORM_STEPS, NEW_STEP_LIMITS } from '../../constants/new-join-form'; +import { validateWordCount } from '../../utils/validator'; + +export default class NewStepThreeComponent extends Component { + @tracked data = JSON.parse(localStorage.getItem('newStepThreeData')) ?? { + hobbies: '', + funFact: '', + }; + + @tracked errorMessage = { + hobbies: '', + funFact: '', + }; + + @tracked wordCount = { + hobbies: + this?.data?.hobbies?.trim()?.split(/\s+/).filter(Boolean).length || 0, + funFact: + this?.data?.funFact?.trim()?.split(/\s+/).filter(Boolean).length || 0, + }; + + maxWords = { + hobbies: NEW_STEP_LIMITS.stepThree.hobbies.max, + funFact: NEW_STEP_LIMITS.stepThree.funFact.max, + }; + + isValid; + setIsValid; + setIsPreValid; + + heading = NEW_FORM_STEPS.headings[2]; + subHeading = NEW_FORM_STEPS.subheadings[2]; + + constructor(...args) { + super(...args); + this.isValid = this.args.isValid; + this.setIsValid = this.args.setIsValid; + this.setIsPreValid = this.args.setIsPreValid; + const validated = this.isDataValid(); + localStorage.setItem('isValid', validated); + this.setIsPreValid(validated); + } + + isDataValid() { + for (let field in this.data) { + const { isValid } = validateWordCount( + this.data[field], + NEW_STEP_LIMITS.stepThree[field], + ); + if (!isValid) return false; + } + return true; + } + + @action inputHandler(e) { + this.setIsPreValid(false); + + const setValToLocalStorage = () => { + this.data = { ...this.data, [e.target.name]: e.target.value }; + localStorage.setItem('newStepThreeData', JSON.stringify(this.data)); + + // Only validate the changed field + const field = e.target.name; + const { isValid, wordCount, remainingToMin } = validateWordCount( + this.data[field], + NEW_STEP_LIMITS.stepThree[field], + ); + this.wordCount = { ...this.wordCount, [field]: wordCount }; + this.errorMessage = { + ...this.errorMessage, + [field]: isValid + ? '' + : remainingToMin + ? `At least, ${remainingToMin} more word(s) required` + : `Maximum ${NEW_STEP_LIMITS.stepThree[field].max} words allowed`, + }; + + const isAllValid = this.isDataValid(); + this.setIsValid(isAllValid); + localStorage.setItem('isValid', isAllValid); + }; + + debounce(this.data, setValToLocalStorage, JOIN_DEBOUNCE_TIME); + } +} diff --git a/app/components/new-join-steps/new-step-two.hbs b/app/components/new-join-steps/new-step-two.hbs index b91320a1..629b9d3f 100644 --- a/app/components/new-join-steps/new-step-two.hbs +++ b/app/components/new-join-steps/new-step-two.hbs @@ -1,48 +1,54 @@
        -
        -

        {{this.heading}}

        -

        {{this.subHeading}}

        -
        - - - {{#if this.errorMessage.skills}} -
        {{this.errorMessage.skills}}
        - {{/if}} +
        +

        {{this.heading}}

        +

        {{this.subHeading}}

        +
        - - {{#if this.errorMessage.company}} -
        {{this.errorMessage.company}}
        - {{/if}} +
        +
        + + {{#if this.errorMessage.skills}} +
        {{this.errorMessage.skills}}
        + {{/if}} +
        - + + /> + {{#if this.errorMessage.company}} +
        {{this.errorMessage.company}}
        + {{/if}} +
        +
        -
        0/500 words
        - {{!--

        Share your background, experiences, and what drives you (100-500 words)

        --}} +

        Please introduce yourself

        + +
        {{this.wordCount.introduction}}/{{this.maxWords.introduction}} words
        +

        Share your background, experiences, and what drives you.

        - {{#if this.errorMessage.introduction}} -
        {{this.errorMessage.introduction}}
        - {{/if}} + {{#if this.errorMessage.introduction}} +
        {{this.errorMessage.introduction}}
        + {{/if}}
        \ No newline at end of file diff --git a/app/components/new-join-steps/new-step-two.js b/app/components/new-join-steps/new-step-two.js index fc5a8720..3f72b711 100644 --- a/app/components/new-join-steps/new-step-two.js +++ b/app/components/new-join-steps/new-step-two.js @@ -1,10 +1,10 @@ -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; -import { validator } from '../../utils/validator'; import { debounce } from '@ember/runloop'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; import { JOIN_DEBOUNCE_TIME } from '../../constants/join'; import { NEW_FORM_STEPS, NEW_STEP_LIMITS } from '../../constants/new-join-form'; +import { validateWordCount, validator } from '../../utils/validator'; export default class NewStepTwoComponent extends Component { @tracked data = JSON.parse(localStorage.getItem('newStepTwoData')) ?? { @@ -19,8 +19,18 @@ export default class NewStepTwoComponent extends Component { introduction: '', }; - heading = NEW_FORM_STEPS.headings[this.currentStep - 1]; - subHeading = NEW_FORM_STEPS.subheadings[this.currentStep - 1]; + @tracked wordCount = { + introduction: + this?.data?.introduction?.trim()?.split(/\s+/).filter(Boolean).length || + 0, + }; + + maxWords = { + introduction: NEW_STEP_LIMITS.stepTwo.introduction.max, + }; + + heading = NEW_FORM_STEPS.headings[1]; + subHeading = NEW_FORM_STEPS.subheadings[1]; isValid; setIsValid; @@ -39,13 +49,11 @@ export default class NewStepTwoComponent extends Component { isDataValid() { for (let field in this.data) { if (field === 'introduction') { - const wordCount = this.data[field].trim().split(/\s+/).length; - if ( - wordCount < NEW_STEP_LIMITS.stepTwo.introduction.min || - wordCount > NEW_STEP_LIMITS.stepTwo.introduction.max - ) { - return false; - } + const { isValid } = validateWordCount( + this.data[field], + NEW_STEP_LIMITS.stepTwo.introduction, + ); + if (!isValid) return false; } else { const { isValid } = validator( this.data[field], @@ -68,23 +76,19 @@ export default class NewStepTwoComponent extends Component { const field = e.target.name; if (field === 'introduction') { - const wordCount = this.data[field].trim().split(/\s+/).length; - if (wordCount < NEW_STEP_LIMITS.stepTwo.introduction.min) { - this.errorMessage = { - ...this.errorMessage, - [field]: `At least ${NEW_STEP_LIMITS.stepTwo.introduction.min - wordCount} more word(s) required`, - }; - } else if (wordCount > NEW_STEP_LIMITS.stepTwo.introduction.max) { - this.errorMessage = { - ...this.errorMessage, - [field]: `Maximum ${NEW_STEP_LIMITS.stepTwo.introduction.max} words allowed`, - }; - } else { - this.errorMessage = { - ...this.errorMessage, - [field]: '', - }; - } + const { isValid, wordCount, remainingToMin } = validateWordCount( + this.data[field], + NEW_STEP_LIMITS.stepTwo.introduction, + ); + this.wordCount = { ...this.wordCount, introduction: wordCount }; + this.errorMessage = { + ...this.errorMessage, + [field]: isValid + ? '' + : remainingToMin + ? `At least ${remainingToMin} more word(s) required` + : `Maximum ${NEW_STEP_LIMITS.stepTwo.introduction.max} words allowed`, + }; } else { const { isValid, remainingWords } = validator( this.data[field], diff --git a/app/components/new-stepper.hbs b/app/components/new-stepper.hbs index 594581df..d40869ca 100644 --- a/app/components/new-stepper.hbs +++ b/app/components/new-stepper.hbs @@ -37,6 +37,13 @@ @isValid={{this.isValid}} @setIsValid={{this.setIsValid}} /> + + {{else if (eq this.currentStep 3)}} + {{/if}} diff --git a/app/constants/new-join-form.js b/app/constants/new-join-form.js index dd38e2d4..3b273c10 100644 --- a/app/constants/new-join-form.js +++ b/app/constants/new-join-form.js @@ -2,10 +2,12 @@ export const NEW_FORM_STEPS = { headings: [ 'Upload Professional Headshot and Complete Personal Details', 'More personal details please', + 'Your hobbies, interests, fun fact', ], subheadings: [ 'Please provide accurate information for verification purposes.', 'Introduce and help us get to know you better', + 'Show us your funny and interesting side', ], }; @@ -30,6 +32,10 @@ export const NEW_STEP_LIMITS = { company: 1, introduction: { min: 100, max: 500 }, }, + stepThree: { + hobbies: { min: 100, max: 500 }, + funFact: { min: 100, max: 500 }, + }, }; export const STEP_DATA_STORAGE_KEY = { From 99ed2b6fd5ce4e049796b5c77b132ae1759eb366 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Tue, 4 Nov 2025 18:17:11 +0530 Subject: [PATCH 15/25] feat: add base step class to handle input validation and local storage sync --- .../new-join-steps/new-step-three.hbs | 4 +- .../new-join-steps/new-step-three.js | 90 ++----------- .../new-join-steps/new-step-two.hbs | 4 +- app/components/new-join-steps/new-step-two.js | 118 ++---------------- app/components/new-stepper.hbs | 4 + app/constants/new-join-form.js | 3 +- 6 files changed, 25 insertions(+), 198 deletions(-) diff --git a/app/components/new-join-steps/new-step-three.hbs b/app/components/new-join-steps/new-step-three.hbs index 8ab34a1c..43d8c901 100644 --- a/app/components/new-join-steps/new-step-three.hbs +++ b/app/components/new-join-steps/new-step-three.hbs @@ -1,6 +1,6 @@
        -

        {{this.heading}}

        -

        {{this.subHeading}}

        +

        {{@heading}}

        +

        {{@subHeading}}

        diff --git a/app/components/new-join-steps/new-step-three.js b/app/components/new-join-steps/new-step-three.js index e8f57710..9b88406f 100644 --- a/app/components/new-join-steps/new-step-three.js +++ b/app/components/new-join-steps/new-step-three.js @@ -1,90 +1,14 @@ -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 { NEW_FORM_STEPS, NEW_STEP_LIMITS } from '../../constants/new-join-form'; -import { validateWordCount } from '../../utils/validator'; +import BaseStepComponent from './base-step'; +import { NEW_STEP_LIMITS } from '../../constants/new-join-form'; -export default class NewStepThreeComponent extends Component { - @tracked data = JSON.parse(localStorage.getItem('newStepThreeData')) ?? { - hobbies: '', - funFact: '', +export default class NewStepThreeComponent extends BaseStepComponent { + storageKey = 'newStepThreeData'; + validationMap = { + hobbies: NEW_STEP_LIMITS.stepThree.hobbies, + funFact: NEW_STEP_LIMITS.stepThree.funFact, }; - - @tracked errorMessage = { - hobbies: '', - funFact: '', - }; - - @tracked wordCount = { - hobbies: - this?.data?.hobbies?.trim()?.split(/\s+/).filter(Boolean).length || 0, - funFact: - this?.data?.funFact?.trim()?.split(/\s+/).filter(Boolean).length || 0, - }; - maxWords = { hobbies: NEW_STEP_LIMITS.stepThree.hobbies.max, funFact: NEW_STEP_LIMITS.stepThree.funFact.max, }; - - isValid; - setIsValid; - setIsPreValid; - - heading = NEW_FORM_STEPS.headings[2]; - subHeading = NEW_FORM_STEPS.subheadings[2]; - - constructor(...args) { - super(...args); - this.isValid = this.args.isValid; - this.setIsValid = this.args.setIsValid; - this.setIsPreValid = this.args.setIsPreValid; - const validated = this.isDataValid(); - localStorage.setItem('isValid', validated); - this.setIsPreValid(validated); - } - - isDataValid() { - for (let field in this.data) { - const { isValid } = validateWordCount( - this.data[field], - NEW_STEP_LIMITS.stepThree[field], - ); - if (!isValid) return false; - } - return true; - } - - @action inputHandler(e) { - this.setIsPreValid(false); - - const setValToLocalStorage = () => { - this.data = { ...this.data, [e.target.name]: e.target.value }; - localStorage.setItem('newStepThreeData', JSON.stringify(this.data)); - - // Only validate the changed field - const field = e.target.name; - const { isValid, wordCount, remainingToMin } = validateWordCount( - this.data[field], - NEW_STEP_LIMITS.stepThree[field], - ); - this.wordCount = { ...this.wordCount, [field]: wordCount }; - this.errorMessage = { - ...this.errorMessage, - [field]: isValid - ? '' - : remainingToMin - ? `At least, ${remainingToMin} more word(s) required` - : `Maximum ${NEW_STEP_LIMITS.stepThree[field].max} words allowed`, - }; - - const isAllValid = this.isDataValid(); - this.setIsValid(isAllValid); - localStorage.setItem('isValid', isAllValid); - }; - - debounce(this.data, setValToLocalStorage, JOIN_DEBOUNCE_TIME); - } } diff --git a/app/components/new-join-steps/new-step-two.hbs b/app/components/new-join-steps/new-step-two.hbs index 629b9d3f..4d947d99 100644 --- a/app/components/new-join-steps/new-step-two.hbs +++ b/app/components/new-join-steps/new-step-two.hbs @@ -1,7 +1,7 @@
        -

        {{this.heading}}

        -

        {{this.subHeading}}

        +

        {{@heading}}

        +

        {{@subHeading}}

        diff --git a/app/components/new-join-steps/new-step-two.js b/app/components/new-join-steps/new-step-two.js index 3f72b711..813ed5a4 100644 --- a/app/components/new-join-steps/new-step-two.js +++ b/app/components/new-join-steps/new-step-two.js @@ -1,112 +1,12 @@ -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 { NEW_FORM_STEPS, NEW_STEP_LIMITS } from '../../constants/new-join-form'; -import { validateWordCount, validator } from '../../utils/validator'; +import BaseStepComponent from './base-step'; +import { NEW_STEP_LIMITS } from '../../constants/new-join-form'; -export default class NewStepTwoComponent extends Component { - @tracked data = JSON.parse(localStorage.getItem('newStepTwoData')) ?? { - skills: '', - company: '', - introduction: '', +export default class NewStepTwoComponent extends BaseStepComponent { + storageKey = 'newStepTwoData'; + validationMap = { + skills: NEW_STEP_LIMITS.stepTwo.skills, + company: NEW_STEP_LIMITS.stepTwo.company, + introduction: NEW_STEP_LIMITS.stepTwo.introduction, }; - - @tracked errorMessage = { - skills: '', - company: '', - introduction: '', - }; - - @tracked wordCount = { - introduction: - this?.data?.introduction?.trim()?.split(/\s+/).filter(Boolean).length || - 0, - }; - - maxWords = { - introduction: NEW_STEP_LIMITS.stepTwo.introduction.max, - }; - - heading = NEW_FORM_STEPS.headings[1]; - subHeading = NEW_FORM_STEPS.subheadings[1]; - - isValid; - setIsValid; - setIsPreValid; - - constructor(...args) { - super(...args); - this.isValid = this.args.isValid; - this.setIsValid = this.args.setIsValid; - this.setIsPreValid = this.args.setIsPreValid; - const validated = this.isDataValid(); - localStorage.setItem('isValid', validated); - this.setIsPreValid(validated); - } - - isDataValid() { - for (let field in this.data) { - if (field === 'introduction') { - const { isValid } = validateWordCount( - this.data[field], - NEW_STEP_LIMITS.stepTwo.introduction, - ); - if (!isValid) return false; - } else { - const { isValid } = validator( - this.data[field], - NEW_STEP_LIMITS.stepTwo[field], - ); - if (!isValid) { - return false; - } - } - } - return true; - } - - @action inputHandler(e) { - this.setIsPreValid(false); - - const setValToLocalStorage = () => { - this.data = { ...this.data, [e.target.name]: e.target.value }; - localStorage.setItem('newStepTwoData', JSON.stringify(this.data)); - - const field = e.target.name; - if (field === 'introduction') { - const { isValid, wordCount, remainingToMin } = validateWordCount( - this.data[field], - NEW_STEP_LIMITS.stepTwo.introduction, - ); - this.wordCount = { ...this.wordCount, introduction: wordCount }; - this.errorMessage = { - ...this.errorMessage, - [field]: isValid - ? '' - : remainingToMin - ? `At least ${remainingToMin} more word(s) required` - : `Maximum ${NEW_STEP_LIMITS.stepTwo.introduction.max} words allowed`, - }; - } else { - const { isValid, remainingWords } = validator( - this.data[field], - NEW_STEP_LIMITS.stepTwo[field], - ); - this.errorMessage = { - ...this.errorMessage, - [field]: isValid - ? '' - : `At least, ${remainingWords} more word(s) required`, - }; - } - - const isAllValid = this.isDataValid(); - this.setIsValid(isAllValid); - localStorage.setItem('isValid', isAllValid); - }; - - debounce(this.data, setValToLocalStorage, JOIN_DEBOUNCE_TIME); - } + maxWords = { introduction: NEW_STEP_LIMITS.stepTwo.introduction.max }; } diff --git a/app/components/new-stepper.hbs b/app/components/new-stepper.hbs index d40869ca..ecb753fd 100644 --- a/app/components/new-stepper.hbs +++ b/app/components/new-stepper.hbs @@ -36,6 +36,8 @@ @setIsPreValid={{this.setIsPreValid}} @isValid={{this.isValid}} @setIsValid={{this.setIsValid}} + @heading={{this.currentHeading}} + @subHeading={{this.currentSubheading}} /> {{else if (eq this.currentStep 3)}} @@ -43,6 +45,8 @@ @setIsPreValid={{this.setIsPreValid}} @isValid={{this.isValid}} @setIsValid={{this.setIsValid}} + @heading={{this.currentHeading}} + @subHeading={{this.currentSubheading}} /> {{/if}} diff --git a/app/constants/new-join-form.js b/app/constants/new-join-form.js index 3b273c10..49b22e31 100644 --- a/app/constants/new-join-form.js +++ b/app/constants/new-join-form.js @@ -28,8 +28,7 @@ export const NEW_STEP_LIMITS = { role: { min: 1 }, }, stepTwo: { - skills: 5, - company: 1, + skills: { min: 5, max: 20 }, introduction: { min: 100, max: 500 }, }, stepThree: { From 2478f760445e270b3982450af9463bc90530a886 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Tue, 4 Nov 2025 21:00:03 +0530 Subject: [PATCH 16/25] fix: postIntialization in new step one --- app/constants/new-join-form.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/constants/new-join-form.js b/app/constants/new-join-form.js index 49b22e31..1fab4739 100644 --- a/app/constants/new-join-form.js +++ b/app/constants/new-join-form.js @@ -29,6 +29,7 @@ export const NEW_STEP_LIMITS = { }, stepTwo: { skills: { min: 5, max: 20 }, + company: { min: 1 }, introduction: { min: 100, max: 500 }, }, stepThree: { From 93969aa479dacd5860a56674793a5dae17ed38a2 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Wed, 5 Nov 2025 18:40:25 +0530 Subject: [PATCH 17/25] fix: grid styling for step two and three --- .../new-join-steps/new-step-three.hbs | 68 +++++++++--------- .../new-join-steps/new-step-two.hbs | 69 +++++++------------ 2 files changed, 60 insertions(+), 77 deletions(-) diff --git a/app/components/new-join-steps/new-step-three.hbs b/app/components/new-join-steps/new-step-three.hbs index 43d8c901..4e6ecfad 100644 --- a/app/components/new-join-steps/new-step-three.hbs +++ b/app/components/new-join-steps/new-step-three.hbs @@ -1,38 +1,38 @@ -
        -

        {{@heading}}

        -

        {{@subHeading}}

        -
        - -
        -
        -

        What do you do for fun? Your hobbies/interests?

        - -
        {{this.wordCount.hobbies}}/{{this.maxWords.hobbies}} words
        - {{#if this.errorMessage.hobbies}} -
        {{this.errorMessage.hobbies}}
        - {{/if}} +
        +
        +

        {{@heading}}

        +

        {{@subHeading}}

        -
        -

        Fun fact about you

        - -
        {{this.wordCount.funFact}}/{{this.maxWords.funFact}} words
        - {{#if this.errorMessage.funFact}} -
        {{this.errorMessage.funFact}}
        - {{/if}} +
        +
        + +
        {{this.wordCount.hobbies}}/{{this.maxWords.hobbies}} words
        + {{#if this.errorMessage.hobbies}} +
        {{this.errorMessage.hobbies}}
        + {{/if}} +
        + +
        + +
        {{this.wordCount.funFact}}/{{this.maxWords.funFact}} words
        + {{#if this.errorMessage.funFact}} +
        {{this.errorMessage.funFact}}
        + {{/if}} +
        \ No newline at end of file diff --git a/app/components/new-join-steps/new-step-two.hbs b/app/components/new-join-steps/new-step-two.hbs index 4d947d99..451601e5 100644 --- a/app/components/new-join-steps/new-step-two.hbs +++ b/app/components/new-join-steps/new-step-two.hbs @@ -1,54 +1,37 @@
        -

        {{@heading}}

        -

        {{@subHeading}}

        +

        {{@heading}}

        +

        {{@subHeading}}

        -
        -
        - - {{#if this.errorMessage.skills}} +
        +
        +
        + + {{#if this.errorMessage.skills}}
        {{this.errorMessage.skills}}
        - {{/if}} -
        + {{/if}} +
        -
        - - {{#if this.errorMessage.company}} +
        + + {{#if this.errorMessage.company}}
        {{this.errorMessage.company}}
        + {{/if}} +
        +
        +
        + +
        {{this.wordCount.introduction}}/{{this.maxWords.introduction}} words
        + + {{#if this.errorMessage.introduction}} +
        {{this.errorMessage.introduction}}
        {{/if}}
        - -

        Please introduce yourself

        - -
        {{this.wordCount.introduction}}/{{this.maxWords.introduction}} words
        -

        Share your background, experiences, and what drives you.

        - - {{#if this.errorMessage.introduction}} -
        {{this.errorMessage.introduction}}
        - {{/if}}
        \ No newline at end of file From fe14c9285c7c33bbcf3fb128a6119ea7d65721ac Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Fri, 7 Nov 2025 17:48:05 +0530 Subject: [PATCH 18/25] refactor: remove redundant max words --- app/components/new-join-steps/new-step-three.hbs | 12 ++++++------ app/components/new-join-steps/new-step-three.js | 4 ---- app/components/new-join-steps/new-step-two.hbs | 12 ++++++------ app/components/new-join-steps/new-step-two.js | 1 - 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/app/components/new-join-steps/new-step-three.hbs b/app/components/new-join-steps/new-step-three.hbs index 4e6ecfad..dc47323e 100644 --- a/app/components/new-join-steps/new-step-three.hbs +++ b/app/components/new-join-steps/new-step-three.hbs @@ -1,7 +1,7 @@
        -

        {{@heading}}

        -

        {{@subHeading}}

        +

        {{@heading}}

        +

        {{@subHeading}}

        @@ -14,9 +14,9 @@ @value={{this.data.hobbies}} @onInput={{this.inputHandler}} /> -
        {{this.wordCount.hobbies}}/{{this.maxWords.hobbies}} words
        +
        {{this.wordCount.hobbies}}/{{this.validationMap.hobbies.max}} words
        {{#if this.errorMessage.hobbies}} -
        {{this.errorMessage.hobbies}}
        +
        {{this.errorMessage.hobbies}}
        {{/if}}
        @@ -29,9 +29,9 @@ @value={{this.data.funFact}} @onInput={{this.inputHandler}} /> -
        {{this.wordCount.funFact}}/{{this.maxWords.funFact}} words
        +
        {{this.wordCount.funFact}}/{{this.validationMap.funFact.max}} words
        {{#if this.errorMessage.funFact}} -
        {{this.errorMessage.funFact}}
        +
        {{this.errorMessage.funFact}}
        {{/if}}
        diff --git a/app/components/new-join-steps/new-step-three.js b/app/components/new-join-steps/new-step-three.js index 9b88406f..e52570f7 100644 --- a/app/components/new-join-steps/new-step-three.js +++ b/app/components/new-join-steps/new-step-three.js @@ -7,8 +7,4 @@ export default class NewStepThreeComponent extends BaseStepComponent { hobbies: NEW_STEP_LIMITS.stepThree.hobbies, funFact: NEW_STEP_LIMITS.stepThree.funFact, }; - maxWords = { - hobbies: NEW_STEP_LIMITS.stepThree.hobbies.max, - funFact: NEW_STEP_LIMITS.stepThree.funFact.max, - }; } diff --git a/app/components/new-join-steps/new-step-two.hbs b/app/components/new-join-steps/new-step-two.hbs index 451601e5..360b5670 100644 --- a/app/components/new-join-steps/new-step-two.hbs +++ b/app/components/new-join-steps/new-step-two.hbs @@ -1,7 +1,7 @@
        -

        {{@heading}}

        -

        {{@subHeading}}

        +

        {{@heading}}

        +

        {{@subHeading}}

        @@ -10,7 +10,7 @@ {{#if this.errorMessage.skills}} -
        {{this.errorMessage.skills}}
        +
        {{this.errorMessage.skills}}
        {{/if}}
        @@ -19,7 +19,7 @@ @placeHolder='Institution or company name' @type='text' @required={{true}} @value={{this.data.company}} @onInput={{this.inputHandler}} /> {{#if this.errorMessage.company}} -
        {{this.errorMessage.company}}
        +
        {{this.errorMessage.company}}
        {{/if}}
        @@ -27,10 +27,10 @@ -
        {{this.wordCount.introduction}}/{{this.maxWords.introduction}} words
        +
        {{this.wordCount.introduction}}/{{this.validationMap.introduction.max}} words
        {{#if this.errorMessage.introduction}} -
        {{this.errorMessage.introduction}}
        +
        {{this.errorMessage.introduction}}
        {{/if}}
        diff --git a/app/components/new-join-steps/new-step-two.js b/app/components/new-join-steps/new-step-two.js index 813ed5a4..67ad7da6 100644 --- a/app/components/new-join-steps/new-step-two.js +++ b/app/components/new-join-steps/new-step-two.js @@ -8,5 +8,4 @@ export default class NewStepTwoComponent extends BaseStepComponent { company: NEW_STEP_LIMITS.stepTwo.company, introduction: NEW_STEP_LIMITS.stepTwo.introduction, }; - maxWords = { introduction: NEW_STEP_LIMITS.stepTwo.introduction.max }; } From 34dfc52b2a8b1c321d8c6f68407a95fcc9a395c9 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Fri, 7 Nov 2025 17:58:43 +0530 Subject: [PATCH 19/25] fix: lint issue and form headings --- app/constants/new-join-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/constants/new-join-form.js b/app/constants/new-join-form.js index 1fab4739..126625cc 100644 --- a/app/constants/new-join-form.js +++ b/app/constants/new-join-form.js @@ -1,7 +1,7 @@ export const NEW_FORM_STEPS = { headings: [ 'Upload Professional Headshot and Complete Personal Details', - 'More personal details please', + 'Additional Personal Information', 'Your hobbies, interests, fun fact', ], subheadings: [ From ee06d8a7afd32f8a742db36f7660b8483932ec2c Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Mon, 10 Nov 2025 01:53:44 +0530 Subject: [PATCH 20/25] refactor: use constant for storage keys --- app/components/new-join-steps/new-step-three.hbs | 4 ++-- app/components/new-join-steps/new-step-three.js | 9 ++++++--- app/components/new-join-steps/new-step-two.hbs | 2 +- app/components/new-join-steps/new-step-two.js | 9 ++++++--- app/constants/new-join-form.js | 2 ++ 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/app/components/new-join-steps/new-step-three.hbs b/app/components/new-join-steps/new-step-three.hbs index dc47323e..7614c45f 100644 --- a/app/components/new-join-steps/new-step-three.hbs +++ b/app/components/new-join-steps/new-step-three.hbs @@ -14,7 +14,7 @@ @value={{this.data.hobbies}} @onInput={{this.inputHandler}} /> -
        {{this.wordCount.hobbies}}/{{this.validationMap.hobbies.max}} words
        +
        {{this.wordCount.hobbies}}/{{this.stepValidation.hobbies.max}} words
        {{#if this.errorMessage.hobbies}}
        {{this.errorMessage.hobbies}}
        {{/if}} @@ -29,7 +29,7 @@ @value={{this.data.funFact}} @onInput={{this.inputHandler}} /> -
        {{this.wordCount.funFact}}/{{this.validationMap.funFact.max}} words
        +
        {{this.wordCount.funFact}}/{{this.stepValidation.funFact.max}} words
        {{#if this.errorMessage.funFact}}
        {{this.errorMessage.funFact}}
        {{/if}} diff --git a/app/components/new-join-steps/new-step-three.js b/app/components/new-join-steps/new-step-three.js index e52570f7..0c3fd6b2 100644 --- a/app/components/new-join-steps/new-step-three.js +++ b/app/components/new-join-steps/new-step-three.js @@ -1,9 +1,12 @@ import BaseStepComponent from './base-step'; -import { NEW_STEP_LIMITS } from '../../constants/new-join-form'; +import { + NEW_STEP_LIMITS, + STEP_DATA_STORAGE_KEY, +} from '../../constants/new-join-form'; export default class NewStepThreeComponent extends BaseStepComponent { - storageKey = 'newStepThreeData'; - validationMap = { + storageKey = STEP_DATA_STORAGE_KEY.stepThree; + stepValidation = { hobbies: NEW_STEP_LIMITS.stepThree.hobbies, funFact: NEW_STEP_LIMITS.stepThree.funFact, }; diff --git a/app/components/new-join-steps/new-step-two.hbs b/app/components/new-join-steps/new-step-two.hbs index 360b5670..c96c5175 100644 --- a/app/components/new-join-steps/new-step-two.hbs +++ b/app/components/new-join-steps/new-step-two.hbs @@ -27,7 +27,7 @@ -
        {{this.wordCount.introduction}}/{{this.validationMap.introduction.max}} words
        +
        {{this.wordCount.introduction}}/{{this.stepValidation.introduction.max}} words
        {{#if this.errorMessage.introduction}}
        {{this.errorMessage.introduction}}
        diff --git a/app/components/new-join-steps/new-step-two.js b/app/components/new-join-steps/new-step-two.js index 67ad7da6..edb38294 100644 --- a/app/components/new-join-steps/new-step-two.js +++ b/app/components/new-join-steps/new-step-two.js @@ -1,9 +1,12 @@ import BaseStepComponent from './base-step'; -import { NEW_STEP_LIMITS } from '../../constants/new-join-form'; +import { + NEW_STEP_LIMITS, + STEP_DATA_STORAGE_KEY, +} from '../../constants/new-join-form'; export default class NewStepTwoComponent extends BaseStepComponent { - storageKey = 'newStepTwoData'; - validationMap = { + storageKey = STEP_DATA_STORAGE_KEY.stepTwo; + stepValidation = { skills: NEW_STEP_LIMITS.stepTwo.skills, company: NEW_STEP_LIMITS.stepTwo.company, introduction: NEW_STEP_LIMITS.stepTwo.introduction, diff --git a/app/constants/new-join-form.js b/app/constants/new-join-form.js index 126625cc..5c1aadcd 100644 --- a/app/constants/new-join-form.js +++ b/app/constants/new-join-form.js @@ -40,4 +40,6 @@ export const NEW_STEP_LIMITS = { export const STEP_DATA_STORAGE_KEY = { stepOne: 'newStepOneData', + stepTwo: 'newStepTwoData', + stepThree: 'newStepThreeData', }; From 459a23c71d0535aebcf00ad0e1a361ecd7381ca2 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Fri, 7 Nov 2025 17:49:16 +0530 Subject: [PATCH 21/25] test: add tests for new step two and three --- .../new-join-steps/new-step-three-test.js | 183 ++++++++++++++++++ .../new-join-steps/new-step-two-test.js | 160 +++++++++++++++ 2 files changed, 343 insertions(+) create mode 100644 tests/integration/components/new-join-steps/new-step-three-test.js create mode 100644 tests/integration/components/new-join-steps/new-step-two-test.js diff --git a/tests/integration/components/new-join-steps/new-step-three-test.js b/tests/integration/components/new-join-steps/new-step-three-test.js new file mode 100644 index 00000000..0d221666 --- /dev/null +++ b/tests/integration/components/new-join-steps/new-step-three-test.js @@ -0,0 +1,183 @@ +import { fillIn, render, waitUntil } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { module, test } from 'qunit'; +import { + NEW_FORM_STEPS, + NEW_STEP_LIMITS, +} from 'website-www/constants/new-join-form'; +import { setupRenderingTest } from 'website-www/tests/helpers'; + +module( + 'Integration | Component | new-join-steps/new-step-three', + function (hooks) { + setupRenderingTest(hooks); + + function generateWords(count) { + return Array(count).fill('word').join(' '); + } + + hooks.beforeEach(function () { + localStorage.removeItem('newStepThreeData'); + localStorage.removeItem('isValid'); + }); + + hooks.afterEach(function () { + localStorage.removeItem('newStepThreeData'); + localStorage.removeItem('isValid'); + }); + + test('it renders step three correctly', async function (assert) { + this.set('setIsPreValid', () => {}); + this.set('setIsValid', () => {}); + this.set('heading', NEW_FORM_STEPS.headings[2]); + this.set('subHeading', NEW_FORM_STEPS.subheadings[2]); + + await render(hbs` + + `); + + assert.dom('[data-test-heading]').hasText(NEW_FORM_STEPS.headings[2]); + assert + .dom('[data-test-sub-heading]') + .hasText(NEW_FORM_STEPS.subheadings[2]); + assert.dom('[data-test-textarea-field][name="hobbies"]').exists(); + assert.dom('[data-test-textarea-field][name="funFact"]').exists(); + assert.dom('[data-test-word-count="hobbies"]').exists(); + assert.dom('[data-test-word-count="funFact"]').exists(); + }); + + test('it validates step three fields and updates word counts', async function (assert) { + this.set('setIsPreValid', () => {}); + this.set('setIsValid', () => {}); + this.set('heading', NEW_FORM_STEPS.headings[2]); + this.set('subHeading', NEW_FORM_STEPS.subheadings[2]); + + await render(hbs` + + `); + + await fillIn( + '[data-test-textarea-field][name="hobbies"]', + generateWords(120), + ); + + await waitUntil(() => + document + .querySelector('[data-test-word-count="hobbies"]') + ?.textContent.includes('120/'), + ); + assert + .dom('[data-test-word-count="hobbies"]') + .hasText(`120/${NEW_STEP_LIMITS.stepThree.hobbies.max} words`); + + await fillIn( + '[data-test-textarea-field][name="funFact"]', + generateWords(130), + ); + + await waitUntil(() => + document + .querySelector('[data-test-word-count="funFact"]') + ?.textContent.includes('130/'), + ); + assert + .dom('[data-test-word-count="funFact"]') + .hasText(`130/${NEW_STEP_LIMITS.stepThree.funFact.max} words`); + + await fillIn( + '[data-test-textarea-field][name="hobbies"]', + generateWords(50), + ); + + await waitUntil(() => + document.querySelector('[data-test-error="hobbies"]'), + ); + assert.dom('[data-test-error="hobbies"]').exists(); + }); + + test('it saves step three data to localStorage and calls callbacks', async function (assert) { + let setIsPreValidCalled = false; + let isValidValue = null; + + this.set('setIsPreValid', () => { + setIsPreValidCalled = true; + }); + this.set('setIsValid', (val) => { + isValidValue = val; + }); + this.set('heading', NEW_FORM_STEPS.headings[2]); + this.set('subHeading', NEW_FORM_STEPS.subheadings[2]); + + await render(hbs` + + `); + + await fillIn( + '[data-test-textarea-field][name="hobbies"]', + generateWords(120), + ); + await fillIn( + '[data-test-textarea-field][name="funFact"]', + generateWords(130), + ); + + await waitUntil( + () => + JSON.parse(localStorage.getItem('newStepThreeData') || '{}').hobbies, + ); + + assert.ok(setIsPreValidCalled); + assert.true(isValidValue); + assert + .dom('[data-test-textarea-field][name="hobbies"]') + .hasValue(generateWords(120)); + assert + .dom('[data-test-textarea-field][name="funFact"]') + .hasValue(generateWords(130)); + }); + + test('it loads step three data from localStorage', async function (assert) { + const testData = { + hobbies: generateWords(200), + funFact: generateWords(300), + }; + localStorage.setItem('newStepThreeData', JSON.stringify(testData)); + + this.set('setIsPreValid', () => {}); + this.set('setIsValid', () => {}); + this.set('heading', NEW_FORM_STEPS.headings[2]); + this.set('subHeading', NEW_FORM_STEPS.subheadings[2]); + + await render(hbs` + + `); + + assert + .dom('[data-test-textarea-field][name="hobbies"]') + .hasValue(testData.hobbies); + assert + .dom('[data-test-textarea-field][name="funFact"]') + .hasValue(testData.funFact); + }); + }, +); diff --git a/tests/integration/components/new-join-steps/new-step-two-test.js b/tests/integration/components/new-join-steps/new-step-two-test.js new file mode 100644 index 00000000..560b3434 --- /dev/null +++ b/tests/integration/components/new-join-steps/new-step-two-test.js @@ -0,0 +1,160 @@ +import { fillIn, render, waitUntil } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { module, test } from 'qunit'; +import { + NEW_FORM_STEPS, + NEW_STEP_LIMITS, +} from 'website-www/constants/new-join-form'; +import { setupRenderingTest } from 'website-www/tests/helpers'; + +module( + 'Integration | Component | new-join-steps/new-step-two', + function (hooks) { + setupRenderingTest(hooks); + + function generateWords(count) { + return Array(count).fill('word').join(' '); + } + + hooks.beforeEach(function () { + localStorage.removeItem('newStepTwoData'); + localStorage.removeItem('isValid'); + }); + + hooks.afterEach(function () { + localStorage.removeItem('newStepTwoData'); + localStorage.removeItem('isValid'); + }); + + test('it renders step two correctly', async function (assert) { + this.set('setIsPreValid', () => {}); + this.set('setIsValid', () => {}); + this.set('heading', NEW_FORM_STEPS.headings[1]); + this.set('subHeading', NEW_FORM_STEPS.subheadings[1]); + + await render(hbs` + + `); + + assert.dom('[data-test-heading]').hasText(NEW_FORM_STEPS.headings[1]); + assert + .dom('[data-test-sub-heading]') + .hasText(NEW_FORM_STEPS.subheadings[1]); + assert.dom('[data-test-input-field][name="skills"]').exists(); + assert.dom('[data-test-input-field][name="company"]').exists(); + assert.dom('[data-test-textarea-field][name="introduction"]').exists(); + assert.dom('[data-test-word-count="introduction"]').exists(); + }); + + test('it validates step two fields and updates word count', async function (assert) { + this.set('setIsPreValid', () => {}); + this.set('setIsValid', () => {}); + this.set('heading', NEW_FORM_STEPS.headings[1]); + this.set('subHeading', NEW_FORM_STEPS.subheadings[1]); + + await render(hbs` + + `); + + await fillIn( + '[data-test-textarea-field][name="introduction"]', + generateWords(120), + ); + + await waitUntil(() => + document + .querySelector('[data-test-word-count="introduction"]') + ?.textContent.includes('120/'), + ); + + assert + .dom('[data-test-word-count="introduction"]') + .hasText(`120/${NEW_STEP_LIMITS.stepTwo.introduction.max} words`); + + await fillIn('[data-test-input-field][name="skills"]', generateWords(3)); + + await waitUntil(() => + document.querySelector('[data-test-error="skills"]'), + ); + + assert.dom('[data-test-error="skills"]').exists(); + }); + + test('it saves to localStorage and calls callbacks', async function (assert) { + let setIsPreValidCalled = false; + let isValidValue = null; + + this.set('setIsPreValid', () => { + setIsPreValidCalled = true; + }); + this.set('setIsValid', (val) => { + isValidValue = val; + }); + this.set('heading', NEW_FORM_STEPS.headings[1]); + this.set('subHeading', NEW_FORM_STEPS.subheadings[1]); + + await render(hbs` + + `); + + await fillIn('[data-test-input-field][name="skills"]', generateWords(10)); + await fillIn('[data-test-input-field][name="company"]', 'Real Dev Squad'); + await fillIn( + '[data-test-textarea-field][name="introduction"]', + generateWords(120), + ); + + await waitUntil( + () => JSON.parse(localStorage.getItem('newStepTwoData') || '{}').skills, + ); + + assert.ok(setIsPreValidCalled); + assert.true(isValidValue); + assert.ok( + JSON.parse(localStorage.getItem('newStepTwoData') || '{}').skills, + ); + }); + + test('it loads data from localStorage', async function (assert) { + const testData = { + skills: generateWords(10), + company: 'Real Dev Squad', + introduction: generateWords(150), + }; + localStorage.setItem('newStepTwoData', JSON.stringify(testData)); + + this.set('setIsPreValid', () => {}); + this.set('setIsValid', () => {}); + this.set('heading', NEW_FORM_STEPS.headings[1]); + this.set('subHeading', NEW_FORM_STEPS.subheadings[1]); + + await render(hbs` + + `); + + assert + .dom('[data-test-input-field][name="skills"]') + .hasValue(testData.skills); + }); + }, +); From 5f8038c65dfbe7794edbed9317bcc30f5a890c30 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Fri, 7 Nov 2025 18:11:37 +0530 Subject: [PATCH 22/25] fix: use before each to clear out local storage --- .../components/new-join-steps/new-step-three-test.js | 8 +------- .../components/new-join-steps/new-step-two-test.js | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/tests/integration/components/new-join-steps/new-step-three-test.js b/tests/integration/components/new-join-steps/new-step-three-test.js index 0d221666..9ea86417 100644 --- a/tests/integration/components/new-join-steps/new-step-three-test.js +++ b/tests/integration/components/new-join-steps/new-step-three-test.js @@ -17,13 +17,7 @@ module( } hooks.beforeEach(function () { - localStorage.removeItem('newStepThreeData'); - localStorage.removeItem('isValid'); - }); - - hooks.afterEach(function () { - localStorage.removeItem('newStepThreeData'); - localStorage.removeItem('isValid'); + localStorage.clear(); }); test('it renders step three correctly', async function (assert) { diff --git a/tests/integration/components/new-join-steps/new-step-two-test.js b/tests/integration/components/new-join-steps/new-step-two-test.js index 560b3434..2f2e0402 100644 --- a/tests/integration/components/new-join-steps/new-step-two-test.js +++ b/tests/integration/components/new-join-steps/new-step-two-test.js @@ -17,13 +17,7 @@ module( } hooks.beforeEach(function () { - localStorage.removeItem('newStepTwoData'); - localStorage.removeItem('isValid'); - }); - - hooks.afterEach(function () { - localStorage.removeItem('newStepTwoData'); - localStorage.removeItem('isValid'); + localStorage.clear(); }); test('it renders step two correctly', async function (assert) { From f091fd9a20c0c150b3c70d9de116dc2950ef6e50 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Mon, 10 Nov 2025 02:17:02 +0530 Subject: [PATCH 23/25] refactor: update test name for new step two --- .../components/new-join-steps/new-step-two-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/components/new-join-steps/new-step-two-test.js b/tests/integration/components/new-join-steps/new-step-two-test.js index 2f2e0402..d730e6df 100644 --- a/tests/integration/components/new-join-steps/new-step-two-test.js +++ b/tests/integration/components/new-join-steps/new-step-two-test.js @@ -84,7 +84,7 @@ module( assert.dom('[data-test-error="skills"]').exists(); }); - test('it saves to localStorage and calls callbacks', async function (assert) { + test('it correctly updates step two data to localStorage', async function (assert) { let setIsPreValidCalled = false; let isValidValue = null; @@ -124,7 +124,7 @@ module( ); }); - test('it loads data from localStorage', async function (assert) { + test('it loads step two data from localStorage', async function (assert) { const testData = { skills: generateWords(10), company: 'Real Dev Squad', From 1b8f4777631ad5825763b179f75207bcdb81b034 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Tue, 11 Nov 2025 19:29:07 +0530 Subject: [PATCH 24/25] refactor: enhance image upload and new stepper error handling --- app/components/new-join-steps/base-step.js | 18 ++++-- .../new-join-steps/new-step-one.hbs | 25 +++++--- app/components/new-join-steps/new-step-one.js | 57 +++++++++++++++---- .../new-join-steps/stepper-header.js | 2 +- app/components/new-stepper.hbs | 4 +- app/components/new-stepper.js | 4 ++ app/utils/validator.js | 6 +- 7 files changed, 85 insertions(+), 31 deletions(-) diff --git a/app/components/new-join-steps/base-step.js b/app/components/new-join-steps/base-step.js index 5e8e1a49..be48caa1 100644 --- a/app/components/new-join-steps/base-step.js +++ b/app/components/new-join-steps/base-step.js @@ -26,8 +26,15 @@ export default class BaseStepComponent extends Component { } initializeFormState() { - const saved = JSON.parse(getLocalStorageItem(this.storageKey, '{}')); - this.data = saved ?? {}; + 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, '']), @@ -35,7 +42,7 @@ export default class BaseStepComponent extends Component { this.wordCount = Object.fromEntries( Object.keys(this.stepValidation).map((k) => { - let val = this.data[k] || ''; + let val = String(this.data[k] || ''); return [k, val.trim().split(/\s+/).filter(Boolean).length || 0]; }), ); @@ -48,6 +55,7 @@ export default class BaseStepComponent extends Component { } @action inputHandler(e) { + if (!e?.target) return; this.args.setIsPreValid(false); const field = e.target.name; const value = e.target.value; @@ -60,7 +68,7 @@ export default class BaseStepComponent extends Component { } isDataValid() { - for (let field of Object.keys(this.stepValidation)) { + for (const field of Object.keys(this.stepValidation)) { const result = this.validateField(field, this.data[field]); if (!result.isValid) return false; } @@ -98,7 +106,7 @@ export default class BaseStepComponent extends Component { if (result.remainingToMin) { return `At least ${result.remainingToMin} more word(s) required`; } - return `Maximum ${limits.max} words allowed`; + return `Maximum ${limits?.max ?? 'N/A'} words allowed`; } syncFormValidity() { diff --git a/app/components/new-join-steps/new-step-one.hbs b/app/components/new-join-steps/new-step-one.hbs index 2bb38f03..bbe08122 100644 --- a/app/components/new-join-steps/new-step-one.hbs +++ b/app/components/new-join-steps/new-step-one.hbs @@ -1,19 +1,26 @@

        Profile Picture

        - {{#if this.imagePreview}} + {{#if this.isImageUploading}}
        - Profile preview - + +

        Processing image...

        {{else}} - + {{#if this.imagePreview}} +
        + Profile preview + +
        + {{else}} + + {{/if}} {{/if}} - +

        Image Requirements:

          diff --git a/app/components/new-join-steps/new-step-one.js b/app/components/new-join-steps/new-step-one.js index 6e3864d2..86b3d720 100644 --- a/app/components/new-join-steps/new-step-one.js +++ b/app/components/new-join-steps/new-step-one.js @@ -11,11 +11,14 @@ import BaseStepComponent from './base-step'; export default class NewStepOneComponent extends BaseStepComponent { @service login; + @service toast; roleOptions = ROLE_OPTIONS; countries = countryList; @tracked imagePreview = null; + @tracked isImageUploading = false; + @tracked fileInputElement = null; get storageKey() { return STEP_DATA_STORAGE_KEY.stepOne; @@ -29,7 +32,11 @@ export default class NewStepOneComponent extends BaseStepComponent { }; postLoadInitialize() { - if (!this.data.fullName && this.login.userData) { + if ( + !this.data.fullName && + this.login.userData?.first_name && + this.login.userData?.last_name + ) { this.updateFieldValue( 'fullName', `${this.login.userData.first_name} ${this.login.userData.last_name}`, @@ -44,28 +51,54 @@ export default class NewStepOneComponent extends BaseStepComponent { this.inputHandler({ target: { name: 'role', value: role } }); } - @action triggerFileInput() { - const fileInput = document.getElementById('profile-image-input'); - if (fileInput) { - fileInput.click(); - } + @action + setFileInputElement(element) { + this.fileInputElement = element; } - @action handleImageSelect(event) { - const file = event.target.files?.[0]; - if (!file) return; + @action + clearFileInputElement() { + this.fileInputElement = null; + } - if (file.size > 2 * 1024 * 1024) { - alert('Image size must be less than 2MB'); + @action + triggerFileInput() { + this.fileInputElement?.click(); + } + + @action + handleImageSelect(event) { + const file = event.target.files?.[0]; + if (!file || !file.type.startsWith('image/')) { + this.toast.error( + 'Invalid file type. Please upload an image file.', + 'Error!', + ); return; } + const maxSize = 2 * 1024 * 1024; + if (file.size > maxSize) { + this.toast.error('Image size must be less than 2MB', 'Error!'); + return; + } + + this.isImageUploading = true; const reader = new FileReader(); reader.onload = (e) => { const base64String = e.target.result; this.imagePreview = base64String; - this.updateFieldValue('profileImageBase64', base64String); + this.updateFieldValue?.('profileImageBase64', base64String); + this.isImageUploading = false; }; + reader.onerror = () => { + this.toast.error( + 'Failed to read the selected file. Please try again.', + 'Error!', + ); + this.isImageUploading = false; + }; + reader.readAsDataURL(file); } } diff --git a/app/components/new-join-steps/stepper-header.js b/app/components/new-join-steps/stepper-header.js index 9b81fd3c..77ce058e 100644 --- a/app/components/new-join-steps/stepper-header.js +++ b/app/components/new-join-steps/stepper-header.js @@ -7,7 +7,7 @@ export default class StepperHeaderComponent extends Component { get progressPercentage() { const totalSteps = Number(this.args.totalSteps) || 1; const currentStep = Number(this.args.currentStep) || 0; - return Math.round((currentStep / totalSteps) * 100); + return Math.min(100, Math.round((currentStep / totalSteps) * 100)); } @cached diff --git a/app/components/new-stepper.hbs b/app/components/new-stepper.hbs index 9c6ae4de..05c567e6 100644 --- a/app/components/new-stepper.hbs +++ b/app/components/new-stepper.hbs @@ -2,8 +2,6 @@ {{#if (not-eq this.currentStep this.MIN_STEP)}} @@ -53,7 +51,7 @@ @test="next-btn" @type="button" @onClick={{this.incrementStep}} - @disabled={{not (or this.preValid this.isValid)}} + @disabled={{this.isNextButtonDisabled}} />
        {{/if}} diff --git a/app/components/new-stepper.js b/app/components/new-stepper.js index c65366d6..73993fef 100644 --- a/app/components/new-stepper.js +++ b/app/components/new-stepper.js @@ -46,6 +46,10 @@ export default class NewStepperComponent extends Component { return NEW_FORM_STEPS.subheadings[this.currentStep - 1] ?? ''; } + get isNextButtonDisabled() { + return !(this.preValid || this.isValid); + } + @action incrementStep() { if (this.currentStep < this.MAX_STEP) { const nextStep = this.currentStep + 1; diff --git a/app/utils/validator.js b/app/utils/validator.js index d1cdd428..c9bdcd9c 100644 --- a/app/utils/validator.js +++ b/app/utils/validator.js @@ -20,7 +20,11 @@ export const validateWordCount = (text, wordLimits) => { const { min, max } = wordLimits; if (!trimmedText) { - return { isValid: false, wordCount: 0, remainingToMin: min }; + return { + isValid: min === 0, + wordCount: 0, + remainingToMin: min > 0 ? min : 0, + }; } if (wordCount < min) { From 677dc2758925e36e998137e94bcf8e33f4f90a8f Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Tue, 11 Nov 2025 20:43:37 +0530 Subject: [PATCH 25/25] refactor: validation for fields in new stepper and enhance button layout --- app/components/new-join-steps/base-step.js | 11 ++++ app/components/new-stepper.hbs | 59 +++++++--------------- app/components/new-stepper.js | 17 +++++-- app/components/reusables/button.hbs | 3 +- app/constants/new-join-form.js | 4 +- app/styles/new-stepper.module.css | 26 ++++++---- 6 files changed, 62 insertions(+), 58 deletions(-) diff --git a/app/components/new-join-steps/base-step.js b/app/components/new-join-steps/base-step.js index be48caa1..2f9578ae 100644 --- a/app/components/new-join-steps/base-step.js +++ b/app/components/new-join-steps/base-step.js @@ -64,6 +64,12 @@ export default class BaseStepComponent extends Component { validateField(field, value) { 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); } @@ -103,6 +109,11 @@ export default class BaseStepComponent extends Component { 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`; } diff --git a/app/components/new-stepper.hbs b/app/components/new-stepper.hbs index 05c567e6..3c6b76af 100644 --- a/app/components/new-stepper.hbs +++ b/app/components/new-stepper.hbs @@ -1,58 +1,33 @@
        {{#if (not-eq this.currentStep this.MIN_STEP)}} - + {{/if}}
        {{#if (eq this.currentStep this.MIN_STEP)}} - + -
        - -
        +
        + +
        - {{else if (eq this.currentStep 1)}} - + {{else if (eq this.currentStep 1)}} + {{/if}} {{#if (not-eq this.currentStep this.MIN_STEP)}} -
        - {{#if this.showPreviousButton}} - - {{else}} -
        - {{/if}} +
        + {{#if this.showPreviousButton}} + + {{/if}} - -
        + +
        {{/if}}
        \ No newline at end of file diff --git a/app/components/new-stepper.js b/app/components/new-stepper.js index 73993fef..919c6803 100644 --- a/app/components/new-stepper.js +++ b/app/components/new-stepper.js @@ -14,12 +14,23 @@ export default class NewStepperComponent extends Component { @service onboarding; @service joinApplicationTerms; - @tracked currentStep = - Number(getLocalStorageItem('currentStep') ?? this.args.step) || 0; - @tracked preValid = false; @tracked isValid = getLocalStorageItem('isValid') === 'true'; + @tracked currentStep = 0; + + constructor() { + super(...arguments); + + const storedStep = getLocalStorageItem('currentStep'); + const stepFromArgs = this.args.step; + this.currentStep = storedStep + ? Number(storedStep) + : stepFromArgs != null + ? Number(stepFromArgs) + : 0; + } + setIsValid = (newVal) => (this.isValid = newVal); setIsPreValid = (newVal) => (this.preValid = newVal); diff --git a/app/components/reusables/button.hbs b/app/components/reusables/button.hbs index 60c9caf1..da4b0155 100644 --- a/app/components/reusables/button.hbs +++ b/app/components/reusables/button.hbs @@ -2,7 +2,8 @@ data-test-button={{@test}} class="btn btn-{{@variant}} btn-{{@classGenerateUsername}} - {{if @disabled 'btn-disabled' ''}}" + {{if @disabled 'btn-disabled' ''}} + {{@class}}" type={{@type}} disabled={{@disabled}} title={{@title}} diff --git a/app/constants/new-join-form.js b/app/constants/new-join-form.js index 6911a135..9a570d71 100644 --- a/app/constants/new-join-form.js +++ b/app/constants/new-join-form.js @@ -16,10 +16,10 @@ export const ROLE_OPTIONS = [ export const NEW_STEP_LIMITS = { stepOne: { - country: { min: 1 }, + country: { min: 1, type: 'dropdown' }, state: { min: 1 }, city: { min: 1 }, - role: { min: 1 }, + role: { min: 1, type: 'select' }, }, }; diff --git a/app/styles/new-stepper.module.css b/app/styles/new-stepper.module.css index bbef5d22..b9da0542 100644 --- a/app/styles/new-stepper.module.css +++ b/app/styles/new-stepper.module.css @@ -8,12 +8,19 @@ } .new-stepper__buttons { - width: 60vw; - display: flex; - justify-content: space-between; + display: grid; + grid-template-columns: 1fr auto 1fr; align-items: center; - gap: 1rem; - margin: 0; + width: 60vw; +} + +.prev-button { + grid-column: 1; +} + +.next-button { + grid-column: 3; + justify-self: end; } .welcome-screen { @@ -398,8 +405,7 @@ border-top: 1px solid var(--color-lightgrey); } -/* MEDIA QUERIES */ -@media screen and (width <= 1280px) { +@media screen and (width <=1280px) { .new-stepper__form, .form-header, .new-stepper__buttons { @@ -407,7 +413,7 @@ } } -@media screen and (width <= 1024px) { +@media screen and (width <=1024px) { .new-stepper__form, .form-header, .new-stepper__buttons { @@ -415,7 +421,7 @@ } } -@media screen and (width <= 768px) { +@media screen and (width <=768px) { .two-column-layout { grid-template-columns: 1fr; } @@ -472,7 +478,7 @@ } } -@media screen and (width <= 480px) { +@media screen and (width <=480px) { .form-header, .new-stepper__form, .new-stepper__buttons {