Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
128 changes: 128 additions & 0 deletions app/components/new-join-steps/base-step.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { action } from '@ember/object';
import { debounce } from '@ember/runloop';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { JOIN_DEBOUNCE_TIME } from '../../constants/join';
import { validateWordCount } from '../../utils/validator';
import { scheduleOnce } from '@ember/runloop';
import { getLocalStorageItem, setLocalStorageItem } from '../../utils/storage';

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

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

get storageKey() {
return '';
}

postLoadInitialize() {}

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

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

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

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

this.postLoadInitialize();

const valid = this.isDataValid();
this.args.setIsPreValid(valid);
setLocalStorageItem('isValid', String(valid));
}

@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));
}
}
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}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field is disabled but has @onInput handler which will never fire.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the field is meant to be read only but passing onInput here since it was throwing an error if onInput is undefined

Uncaught (in promise) Error: You must pass a function as the second argument to the on modifier; you passed undefined.

i didn't want to change in reusables input box directly since it's being used at multiple places and it might cause some issues

@required={{true}} @value={{this.data.fullName}} @onInput={{this.inputHandler}} />

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

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

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

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