Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d0adec7
feat: add step one for image upload and personal details
MayankBansal12 Nov 3, 2025
6c6687f
fix: styling for form header
MayankBansal12 Nov 3, 2025
5ebca46
fix: styling for image box and input boxes
MayankBansal12 Nov 3, 2025
c9fe6f6
feat: add prev and next stepper buttons
MayankBansal12 Nov 3, 2025
3cc8996
fix: incorrect data testt for stepper buttons
MayankBansal12 Nov 3, 2025
77f0031
feat: add base step component for all new steps
MayankBansal12 Nov 4, 2025
aff01fa
fix: input validation in new step one
MayankBansal12 Nov 4, 2025
e0e1d3b
refactor: improve progress calculation and update header text
MayankBansal12 Nov 6, 2025
a90a012
refactor: improve step navigation
MayankBansal12 Nov 8, 2025
e5edda0
fix: image upload preview and update storage keys
MayankBansal12 Nov 8, 2025
2a697ea
refactor: use try catch in local storage actions
MayankBansal12 Nov 8, 2025
d656d03
refactor: improve accessibility in new step one
MayankBansal12 Nov 8, 2025
7ecf73e
Merge branch 'develop' into feat/new-step-1
MayankBansal12 Nov 9, 2025
3a70ecd
feat: add new step two for introduction
MayankBansal12 Nov 3, 2025
4e9fe8a
feat: add step three for interests
MayankBansal12 Nov 4, 2025
99ed2b6
feat: add base step class to handle input validation and local storag…
MayankBansal12 Nov 4, 2025
2478f76
fix: postIntialization in new step one
MayankBansal12 Nov 4, 2025
93969aa
fix: grid styling for step two and three
MayankBansal12 Nov 5, 2025
fe14c92
refactor: remove redundant max words
MayankBansal12 Nov 7, 2025
34dfc52
fix: lint issue and form headings
MayankBansal12 Nov 7, 2025
ee06d8a
refactor: use constant for storage keys
MayankBansal12 Nov 9, 2025
0fd986f
feat: add new step for social links
MayankBansal12 Nov 4, 2025
cd7db59
feat: add snew step four for social links
MayankBansal12 Nov 4, 2025
5fe4220
feat: add new step fivve for stepper
MayankBansal12 Nov 4, 2025
24ad300
fix: styling for selectt in new step five
MayankBansal12 Nov 5, 2025
14566a3
refactor: replace hardcoded storage keys with constants
MayankBansal12 Nov 9, 2025
e794f60
feat: add review step for new stepper
MayankBansal12 Nov 5, 2025
c4a96bd
feat: add thank you screen for new stepper
MayankBansal12 Nov 9, 2025
ec0cbfc
refactor: use structure for step data and replace hardcoded storage keys
MayankBansal12 Nov 9, 2025
7dd08c5
refactor: add svalidation for current step
MayankBansal12 Nov 10, 2025
2da4b3f
fix: stub router for new stepper test
MayankBansal12 Nov 10, 2025
aaa348c
test: add unit test for base-step
MayankBansal12 Nov 10, 2025
1b8f477
refactor: enhance image upload and new stepper error handling
MayankBansal12 Nov 11, 2025
9e622c5
Merge branch 'develop' into feat/new-step-1
MayankBansal12 Nov 11, 2025
677dc27
refactor: validation for fields in new stepper and enhance button layout
MayankBansal12 Nov 11, 2025
1175cea
Merge branch 'feat/new-step-1' into feat/new-step-2
MayankBansal12 Nov 11, 2025
817530d
Merge branch 'feat/new-step-2' into feat/new-step-3
MayankBansal12 Nov 11, 2025
51ed474
Merge branch 'feat/new-step-3' into feat/review-step
MayankBansal12 Nov 11, 2025
c3fd942
refactor: use getter func for review step button
MayankBansal12 Nov 11, 2025
241d6a6
Merge branch 'feat/review-step' into feat/refactor-step-validation
MayankBansal12 Nov 11, 2025
4e8b402
refactor: update button logic and improve state management in new ste…
MayankBansal12 Nov 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions app/components/new-join-steps/base-step.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { action } from '@ember/object';
import { debounce } from '@ember/runloop';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { JOIN_DEBOUNCE_TIME } from '../../constants/join';
import { validateWordCount } from '../../utils/validator';
import { scheduleOnce } from '@ember/runloop';
import { getLocalStorageItem, setLocalStorageItem } from '../../utils/storage';

