Skip to content

Commit

Permalink
feat: Implement new Final Registration (FR) Page (#883)
Browse files Browse the repository at this point in the history
## Describe your changes

The user registration page 'https://register.armada.nu/register/user'
style has been changed in order to make it more accessible since people
noticed the previous version confused some clients.

- The form is now a stepped form that allows users to keep track of the
different steps the form has, and in which one they are currently in.
- For them to select a company, a company browser has been developed to
replace the old dropdown menu which contained 1000+ companies. Now, some
companies will show up (10 companies MAX) that match what they wrote in
the browser. For that, a new API endpoint has been created.

Fixes: #853

## Checklist before requesting review

- [x] Feature/fix PRs should add one atomic (as small as possible)
feature or fix.
- [x] The code compiles and all the tests pass.

---------

Co-authored-by: anmp <[email protected]>
  • Loading branch information
ferran98campos and anmpe authored Jul 18, 2023
1 parent d6059bf commit 59cab0d
Show file tree
Hide file tree
Showing 10 changed files with 506 additions and 56 deletions.
3 changes: 3 additions & 0 deletions ais_static/images/search_icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
262 changes: 262 additions & 0 deletions ais_static/js/stepform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
//Global Variables
let currentStep;
let numberSteps;
let nextButton;
let backButton;
let submitButton;
let formSteps;
let browser;
let companyTitle;
let list;
let writingtimer;
let stepTrackerChildren;
let requiredElementsForm;
let form;
let timeout;
let noResults;

function init() {
//Get elements from DOM
numberSteps = document.getElementsByClassName('form-step').length;
formSteps = document.getElementsByClassName('form-step');
nextButton = document.getElementById('next-step-btn');
backButton = document.getElementById('back-step-btn');
submitButton = document.getElementById('submit-form-btn');
browser = document.getElementById('browser');
companyTitle = document.getElementById('company-confirmation-title');
list = document.getElementById('company-list');
stepTrackerChildren = document.getElementById('form-step-tracker').children;
form = document.getElementById('user-registration-form');
noResults = document.getElementById('company-list-no-results');

//Setting static initial values
currentStep = 0;
browser.placeholder = 'Type your company name...';
writingtimer = 500
requiredElementsForm = [];
timeout = null;

//Remove class 'no-transition' from all trackers. 'no-transition' is a CSS class that prevents animations from happening. Elements in DOM are initialized with it
//to not execute animations when loading, which would effect at the aesthetics of the webpage. Once they are load, animations can now be played.
for (let i = 0; i < stepTrackerChildren.length; i++) {
stepTrackerChildren[i].classList.remove('no-transition');
}

//Creation of an array o arrays that contains all the 'required' form input tags of each step. Will be later used for validation purposes
for (let i = 0; i < formSteps.length; i++) {
let slice = [...Array.prototype.slice.call(formSteps[i].querySelectorAll("[required]"))];
requiredElementsForm = [...requiredElementsForm, slice];
}
}

//Colors the step in green if the user has reached it
function colorStepNumber() {
for (let i = 0; i < stepTrackerChildren.length; i++) {
//If the step is greater than the current step we turn it to active
if (i > currentStep) {
stepTrackerChildren[i].classList.remove('active');
} else {
stepTrackerChildren[i].classList.add('active');
}
}
}

//Makes the 'next', 'back' and 'submit' button visible (or not) depending on the actual step. It can also modify the content of 'next' and 'back button
function setVisibleButtons(next, back, submit, confirmation) {
//Visibility
((next) ? nextButton.style.display = 'inline-block' : nextButton.style.display = 'none');
((back) ? backButton.style.display = 'inline-block' : backButton.style.display = 'none');
((submit) ? submitButton.style.display = 'inline-block' : submitButton.style.display = 'none');

//Content. The boolean variable: 'confirmation' is used to determine the content of the 'next' and 'back button
if (confirmation) {
nextButton.innerHTML = 'Yes';
backButton.innerHTML = 'No';
} else {
nextButton.innerHTML = 'Next';
backButton.innerHTML = 'Back';
}
}

//Sets the active form step depending on the number of clicks on the 'next' button
function setActiveStep() {
//Changes classes from <div> tags in DOM to make the active step visible and the rest invisible
for (let i = 0; i < numberSteps; i++) {
((currentStep == i) ? formSteps[i].setAttribute('id', 'activeStep') : formSteps[i].removeAttribute('id'));
}

//Calls the setVisibleButtons() with the specific configuration for that step
if (currentStep == 0) {
//Reset browser each time we go back to it
browser.value = '';
list.innerHTML = '';

//Shows the browser and hides the 'company confirmation' page
document.getElementById('company-confirmation').style.display = 'none';
document.getElementById('company-search').style.display = 'block';
setVisibleButtons(false, false, false, false);

} else if (currentStep == numberSteps - 1) {
setVisibleButtons(false, true, true, false);
} else {
setVisibleButtons(true, true, false, false);
}

//Color the steps to green to indicate the user in which step its currently in
colorStepNumber();
}

function validateStep(stepNumber) {
//Validates all required input tags in the current step
for (let i = 0; i < requiredElementsForm[stepNumber].length; i++) {
if (!requiredElementsForm[stepNumber][i].checkValidity()) {
requiredElementsForm[stepNumber][i].reportValidity();
return false;
}
}
return true;
}

function validate(submit) {
if (submit) {
//This part is only executed before submitting the form. It basically validates all steps again.
for (let i = 0; i < requiredElementsForm.length; i++) {

if (!validateStep(i)) {
currentStep = i;
setActiveStep();
}
}
} else {
//This part is executed only when the form is loaded. The code looks for the DOM elements with the class 'form-control-danger'
//which Django places in those form elements that were incorrect when the form was last submitted. If any of these elements
//is found, then the form jumps to the right step, so that the user knows which step needs to be corrected.
let requiredElementsFlat = requiredElementsForm.flat();
for (let i = 0; i < requiredElementsFlat.length; i++) {
if (requiredElementsFlat[i].classList.contains('form-control-danger')) {
if (requiredElementsFlat[i].type == 'password')
currentStep = 2;
else if (requiredElementsFlat[i].id == 'browser')
currentStep = 0;
else
currentStep = 1;

break;
}
}

}
}

function initBrowser() {
browser.addEventListener('keyup', function (e) {
clearTimeout(timeout);

//No Results Found text turns invisible each time a key is entered
noResults.style.visibility = 'hidden';

// Make a new timeout set to go off in 1000ms (1 second)
timeout = setTimeout(function () {


//Remove all elements from list
while (list.firstChild) {
list.removeChild(list.lastChild);
}

//Makes list not visible when the input is empty
if (browser.value.replace(/[` ~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '').length == 0) {
list.style.visibility = 'hidden'
} else {
//Fetches the top 10 companies related to the input search
fetch("http://localhost:3000/api/companies?limit=10&input=" + browser.value).then((response) => response.json()).then((json) => {

//No Results Found text turns visible if no company is returned by API
if (json.length == 0)
noResults.style.visibility = 'visible';

//Create a list element for each company returned
for (var i = 0; i < json.length; i++) {
//Name of company
const newCompany = document.createElement("span");
newCompany.innerHTML = json[i]["Organization Name"];
newCompany.value = json[i]["id"];

//Div tag that encloses the name. The span is now appended to it
const div = document.createElement("div");
div.classList.add("p-2");
div.appendChild(newCompany);

//Link tag that encloses the div
const listElement = document.createElement("a");
listElement.appendChild(div);

//Append this new object to the list
list.appendChild(listElement);

//Add an event listener to it that triggers the confirmation and sets the selected value as the company
listElement.addEventListener('click', function () {
//Set selected value as the <input> value
companyTitle.innerHTML = newCompany.innerHTML;
browser.value = newCompany.value;

//Trigger confirmation
document.getElementById('company-confirmation').style.display = 'block';
document.getElementById('company-search').style.display = 'none';
setVisibleButtons(true, true, false, true);
});
}

//Makes list visible
list.style.visibility = 'visible'
});
}
}, writingtimer);
});
}

function execute() {
init();
validate(false);
setActiveStep();
initBrowser();

nextButton.addEventListener('click', function () {
//Limiting the step amount between 0 and numberSteps
if (currentStep < numberSteps - 1 && validateStep(currentStep)) {
currentStep = currentStep + 1;
setActiveStep();
}
});

backButton.addEventListener('click', function () {
if (currentStep > 0) {
//Limiting the step amount between 0 and numberSteps
currentStep = currentStep - 1;
}
setActiveStep();
});

submitButton.addEventListener('click', function () {
//Validate all form
validate(true);
if (form.reportValidity()) {
//Submit if client-side validation is correct
form.submit();
}
});
}

(function () {

if (document.readyState !== 'loading') {
//document is already ready and JS can be executed
execute();
} else {
document.addEventListener('DOMContentLoaded', function () {
//document was not ready and JS needs to wait for DOm to be fully loaded to be executed
execute();
});
}

})();
102 changes: 101 additions & 1 deletion ais_static/registration_style.css
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ a:hover,
a:active,
a:visited {
color: #00d790;
text-decoration: underline;
cursor: pointer;
}

.btn-success,
Expand Down Expand Up @@ -171,4 +171,104 @@ input[type="tel"]:focus,
input[type="color"]:focus,
.uneditable-input:focus {
border-color: #00d790;
}

.form-step {
display: none;
}

#activeStep {
display: block;
}

#company-list {
font-size: 1.2em;
width: 100%;
position: absolute;
background-color: #ffffff;
cursor: pointer;
}

#company-list div {
padding: 15px;
}

#company-search input {
padding: 7px 8px 7px 30px;
background-image: url("images/search_icon.svg");
background-size: 13px;
background-repeat: no-repeat;
background-position: 10px center;
}

#form-step-tracker {
margin: 20px auto;
padding: 0px;
display: flex;
justify-content: center;
}

#form-step-tracker>li {
max-width: 50px;
font-size: 1rem;
margin-left: 10vw;
display: flex;
flex-direction: column;
text-align: center;
}

#form-step-tracker>li span {
position: relative;
font-weight: bold;
background: linear-gradient(to left, rgba(45, 45, 44, 20%) 50%, #00d790 50%) right;
background-size: 200%;
border-radius: 50%;
line-height: 50px;
transition: background-position .3s;
}

#form-step-tracker>li span:before {
content: "";
position: absolute;
background: linear-gradient(to left, rgba(45, 45, 44, 20%) 50%, #00d790 50%) right;
background-size: 200%;
transition: background-position .3s;
transition-delay: .3s;
top: 24px;
right: 50px;
width: 10vw;
height: 2px;
}

#form-step-tracker>li.no-transition span,
#form-step-tracker>li.no-transition span:before {
transition: none;
}

#company-confirmation {
display: none;
}

#form-step-tracker>li.active span {
background-position: left;
transition: background-position .3s;
transition-delay: .3s;
}

#form-step-tracker>li.active span:before {
background-position: left;
transition: background-position .3s;
}

#form-step-tracker>li:first-of-type {
margin-left: 0px;
}

#form-step-tracker>li:first-of-type span:before {
display: none;
}

#company-list-no-results {
color: #e73953;
visibility: hidden;
}
9 changes: 9 additions & 0 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ def exhibitor(request, exhibitor, company):
)


def companies(request, company):
return OrderedDict(
[
("Organization Name", company.name),
("id", company.id),
]
)


def event(request, event):
tags = tags_mappings(event.tags.all())
signup_link = (
Expand Down
Loading

0 comments on commit 59cab0d

Please sign in to comment.