Skip to content

Commit

Permalink
⚡️ decrease time-to-interactive (#162)
Browse files Browse the repository at this point in the history
The navbar, the buttons to choose the response, and the comment area are now responsive before the Firebase authentication is completed.
  • Loading branch information
jsulpis authored Nov 5, 2020
1 parent da5d581 commit 9e63489
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 127 deletions.
29 changes: 17 additions & 12 deletions ui/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
</button>
</div>
<div class="navbar__about">
<button type="button" class="navbar__button hidden" id="logoutButton">
<button type="button" class="navbar__button" id="logoutButton">
<svg viewBox="0 0 24 24" class="navbar__button-emoji">
<path
d="M10.09 15.59L11.5 17l5-5-5-5-1.41 1.41L12.67 11H3v2h9.67l-2.58 2.59zM19 3H5c-1.11 0-2 .9-2 2v4h2V5h14v14H5v-4H3v4c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
Expand Down Expand Up @@ -120,16 +120,19 @@ <h2>How have you been at Zenika?</h2>
</div>
<h2>A little comment?</h2>
<textarea class="textarea" id="comment" rows="4"></textarea>
<div id="managerNotice" class="managerNotice hidden">
The result will be sent to <span id="managerName"></span>
</div>
<div id="errorDisplay" class="errorDisplay" hidden>
Please, click on one mood
</div>
<div class="button-vote-wrapper">
<button id="buttonVote" type="button" class="button--small">
Vote
</button>
<div id="sendingSectionLoader"></div>
<div id="sendingSection" class="hidden">
<div id="managerNotice" class="managerNotice">
The result will be sent to <span id="managerName"></span>
</div>
<div id="errorDisplay" class="errorDisplay" hidden>
Please, click on one mood
</div>
<div class="button-vote-wrapper">
<button id="buttonVote" type="button" class="button--small">
Vote
</button>
</div>
</div>
</div>
<div id="displayStats" class="page hidden">
Expand All @@ -139,7 +142,9 @@ <h1 class="page__title">
<select id="agencySelector"> </select>
</div>
</h1>
<h2 id="statsTitle">Stats for agency: <span id="agencyName"></span></h2>
<h2 id="statsTitle" class="hidden">
Stats for agency:&nbsp;<span id="agencyName"></span>
</h2>
<div id="statsTab"></div>
</div>
<div id="recordingPage" class="page hidden">
Expand Down
254 changes: 141 additions & 113 deletions ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,17 @@ window.addEventListener("load", async function () {
unknownEmployeePage,
statsPage
];
const userId = document.getElementById("userId")!;
const userIdElement = document.getElementById("userId")!;
const userEmail = document.getElementById("userEmail")!;
const managerNotice = document.getElementById("managerNotice")!;
const sendingSection = document.getElementById("sendingSection")!;
const sendingSectionLoader = document.getElementById("sendingSectionLoader")!;
const managerName = document.getElementById("managerName")!;
const hideClass = "hidden";

let voteData: VoteData;
let selectedAgency = "";

const htmlLoader = `
<h2>
<div class="loader">
<svg class="circular" viewBox="25 25 50 50">
<circle
Expand All @@ -71,9 +71,12 @@ window.addEventListener("load", async function () {
strokeMiterlimit="10"
/>
</svg>
</div>
Hold on, we're retrieving the data you requested ...
</h2>`;
</div>`;

const statsLoader = `<div class="loading-message">${htmlLoader} Hold on, we're retrieving the data you requested ...</div>`;

sendingSectionLoader.innerHTML = htmlLoader;
statsTab.innerHTML = statsLoader;

const show = (element: HTMLElement) => {
element.classList.remove(hideClass);
Expand All @@ -89,20 +92,21 @@ window.addEventListener("load", async function () {
show(incoming);
};

const displayStatsPage = async (agency?: string) => {
changePageTo(statsPage);
statsTitle.style.display = "none";
statsTab.innerHTML = htmlLoader;
voteData = await retrieveStatsData(agency);
statsTitle.style.display = "";
displayStatsData(voteData, agency);
};

const displayHomePage = () => {
hideAllPages();
show(homePage);
};

const renderInitialStatsPage = async () => {
voteData = await retrieveStatsData();
await fillAgenciesList();
renderStatsData(voteData);
};

const displayStatsPage = async (agency?: string) => {
changePageTo(statsPage);
};

const retrieveStatsData = async (agency?: string) => {
const db = firebase.firestore();
db.settings({ timestampsInSnapshots: true });
Expand All @@ -120,7 +124,18 @@ window.addEventListener("load", async function () {
}));
};

const fillAgenciesList = (agencyList: Set<string>) => {
const fillAgenciesList = async () => {
const statsCampaignAgency: firebase.firestore.QuerySnapshot = await firebase
.firestore()
.collection(`stats-campaign-agency`)
.get();

const agencies = new Set(
statsCampaignAgency.docs.map(
snapshot => (snapshot.data() as StatsData).agency
)
);

agencySelector.childNodes.forEach(child => {
agencySelector.removeChild(child);
});
Expand All @@ -129,15 +144,16 @@ window.addEventListener("load", async function () {
agencyName.innerText = "Global";
globalElement.setAttribute("value", "");
agencySelector.appendChild(globalElement);
agencyList.forEach(agency => {

agencies.forEach(agency => {
const element = this.document.createElement("option");
element.innerText = agency;
element.setAttribute("value", agency);
agencySelector.appendChild(element);
});
};

const displayStatsData = (voteData: VoteData, agency?: string) => {
const renderStatsData = (voteData: VoteData, agency?: string) => {
statsTab.innerHTML = renderTemplate(
computeDataFromDataBase(
agency ? filterStatsData(voteData, agency) : voteData
Expand All @@ -153,50 +169,60 @@ window.addEventListener("load", async function () {
console.error(err);
changePageTo(errorPage);
};
try {
const { session, webAuth } = await authenticateAuth0({
...AUTH0_CONFIG,
redirectUri: window.location.href
});
if (!session) return; // this means a redirect has been issued
if (!session.user.email) {
throw new Error("expected user to have email but it did not");
}
await authenticateFirebase(session);
userId.innerText = session.user.email;

logoutButton.onclick = async () => {
await signOutFirebase();
signOutAuth0(webAuth);
const saveResponse = async (response: string, comment?: string) => {
changePageTo(recordingPage);
const payload: Payload = {
vote: response
};
if (comment) {
payload.comment = comment;
}
try {
await castVote(payload);
} catch (err) {
if (err.status === "ALREADY_EXISTS") {
changePageTo(alreadyVotedPage);
} else {
errorOut(err.message);
}
return;
}
changePageTo(thankYouPage);
};

await enableFirestore(session.user.email);
show(logoutButton);
} catch (err) {
errorOut(err);
return;
}

async function enableFirestore(userId: string) {
const db = firebase.firestore();
db.settings({ timestampsInSnapshots: true });

const { campaign, alreadyVoted } = await getCurrentCampaignState();

statsButton.onclick = () => {
displayStatsPage();
const initVoteButtonsEventHandlers = () => {
let mood: string;
const buttonMap = [submitGreat, submitNotThatGreat, submitNotGreatAtAll];
submitGreat.onclick = () => {
buttonMap.forEach(button => button.classList.remove("focusButton"));
submitGreat.classList.add("focusButton");
mood = "great";
};
submitNotThatGreat.onclick = () => {
buttonMap.forEach(button => button.classList.remove("focusButton"));
submitNotThatGreat.classList.add("focusButton");
mood = "notThatGreat";
};
submitNotGreatAtAll.onclick = () => {
buttonMap.forEach(button => button.classList.remove("focusButton"));
submitNotGreatAtAll.classList.add("focusButton");
mood = "notGreatAtAll";
};

homeButton.onclick = () => {
if (!campaign) {
changePageTo(noCampaignPage);
return;
}
if (alreadyVoted) {
changePageTo(alreadyVotedPage);
voteButton.onclick = () => {
const comment = commentTextarea.value || undefined;
if (!mood) {
errorDisplay.hidden = false;
return;
}
displayHomePage();
saveResponse(mood, comment);
};
};

const initStatsButtonsEventHandlers = () => {
statsButton.onclick = () => {
displayStatsPage();
};

agencySelector.onchange = async () => {
Expand All @@ -210,22 +236,30 @@ window.addEventListener("load", async function () {
agencyName.innerText = newSelectedAgency;

selectedAgency = newSelectedAgency;
displayStatsData(
renderStatsData(
voteData,
selectedAgency === "" ? undefined : selectedAgency
);
};
const statsCampaignAgency: firebase.firestore.QuerySnapshot = await firebase
.firestore()
.collection(`stats-campaign-agency`)
.get();
fillAgenciesList(
new Set(
statsCampaignAgency.docs.map(
snapshot => (snapshot.data() as StatsData).agency
)
)
);
};

const initCampaignAndEmployeeData = async (userId: string) => {
const db = firebase.firestore();
db.settings({ timestampsInSnapshots: true });

const { campaign, alreadyVoted } = await getCurrentCampaignState();

homeButton.onclick = () => {
if (!campaign) {
changePageTo(noCampaignPage);
return;
}
if (alreadyVoted) {
changePageTo(alreadyVotedPage);
return;
}
displayHomePage();
};

if (!campaign) {
changePageTo(noCampaignPage);
Expand All @@ -252,56 +286,50 @@ window.addEventListener("load", async function () {
}
if (employee.managerEmail) {
managerName.innerText = employee.managerEmail;
show(managerNotice);
}
};

const saveResponse = async (response: string, comment?: string) => {
changePageTo(recordingPage);
const payload: Payload = {
vote: response
};
if (comment) {
payload.comment = comment;
}
try {
await castVote(payload);
} catch (err) {
if (err.status === "ALREADY_EXISTS") {
changePageTo(alreadyVotedPage);
} else {
errorOut(err.message);
}
return;
}
changePageTo(thankYouPage);
};
let mood: string;
const buttonMap = [submitGreat, submitNotThatGreat, submitNotGreatAtAll];
submitGreat.onclick = () => {
buttonMap.map(button => button.classList.remove("focusButton"));
submitGreat.classList.add("focusButton");
mood = "great";
};
submitNotThatGreat.onclick = () => {
buttonMap.map(button => button.classList.remove("focusButton"));
submitNotThatGreat.classList.add("focusButton");
mood = "notThatGreat";
};
submitNotGreatAtAll.onclick = () => {
buttonMap.map(button => button.classList.remove("focusButton"));
submitNotGreatAtAll.classList.add("focusButton");
mood = "notGreatAtAll";
};
try {
// 1 - Auth0 authentication
const { session, webAuth } = await authenticateAuth0({
...AUTH0_CONFIG,
redirectUri: window.location.href
});
if (!session) return; // this means a redirect has been issued
if (!session.user.email) {
throw new Error("expected user to have email but it did not");
}

voteButton.onclick = () => {
const comment = commentTextarea.value || undefined;
if (!mood) {
errorDisplay.hidden = false;
return;
}
saveResponse(mood, comment);
// 2 - Display the main content (Optimistic UI)
userIdElement.innerText = session.user.email;
displayHomePage();
initVoteButtonsEventHandlers();
initStatsButtonsEventHandlers();

// 3 - Firebase authentication
await authenticateFirebase(session);

// 4 - Now that the user is fully logged in, they can logout
logoutButton.onclick = async () => {
await signOutFirebase();
signOutAuth0(webAuth);
};

changePageTo(homePage);
// 5 - Load data for both pages in parallel
initCampaignAndEmployeeData(session.user.email)
.then(() => {
hide(sendingSectionLoader);
show(sendingSection);
})
.catch(errorOut);

renderInitialStatsPage()
.then(() => {
show(statsTitle);
})
.catch(errorOut);
} catch (err) {
errorOut(err);
return;
}
});
Loading

0 comments on commit 9e63489

Please sign in to comment.