export default class BaseStepComponent extends Component {
stepValidation = {};

@tracked data = {};
@tracked errorMessage = {};
@tracked wordCount = {};

get storageKey() {
return '';
}

postLoadInitialize() {}

constructor(...args) {
super(...args);
scheduleOnce('afterRender', this, this.initializeFormState);
}

initializeFormState() {
let saved = {};
try {
const stored = getLocalStorageItem(this.storageKey, '{}');
saved = stored ? JSON.parse(stored) : {};
} catch (e) {
console.warn('Failed to parse stored form data:', e);
saved = {};
}
this.data = saved;

this.errorMessage = Object.fromEntries(
Object.keys(this.stepValidation).map((k) => [k, '']),
);

this.wordCount = Object.fromEntries(
Object.keys(this.stepValidation).map((k) => {
let val = String(this.data[k] || '');
return [k, val.trim().split(/\s+/).filter(Boolean).length || 0];
}),
);

this.postLoadInitialize();

const valid = this.isDataValid();
this.args.onValidityChange(valid);
}

@action inputHandler(e) {
if (!e?.target) return;
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.stepValidation[field];
const fieldType = limits?.type || 'text';

if (fieldType === 'select' || fieldType === 'dropdown') {
const hasValue = value && String(value).trim().length > 0;
return { isValid: hasValue };
}
return validateWordCount(value, limits);
}

isDataValid() {
for (const field of Object.keys(this.stepValidation)) {
const result = this.validateField(field, this.data[field]);
if (!result.isValid) return false;
}
return true;
}

handleFieldUpdate(field, value) {
this.updateFieldValue(field, value);
const result = this.validateField(field, value);
this.updateWordCount(field, result);
this.updateErrorMessage(field, result);
this.syncFormValidity();
}

updateFieldValue(field, value) {
this.data = { ...this.data, [field]: value };
setLocalStorageItem(this.storageKey, JSON.stringify(this.data));
}

updateWordCount(field, result) {
const wordCount = result.wordCount ?? 0;
this.wordCount = { ...this.wordCount, [field]: wordCount };
}

updateErrorMessage(field, result) {
this.errorMessage = {
...this.errorMessage,
[field]: this.formatError(field, result),
};
}

formatError(field, result) {
const limits = this.stepValidation[field];
if (result.isValid) return '';

const fieldType = limits?.type || 'text';
if (fieldType === 'select' || fieldType === 'dropdown') {
return 'Please choose an option';
}
if (result.remainingToMin) {
return `At least ${result.remainingToMin} more word(s) required`;
}
return `Maximum ${limits?.max ?? 'N/A'} words allowed`;
}

syncFormValidity() {
const allValid = this.isDataValid();
this.args.onValidityChange(allValid);
}
}
33 changes: 33 additions & 0 deletions app/components/new-join-steps/new-step-five.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<div class="step-container">
<div class="form-header__text">
<h1 class="section-heading">{{@heading}}</h1>
<p class="section-instruction">{{@subHeading}}</p>
</div>

