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 000000000..2f9578aef
--- /dev/null
+++ b/app/components/new-join-steps/base-step.js
@@ -0,0 +1,128 @@
+import { action } from '@ember/object';
+import { debounce } from '@ember/runloop';
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { JOIN_DEBOUNCE_TIME } from '../../constants/join';
+import { validateWordCount } from '../../utils/validator';
+import { scheduleOnce } from '@ember/runloop';
+import { getLocalStorageItem, setLocalStorageItem } from '../../utils/storage';
+
+export default class BaseStepComponent extends Component {
+ stepValidation = {};
+
+ @tracked data = {};
+ @tracked errorMessage = {};
+ @tracked wordCount = {};
+
+ get storageKey() {
+ return '';
+ }
+
+ postLoadInitialize() {}
+
+ constructor(...args) {
+ super(...args);
+ scheduleOnce('afterRender', this, this.initializeFormState);
+ }
+
+ initializeFormState() {
+ let saved = {};
+ try {
+ const stored = getLocalStorageItem(this.storageKey, '{}');
+ saved = stored ? JSON.parse(stored) : {};
+ } catch (e) {
+ console.warn('Failed to parse stored form data:', e);
+ saved = {};
+ }
+ this.data = saved;
+
+ this.errorMessage = Object.fromEntries(
+ Object.keys(this.stepValidation).map((k) => [k, '']),
+ );
+
+ this.wordCount = Object.fromEntries(
+ Object.keys(this.stepValidation).map((k) => {
+ let val = String(this.data[k] || '');
+ return [k, val.trim().split(/\s+/).filter(Boolean).length || 0];
+ }),
+ );
+
+ this.postLoadInitialize();
+
+ const valid = this.isDataValid();
+ this.args.setIsPreValid(valid);
+ setLocalStorageItem('isValid', String(valid));
+ }
+
+ @action inputHandler(e) {
+ if (!e?.target) return;
+ this.args.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.stepValidation[field];
+ const fieldType = limits?.type || 'text';
+
+ if (fieldType === 'select' || fieldType === 'dropdown') {
+ const hasValue = value && String(value).trim().length > 0;
+ return { isValid: hasValue };
+ }
+ return validateWordCount(value, limits);
+ }
+
+ isDataValid() {
+ for (const field of Object.keys(this.stepValidation)) {
+ const result = this.validateField(field, this.data[field]);
+ if (!result.isValid) return false;
+ }
+ return true;
+ }
+
+ handleFieldUpdate(field, value) {
+ this.updateFieldValue(field, value);
+ const result = this.validateField(field, value);
+ this.updateWordCount(field, result);
+ this.updateErrorMessage(field, result);
+ this.syncFormValidity();
+ }
+
+ updateFieldValue(field, value) {
+ this.data = { ...this.data, [field]: value };
+ setLocalStorageItem(this.storageKey, JSON.stringify(this.data));
+ }
+
+ updateWordCount(field, result) {
+ const wordCount = result.wordCount ?? 0;
+ this.wordCount = { ...this.wordCount, [field]: wordCount };
+ }
+
+ updateErrorMessage(field, result) {
+ this.errorMessage = {
+ ...this.errorMessage,
+ [field]: this.formatError(field, result),
+ };
+ }
+
+ formatError(field, result) {
+ const limits = this.stepValidation[field];
+ if (result.isValid) return '';
+
+ const fieldType = limits?.type || 'text';
+ if (fieldType === 'select' || fieldType === 'dropdown') {
+ return 'Please choose an option';
+ }
+ if (result.remainingToMin) {
+ return `At least ${result.remainingToMin} more word(s) required`;
+ }
+ return `Maximum ${limits?.max ?? 'N/A'} words allowed`;
+ }
+
+ syncFormValidity() {
+ const allValid = this.isDataValid();
+ this.args.setIsValid(allValid);
+ setLocalStorageItem('isValid', String(allValid));
+ }
+}
diff --git a/app/components/new-join-steps/new-step-five.hbs b/app/components/new-join-steps/new-step-five.hbs
new file mode 100644
index 000000000..ded36fdc4
--- /dev/null
+++ b/app/components/new-join-steps/new-step-five.hbs
@@ -0,0 +1,33 @@
+
+
+
+
+ {{#if this.errorMessage.whyRds}}
+
{{this.errorMessage.whyRds}}
+ {{/if}}
+
+
+
\ No newline at end of file
diff --git a/app/components/new-join-steps/new-step-five.js b/app/components/new-join-steps/new-step-five.js
new file mode 100644
index 000000000..71a353264
--- /dev/null
+++ b/app/components/new-join-steps/new-step-five.js
@@ -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,
+ };
+}
diff --git a/app/components/new-join-steps/new-step-four.hbs b/app/components/new-join-steps/new-step-four.hbs
new file mode 100644
index 000000000..40106a442
--- /dev/null
+++ b/app/components/new-join-steps/new-step-four.hbs
@@ -0,0 +1,62 @@
+
+
+
+
+ {{#if this.errorMessage.phoneNumber}}
+
{{this.errorMessage.phoneNumber}}
+ {{/if}}
+
+
+ {{#if this.errorMessage.twitter}}
+
{{this.errorMessage.twitter}}
+ {{/if}}
+
+ {{#if this.showGitHub}}
+
+ {{#if this.errorMessage.github}}
+
{{this.errorMessage.github}}
+ {{/if}}
+ {{/if}}
+
+
+ {{#if this.errorMessage.linkedin}}
+
{{this.errorMessage.linkedin}}
+ {{/if}}
+
+
+ {{#if this.errorMessage.instagram}}
+
{{this.errorMessage.instagram}}
+ {{/if}}
+
+
+ {{#if this.errorMessage.peerlist}}
+
{{this.errorMessage.peerlist}}
+ {{/if}}
+
+ {{#if this.showBehance}}
+
+ {{#if this.errorMessage.behance}}
+
{{this.errorMessage.behance}}
+ {{/if}}
+ {{/if}}
+
+ {{#if this.showDribble}}
+
+ {{#if this.errorMessage.dribble}}
+
{{this.errorMessage.dribble}}
+ {{/if}}
+ {{/if}}
+
\ No newline at end of file
diff --git a/app/components/new-join-steps/new-step-four.js b/app/components/new-join-steps/new-step-four.js
new file mode 100644
index 000000000..bae007588
--- /dev/null
+++ b/app/components/new-join-steps/new-step-four.js
@@ -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);
+ }
+}
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 000000000..bbe08122d
--- /dev/null
+++ b/app/components/new-join-steps/new-step-one.hbs
@@ -0,0 +1,65 @@
+
+
+
Profile Picture
+ {{#if this.isImageUploading}}
+
+
+
Processing image...
+
+ {{else}}
+ {{#if this.imagePreview}}
+
+

+
+
+ {{else}}
+
+ {{/if}}
+ {{/if}}
+
+
+
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
+ - Image will be validated before moving to next step
+
+
+
+
+
+
Personal Details
+
Please provide correct details and choose your role carefully, it won't be changed
+ later.
+
+
+
+
+
+
+
+
+
+
+
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 000000000..86b3d7207
--- /dev/null
+++ b/app/components/new-join-steps/new-step-one.js
@@ -0,0 +1,104 @@
+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,
+ STEP_DATA_STORAGE_KEY,
+} from '../../constants/new-join-form';
+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;
+ }
+
+ stepValidation = {
+ country: NEW_STEP_LIMITS.stepOne.country,
+ state: NEW_STEP_LIMITS.stepOne.state,
+ city: NEW_STEP_LIMITS.stepOne.city,
+ role: NEW_STEP_LIMITS.stepOne.role,
+ };
+
+ postLoadInitialize() {
+ 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}`,
+ );
+ }
+ if (this.data.profileImageBase64) {
+ this.imagePreview = this.data.profileImageBase64;
+ }
+ }
+
+ @action selectRole(role) {
+ this.inputHandler({ target: { name: 'role', value: role } });
+ }
+
+ @action
+ setFileInputElement(element) {
+ this.fileInputElement = element;
+ }
+
+ @action
+ clearFileInputElement() {
+ this.fileInputElement = null;
+ }
+
+ @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.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/new-step-six.hbs b/app/components/new-join-steps/new-step-six.hbs
new file mode 100644
index 000000000..cb18d7d54
--- /dev/null
+++ b/app/components/new-join-steps/new-step-six.hbs
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+ Full Name:
+
+ {{if this.stepData.one.fullName this.stepData.one.fullName 'Not provided'}}
+
+
+
+ Location:
+
+ {{this.locationDisplay}}
+
+
+
+ Applying as:
+
+ {{if this.stepData.one.role this.stepData.one.role 'Not provided'}}
+
+
+
+ Profile Image:
+
+ Not uploaded
+
+
+
+
+
+
+
+
+
+ Skills:
+
+ {{if this.stepData.two.skills this.stepData.two.skills 'Not provided'}}
+
+
+
+ Institution/Company:
+
+ {{if this.stepData.two.company this.stepData.two.company 'Not provided'}}
+
+
+
+ Introduction:
+
+ {{if this.stepData.two.introduction this.stepData.two.introduction 'Not provided'}}
+
+
+
+
+
+
+
+
+
+ Hobbies:
+
+ {{if this.stepData.three.hobbies this.stepData.three.hobbies 'Not provided'}}
+
+
+
+ Fun Fact:
+
+ {{if this.stepData.three.funFact this.stepData.three.funFact 'Not provided'}}
+
+
+
+
+
+
+
+
+
+ Phone Number:
+
+ {{if this.stepData.four.phoneNumber this.stepData.four.phoneNumber 'Not provided'}}
+
+
+
+ Twitter:
+
+
+ {{#if this.showGitHub}}
+
+ GitHub:
+
+ {{if this.stepData.four.github this.stepData.four.github 'Not provided'}}
+
+
+ {{/if}}
+
+ LinkedIn:
+
+ {{if this.stepData.four.linkedin this.stepData.four.linkedin 'Not provided'}}
+
+
+
+ Instagram:
+
+ {{if this.stepData.four.instagram this.stepData.four.instagram 'Not uploaded'}}
+
+
+
+ Peerlist:
+
+ {{if this.stepData.four.peerlist this.stepData.four.peerlist 'Not provided'}}
+
+
+ {{#if this.showBehance}}
+
+ Behance:
+
+ {{if this.stepData.four.behance this.stepData.four.behance 'Not provided'}}
+
+
+ {{/if}}
+ {{#if this.showDribble}}
+
+ Dribble:
+
+ {{if this.stepData.four.dribble this.stepData.four.dribble 'Not provided'}}
+
+
+ {{/if}}
+
+
+
+
+
+
+
+ Why you want to join Real Dev Squad?:
+
+ {{if this.stepData.five.whyRds this.stepData.five.whyRds 'Not provided'}}
+
+
+
+ Hours per week:
+
+ {{if this.stepData.five.numberOfHours this.stepData.five.numberOfHours 'Not provided'}}
+
+
+
+ How did you hear about us?:
+
+ {{if this.stepData.five.foundFrom this.stepData.five.foundFrom 'Not provided'}}
+
+
+
+
+
diff --git a/app/components/new-join-steps/new-step-six.js b/app/components/new-join-steps/new-step-six.js
new file mode 100644
index 000000000..9928e6631
--- /dev/null
+++ b/app/components/new-join-steps/new-step-six.js
@@ -0,0 +1,57 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { getLocalStorageItem } from '../../utils/storage';
+import { STEP_DATA_STORAGE_KEY } from '../../constants/new-join-form';
+
+export default class NewStepSixComponent extends Component {
+ @tracked stepData = {
+ one: {},
+ two: {},
+ three: {},
+ four: {},
+ five: {},
+ };
+
+ constructor(...args) {
+ super(...args);
+ this.loadAllStepData();
+ }
+
+ loadAllStepData() {
+ this.stepData.one = JSON.parse(
+ getLocalStorageItem(STEP_DATA_STORAGE_KEY.stepOne),
+ );
+ this.stepData.two = JSON.parse(
+ getLocalStorageItem(STEP_DATA_STORAGE_KEY.stepTwo),
+ );
+ this.stepData.three = JSON.parse(
+ getLocalStorageItem(STEP_DATA_STORAGE_KEY.stepThree),
+ );
+ this.stepData.four = JSON.parse(
+ getLocalStorageItem(STEP_DATA_STORAGE_KEY.stepFour),
+ );
+ this.stepData.five = JSON.parse(
+ getLocalStorageItem(STEP_DATA_STORAGE_KEY.stepFive),
+ );
+ }
+
+ get userRole() {
+ return this.stepData.one.role || '';
+ }
+
+ get showGitHub() {
+ return this.userRole === 'Developer';
+ }
+
+ get showBehance() {
+ return this.userRole === 'Designer';
+ }
+
+ get showDribble() {
+ return this.userRole === 'Designer';
+ }
+
+ get locationDisplay() {
+ return `${this.stepData.one.city}, ${this.stepData.one.state}, ${this.stepData.one.country}`;
+ }
+}
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 000000000..7614c45f2
--- /dev/null
+++ b/app/components/new-join-steps/new-step-three.hbs
@@ -0,0 +1,38 @@
+
\ 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 000000000..0c3fd6b25
--- /dev/null
+++ b/app/components/new-join-steps/new-step-three.js
@@ -0,0 +1,13 @@
+import BaseStepComponent from './base-step';
+import {
+ NEW_STEP_LIMITS,
+ STEP_DATA_STORAGE_KEY,
+} from '../../constants/new-join-form';
+
+export default class NewStepThreeComponent extends BaseStepComponent {
+ 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
new file mode 100644
index 000000000..c96c5175e
--- /dev/null
+++ b/app/components/new-join-steps/new-step-two.hbs
@@ -0,0 +1,37 @@
+
\ 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 000000000..edb382940
--- /dev/null
+++ b/app/components/new-join-steps/new-step-two.js
@@ -0,0 +1,14 @@
+import BaseStepComponent from './base-step';
+import {
+ NEW_STEP_LIMITS,
+ STEP_DATA_STORAGE_KEY,
+} from '../../constants/new-join-form';
+
+export default class NewStepTwoComponent extends BaseStepComponent {
+ 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/components/new-join-steps/stepper-header.hbs b/app/components/new-join-steps/stepper-header.hbs
new file mode 100644
index 000000000..7fcd94380
--- /dev/null
+++ b/app/components/new-join-steps/stepper-header.hbs
@@ -0,0 +1,14 @@
+
\ 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 000000000..77ce058e9
--- /dev/null
+++ b/app/components/new-join-steps/stepper-header.js
@@ -0,0 +1,17 @@
+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 = Number(this.args.totalSteps) || 1;
+ const currentStep = Number(this.args.currentStep) || 0;
+ return Math.min(100, Math.round((currentStep / totalSteps) * 100));
+ }
+
+ @cached
+ get progressStyle() {
+ return htmlSafe(`width: ${this.progressPercentage}%`);
+ }
+}
diff --git a/app/components/new-join-steps/thank-you-screen.hbs b/app/components/new-join-steps/thank-you-screen.hbs
new file mode 100644
index 000000000..b014fad5c
--- /dev/null
+++ b/app/components/new-join-steps/thank-you-screen.hbs
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+ Head over to Application Tracking Page.
+
+
+
+ Checkout AI review and and edit your application to improve application rank.
+
+
+
+ Complete quests to improve your ranking and increase your chances of early reviews.
+
+
+
+
+
Application ID
+
{{@applicationId}}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/components/new-stepper.hbs b/app/components/new-stepper.hbs
index 598cf5475..f2962269b 100644
--- a/app/components/new-stepper.hbs
+++ b/app/components/new-stepper.hbs
@@ -1,18 +1,57 @@
-
+
+ {{#if (and (not-eq this.currentStep this.MIN_STEP) (not-eq this.currentStep 7))}}
+
+ {{#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 1cb753e2e..ff0cc434c 100644
--- a/app/components/new-stepper.js
+++ b/app/components/new-stepper.js
@@ -2,22 +2,41 @@ 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';
+import { getLocalStorageItem, setLocalStorageItem } from '../utils/storage';
export default class NewStepperComponent extends Component {
MIN_STEP = 0;
MAX_STEP = 6;
+ applicationId = '4gchuf690';
@service login;
@service router;
@service onboarding;
@service joinApplicationTerms;
- @tracked currentStep =
- Number(localStorage.getItem('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);
updateQueryParam(step) {
const existingQueryParams = this.router.currentRoute?.queryParams;
-
this.router.transitionTo('join', {
queryParams: {
...existingQueryParams,
@@ -26,10 +45,35 @@ export default class NewStepperComponent extends Component {
});
}
+ get showPreviousButton() {
+ 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] ?? '';
+ }
+
+ get firstName() {
+ return localStorage.getItem('first_name') ?? '';
+ }
+
+ get isNextButtonDisabled() {
+ return !(this.preValid || this.isValid);
+ }
+
+ get isReviewStep() {
+ return this.currentStep === this.MAX_STEP;
+ }
+
@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);
}
}
@@ -37,7 +81,8 @@ 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);
}
}
@@ -48,4 +93,23 @@ export default class NewStepperComponent extends Component {
sessionStorage.setItem('last_name', this.login.userData.last_name);
this.incrementStep();
}
+
+ @action navigateToStep(stepNumber) {
+ if (stepNumber >= this.MIN_STEP + 1 && stepNumber <= this.MAX_STEP) {
+ this.isValid = false;
+ this.preValid = false;
+ this.currentStep = stepNumber;
+ setLocalStorageItem('currentStep', String(stepNumber));
+ setLocalStorageItem('isValid', 'false');
+ this.updateQueryParam(stepNumber);
+ }
+ }
+
+ @action handleSubmit() {
+ // ToDo: handle create application
+ console.log('Submit application for review');
+ this.currentStep = this.MAX_STEP + 1;
+ setLocalStorageItem('currentStep', String(this.currentStep));
+ this.updateQueryParam(this.currentStep);
+ }
}
diff --git a/app/components/reusables/button.hbs b/app/components/reusables/button.hbs
index 60c9caf1b..da4b0155e 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/components/reusables/input-box.hbs b/app/components/reusables/input-box.hbs
index 4d842bb40..30491d75d 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/constants/new-join-form.js b/app/constants/new-join-form.js
new file mode 100644
index 000000000..0cfb9fb63
--- /dev/null
+++ b/app/constants/new-join-form.js
@@ -0,0 +1,67 @@
+export const NEW_FORM_STEPS = {
+ headings: [
+ 'Upload Professional Headshot and Complete Personal Details',
+ 'Additional Personal Information',
+ 'Your hobbies, interests, fun fact',
+ 'Connect your social profiles',
+ 'Why Real Dev Squad?',
+ 'Review and Submit',
+ ],
+ subheadings: [
+ 'Please provide accurate information for verification purposes.',
+ 'Introduce and help us get to know you better',
+ 'Show us your funny and interesting side',
+ 'Share your social media and professional profiles',
+ 'Tell us why you want to join our community',
+ 'Review your answers before submitting.',
+ ],
+};
+
+export const ROLE_OPTIONS = [
+ 'Developer',
+ 'Designer',
+ 'Product Manager',
+ 'Project Manager',
+ 'QA',
+ 'Social Media',
+];
+
+export const NEW_STEP_LIMITS = {
+ stepOne: {
+ country: { min: 1, type: 'dropdown' },
+ state: { min: 1 },
+ city: { min: 1 },
+ role: { min: 1, type: 'select' },
+ },
+ stepTwo: {
+ skills: { min: 5, max: 20 },
+ company: { min: 1 },
+ introduction: { min: 100, max: 500 },
+ },
+ stepThree: {
+ hobbies: { min: 100, max: 500 },
+ funFact: { min: 100, max: 500 },
+ },
+ stepFour: {
+ phoneNumber: { min: 1 },
+ twitter: { min: 1 },
+ github: { min: 1 },
+ linkedin: { min: 1 },
+ instagram: { min: 0 },
+ peerlist: { min: 1 },
+ behance: { min: 1 },
+ dribble: { min: 1 },
+ },
+ stepFive: {
+ whyRds: { min: 100 },
+ foundFrom: { min: 1 },
+ },
+};
+
+export const STEP_DATA_STORAGE_KEY = {
+ stepOne: 'newStepOneData',
+ stepTwo: 'newStepTwoData',
+ stepThree: 'newStepThreeData',
+ stepFour: 'newStepFourData',
+ stepFive: 'newStepFiveData',
+};
diff --git a/app/styles/new-stepper.module.css b/app/styles/new-stepper.module.css
index bbef5d22e..10f1aec86 100644
--- a/app/styles/new-stepper.module.css
+++ b/app/styles/new-stepper.module.css
@@ -8,15 +8,23 @@
}
.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;
}
-.welcome-screen {
+.next-button {
+ grid-column: 3;
+ justify-self: end;
+}
+
+.welcome-screen,
+.thank-you-screen {
width: 90%;
margin: 0 auto 2rem;
display: flex;
@@ -30,7 +38,8 @@
justify-content: center;
}
-.welcome-screen__info-item--bullet {
+.welcome-screen__info-item--bullet,
+.thank-you-screen__info-item--bullet {
display: flex;
align-items: center;
margin-bottom: 1rem;
@@ -39,7 +48,8 @@
line-height: 1.5;
}
-.welcome-screen__info-item--bullet::before {
+.welcome-screen__info-item--bullet::before,
+.thank-you-screen__info-item--bullet::before {
content: "•";
color: var(--color-pink);
font-size: 1.5rem;
@@ -60,7 +70,8 @@
line-height: 1.5;
}
-.welcome-screen__actions {
+.welcome-screen__actions,
+.thank-you-screen__actions {
display: flex;
justify-content: center;
}
@@ -398,8 +409,39 @@
border-top: 1px solid var(--color-lightgrey);
}
-/* MEDIA QUERIES */
-@media screen and (width <= 1280px) {
+.thank-you-screen {
+ align-items: center;
+ text-align: center;
+ margin: 2rem auto 0;
+}
+
+.thank-you-screen__info-container {
+ text-align: start;
+ margin-block: 1.5rem;
+}
+
+.thank-you-screen__logo {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 3rem;
+ height: 3em;
+ border-radius: 50%;
+ background-color: var(--color-navyblue);
+ color: var(--color-white);
+}
+
+.application-id h3 {
+ font-size: 1rem;
+ font-weight: 500;
+}
+
+.application-id p {
+ font-size: 1.25rem;
+ font-weight: 700;
+}
+
+@media screen and (width <=1280px) {
.new-stepper__form,
.form-header,
.new-stepper__buttons {
@@ -407,7 +449,7 @@
}
}
-@media screen and (width <= 1024px) {
+@media screen and (width <=1024px) {
.new-stepper__form,
.form-header,
.new-stepper__buttons {
@@ -415,7 +457,7 @@
}
}
-@media screen and (width <= 768px) {
+@media screen and (width <=768px) {
.two-column-layout {
grid-template-columns: 1fr;
}
@@ -434,7 +476,8 @@
grid-template-columns: 1fr;
}
- .welcome-screen {
+ .welcome-screen,
+ .thank-you-screen {
margin: 1rem auto;
padding: 1.5rem;
}
@@ -470,9 +513,13 @@
text-align: left;
max-width: 100%;
}
+
+ .form-header__text {
+ line-height: 1.5;
+ }
}
-@media screen and (width <= 480px) {
+@media screen and (width <=480px) {
.form-header,
.new-stepper__form,
.new-stepper__buttons {
@@ -487,7 +534,8 @@
padding: 1rem;
}
- .welcome-screen {
+ .welcome-screen,
+ .thank-you-screen {
margin: 0.5rem auto 1rem;
padding: 1rem;
}
diff --git a/app/utils/storage.js b/app/utils/storage.js
new file mode 100644
index 000000000..31a65c78a
--- /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;
+ }
+}
diff --git a/app/utils/validator.js b/app/utils/validator.js
index ec26bdea5..c9bdcd9cc 100644
--- a/app/utils/validator.js
+++ b/app/utils/validator.js
@@ -13,3 +13,24 @@ export const validator = (value, words) => {
remainingWords: remainingWords,
};
};
+
+export const validateWordCount = (text, wordLimits) => {
+ const trimmedText = text?.trim();
+ const wordCount = trimmedText?.split(/\s+/).filter(Boolean).length ?? 0;
+
+ const { min, max } = wordLimits;
+ if (!trimmedText) {
+ return {
+ isValid: min === 0,
+ wordCount: 0,
+ remainingToMin: min > 0 ? min : 0,
+ };
+ }
+
+ 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 };
+};