<Reusables::TextAreaBox @field='Why you want to join Real Dev Squad?' @name='whyRds'
@placeHolder='Tell us why you want to join our community...' @required={{true}} @value={{this.data.whyRds}}
@onInput={{this.inputHandler}} />
{{#if this.errorMessage.whyRds}}
<div class='error__message'>{{this.errorMessage.whyRds}}</div>
{{/if}}

<div class="form-grid form-grid--2">
<div class="form-grid__item">
<Reusables::InputBox @field='No of hours/week you are willing to contribute?' @name='numberOfHours'
@placeHolder='Enter value between 1-100.' @type='number' @required={{true}} @value={{this.data.numberOfHours}}
@onInput={{this.inputHandler}} @options={{this.heardFrom}} />
{{#if this.errorMessage.numberOfHours}}
<div class='error__message'>{{this.errorMessage.numberOfHours}}</div>
{{/if}}
</div>

<div class="form-grid__item">
<Reusables::Dropdown @field='How did you hear about us?' @name='foundFrom'
@placeHolder='Choose from below options' @required={{true}} @value={{this.data.foundFrom}}
@options={{this.heardFrom}} @onChange={{this.inputHandler}} />
{{#if this.errorMessage.foundFrom}}
<div class='error__message'>{{this.errorMessage.foundFrom}}</div>
{{/if}}
</div>
</div>
</div>
16 changes: 16 additions & 0 deletions app/components/new-join-steps/new-step-five.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import BaseStepComponent from './base-step';
import {
NEW_STEP_LIMITS,
STEP_DATA_STORAGE_KEY,
} from '../../constants/new-join-form';
import { heardFrom } from '../../constants/social-data';

export default class NewStepFiveComponent extends BaseStepComponent {
storageKey = STEP_DATA_STORAGE_KEY.stepFive;
heardFrom = heardFrom;

stepValidation = {
whyRds: NEW_STEP_LIMITS.stepFive.whyRds,
foundFrom: NEW_STEP_LIMITS.stepFive.foundFrom,
};
}
62 changes: 62 additions & 0 deletions app/components/new-join-steps/new-step-four.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<div class="step-container">
<div class="form-header__text">
<h1 class="section-heading">{{@heading}}</h1>
<p class="section-instruction">{{@subHeading}}</p>
</div>

<Reusables::InputBox @field='Phone Number' @name='phoneNumber' @placeHolder='+91 80000 00000' @type='tel'
@required={{true}} @value={{this.data.phoneNumber}} @onInput={{this.inputHandler}} @variant='input--full-width' />
{{#if this.errorMessage.phoneNumber}}
<div class='error__message'>{{this.errorMessage.phoneNumber}}</div>
{{/if}}

<Reusables::InputBox @field='Twitter' @name='twitter' @placeHolder='https://twitter.com/gangster-rishi' @type='text'
@required={{true}} @value={{this.data.twitter}} @onInput={{this.inputHandler}} @variant='input--full-width' />
{{#if this.errorMessage.twitter}}
<div class='error__message'>{{this.errorMessage.twitter}}</div>
{{/if}}

{{#if this.showGitHub}}
<Reusables::InputBox @field='GitHub' @name='github' @placeHolder='https://github.com/codewithrishi' @type='text'
@required={{true}} @value={{this.data.github}} @onInput={{this.inputHandler}} @variant='input--full-width' />
{{#if this.errorMessage.github}}
<div class='error__message'>{{this.errorMessage.github}}</div>
{{/if}}
{{/if}}

<Reusables::InputBox @field='LinkedIn' @name='linkedin' @placeHolder='https://linkedin.com/in/professional-rishi'
@type='text' @required={{true}} @value={{this.data.linkedin}} @onInput={{this.inputHandler}}
@variant='input--full-width' />
{{#if this.errorMessage.linkedin}}
<div class='error__message'>{{this.errorMessage.linkedin}}</div>
{{/if}}

<Reusables::InputBox @field='Instagram' @name='instagram' @placeHolder='https://instagram.com/gangster-rishi'
@type='text' @required={{false}} @value={{this.data.instagram}} @onInput={{this.inputHandler}}
@variant='input--full-width' />
{{#if this.errorMessage.instagram}}
<div class='error__message'>{{this.errorMessage.instagram}}</div>
{{/if}}

<Reusables::InputBox @field='Peerlist' @name='peerlist' @placeHolder='https://peerlist.io/richy-rishi' @type='text'
@required={{true}} @value={{this.data.peerlist}} @onInput={{this.inputHandler}} @variant='input--full-width' />
{{#if this.errorMessage.peerlist}}
<div class='error__message'>{{this.errorMessage.peerlist}}</div>
{{/if}}

{{#if this.showBehance}}
<Reusables::InputBox @field='Behance' @name='behance' @placeHolder='https://behance.net/designer-rishi' @type='text'
@required={{true}} @value={{this.data.behance}} @onInput={{this.inputHandler}} @variant='input--full-width' />
{{#if this.errorMessage.behance}}
<div class='error__message'>{{this.errorMessage.behance}}</div>
{{/if}}
{{/if}}

{{#if this.showDribble}}
<Reusables::InputBox @field='Dribble' @name='dribble' @placeHolder='https://dribbble.com/dribwithrishi' @type='text'
@required={{true}} @value={{this.data.dribble}} @onInput={{this.inputHandler}} @variant='input--full-width' />
{{#if this.errorMessage.dribble}}
<div class='error__message'>{{this.errorMessage.dribble}}</div>
{{/if}}
{{/if}}
</div>
80 changes: 80 additions & 0 deletions app/components/new-join-steps/new-step-four.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import BaseStepComponent from './base-step';
import {
NEW_STEP_LIMITS,
STEP_DATA_STORAGE_KEY,
} from '../../constants/new-join-form';
import { phoneNumberRegex } from '../../constants/regex';

export default class NewStepFourComponent extends BaseStepComponent {
storageKey = STEP_DATA_STORAGE_KEY.stepFour;

stepValidation = {
phoneNumber: NEW_STEP_LIMITS.stepFour.phoneNumber,
twitter: NEW_STEP_LIMITS.stepFour.twitter,
linkedin: NEW_STEP_LIMITS.stepFour.linkedin,
instagram: NEW_STEP_LIMITS.stepFour.instagram,
peerlist: NEW_STEP_LIMITS.stepFour.peerlist,
};

get userRole() {
const stepOneData = JSON.parse(
localStorage.getItem('newStepOneData') || '{}',
);
return stepOneData.role || '';
}

postLoadInitialize() {
if (this.userRole === 'Developer') {
this.stepValidation.github = NEW_STEP_LIMITS.stepFour.github;
}

if (this.userRole === 'Designer') {
this.stepValidation.behance = NEW_STEP_LIMITS.stepFour.behance;
this.stepValidation.dribble = NEW_STEP_LIMITS.stepFour.dribble;
}

// re-calculate the errorMessage and wordCount for new input fields
this.errorMessage = Object.fromEntries(
Object.keys(this.stepValidation).map((k) => [k, '']),
);

this.wordCount = Object.fromEntries(
Object.keys(this.stepValidation).map((k) => {
let val = this.data[k] || '';
return [k, val.trim().split(/\s+/).filter(Boolean).length || 0];
}),
);
}

get showGitHub() {
return this.userRole === 'Developer';
}

get showBehance() {
return this.userRole === 'Designer';
}

get showDribble() {
return this.userRole === 'Designer';
}

validateField(field, value) {
if (field === 'phoneNumber') {
const trimmedValue = value?.trim() || '';
const isValid = trimmedValue && phoneNumberRegex.test(trimmedValue);
return {
isValid,
wordCount: 0,
};
}
return super.validateField(field, value);
}

formatError(field, result) {
if (field === 'phoneNumber') {
if (result.isValid) return '';
return 'Please enter a valid phone number (e.g., +91 80000 00000)';
}
return super.formatError(field, result);
}
}
65 changes: 65 additions & 0 deletions app/components/new-join-steps/new-step-one.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<div class="two-column-layout">
<div class="two-column-layout__left">
<h3 class="section-heading">Profile Picture</h3>
{{#if this.isImageUploading}}
<div class="image-preview-container">
<Spinner />
<p>Processing image...</p>
</div>
{{else}}
{{#if this.imagePreview}}
<div class="image-preview-container">
<img src={{this.imagePreview}} alt="Profile preview" class="image-preview" />
<Reusables::Button @type="button" @text="Change Image" @variant="light" @onClick={{this.triggerFileInput}} />
</div>
{{else}}
<button class="image-upload-box" type="button" {{on 'click' this.triggerFileInput}}>
<FaIcon @icon="upload" />
<p class="image-upload-box__text">Upload Image</p>
</button>
{{/if}}
{{/if}}
<input type="file" accept="image/png,image/jpeg" class="image-form__input" hidden aria-label="Upload profile image"
{{did-insert this.setFileInputElement}} {{will-destroy this.clearFileInputElement}} {{on 'change' this.handleImageSelect}} />
<div class="image-requirements">
<h4>Image Requirements:</h4>
<ul>
<li>Must be a real, clear photograph (no anime, filters, or drawings)</li>
<li>Must contain exactly one face</li>
<li>Face must cover at least 60% of the image</li>
<li>Supported formats: JPG, PNG</li>
<li>Image will be validated before moving to next step</li>
</ul>
</div>
</div>

<div class="two-column-layout__right">
<h3 class="section-heading">Personal Details</h3>
<p class="section-instruction">Please provide correct details and choose your role carefully, it won't be changed
later.</p>

<Reusables::InputBox @field='Your Name' @name='fullName' @placeHolder='Full Name' @type='text' @disabled={{true}}
@required={{true}} @value={{this.data.fullName}} @onInput={{this.inputHandler}} />

<Reusables::Dropdown @field='Your Country' @name='country' @placeHolder='Choose your country' @required={{true}}
@value={{this.data.country}} @options={{this.countries}} @onChange={{this.inputHandler}} />

<Reusables::InputBox @field='State' @name='state' @placeHolder='Your State e.g. Karnataka' @type='text'
@required={{true}} @value={{this.data.state}} @onInput={{this.inputHandler}} />

<Reusables::InputBox @field='City' @name='city' @placeHolder='Your City e.g. Bengaluru' @type='text'
@required={{true}} @value={{this.data.city}} @onInput={{this.inputHandler}} />

<div class="role-selection">
<p>Applying as</p>
<div class="role-buttons" role="radiogroup" aria-label="Select your role">
{{#each this.roleOptions as |role|}}
<button type="button" class="role-button {{if (eq this.data.role role) 'role-button--selected'}}" role="radio"
aria-checked="{{if (eq this.data.role role) 'true' 'false'}}" {{on 'click' (fn this.selectRole role)}}>
{{role}}
</button>
{{/each}}
</div>
</div>
</div>
</div>
Loading