3
\$\begingroup\$

Today I decided to continue refining my JavaScript skills and utilized the Stack Exchange API (v2.2) to write a "user flair" replica.

In case you don't know what the flair that SE provides is:

profile for PerpetualJ on Stack Exchange, a network of free, community-driven Q&A sites

The process was a little tedious in places, but only because I am still learning the concept of callbacks in JavaScript and the scope of variables kept biting me hard.

/* Definitions */
var CardType = { Wheel: "wheel" }
var userCard = {
	username: '',
	profileImageUrl: '',
	reputation: 0,
	badges: {
		gold: 0,
		silver: 0,
		bronze: 0
	},
	siteUrls: []
}

/* Initial Calls */
var accountID = '13342919';
generateCard('user-flair-wheel', accountID, CardType.Wheel);

/* Required Events */
function showSitename(tooltipID, siteName) {
	var tooltip = document.getElementById(tooltipID);
	tooltip.innerHTML = siteName.replace('Stack Exchange', '');
	tooltip.classList.add('active');
}
function hideSitename(tooltipID) {
	document.getElementById(tooltipID).classList.remove('active');
}

/* UI Generation Functions */
function generateCard(containerid, accountid, cardType) {
	getAssociatedAccounts(accountID, function() {
		var className = cardType.toString().toLowerCase();
		var container = document.getElementById(containerid);
		container.classList.add("flair");
		container.classList.add(className);
		
		// Build the card.
		addProfile(container);
		addScores(container, className);
		addSites(container, className);
		container.innerHTML += '<div id="' + containerid +
									  '-tooltip" class="se-tooltip"></div>';
	});
}
function addProfile(container) {
	container.innerHTML += '<img class="user-image" src="' +
								   userCard.profileImageUrl + '"/>';
	container.innerHTML += '<h1 class="username display-4">' +
									userCard.username + '</h1>';
}
function addScores(container, cardType) {
	var badges = '<ul class="badges">';
	badges += '<li><i class="fas fa-trophy"></i> <span id="reputation-' +
				 cardType + '">' + userCard.reputation + '</span></li>';
	badges += '<li><span id="gold-badges-' + cardType + '">' +
				 userCard.badges.gold + '</span></li>';
	badges += '<li><span id="silver-badges-' + cardType + '">' +
				 userCard.badges.silver + '</span></li>';
	badges += '<li><span id="bronze-badges-' + cardType + '">' +
				 userCard.badges.bronze + '</span></li>';
	badges += '</ul>';
	container.innerHTML += badges;
}
function addSites(container, cardType) {
	var sites = '<ul id="sites-' + cardType + '" class="sites">';
	for (var i = 0; i < userCard.siteUrls.length; i++) {
		var site = '<li>';
		var siteLinkSplit = userCard.siteUrls[i].split('|');
		site += '<a href="' + siteLinkSplit[0] + '">';
		
		var tooltipID = container.id +'-tooltip';
		var linkElement = '<a href="' + siteLinkSplit[0] + '"';
		linkElement += ' onmouseover="showSitename(\'' + tooltipID + '\',\'' + siteLinkSplit[2] + '\')"';
		linkElement += ' onmouseout="hideSitename(\'' + tooltipID + '\');"';
		site += linkElement + '>';
		site += '<img src="' + (siteLinkSplit[1] == '<IMG>' ? '#' : siteLinkSplit[1]) + '"/></a></li>';
		sites += site;
	}
	
	sites += '</ul>';
	container.innerHTML += sites;
}

/* Stack Exchange API Based Functions */
function getAssociatedAccounts(accountID, callback) {
	let url = 'users/' + accountID + '/associated';
	getSEWebServiceResponse(url, function(response) {
		if (!response.items)
			return;
		
		var accounts = sortAccountsByReputation(response.items);
		var accountsProcessed = 0;
		for (let i = 0; i < accounts.length; i++) {
			let siteName = accounts[i].site_url.replace('https://', '');
			siteName = siteName.replace('.stackexchange', '');
			siteName = siteName.replace('.com', '');
			getAssociatedAccountDetails(accounts[i].user_id, siteName, accounts[i].site_name, function() {
				if (++accountsProcessed >= accounts.length)
					callback();
			});
		}
	});
}
function getAssociatedAccountDetails(userID, siteName, fullSiteName, callback) {
	let url = 'users/' + userID +'?order=desc&sort=reputation&site=' + siteName;
	getSEWebServiceResponse(url, function(response) {
		if (!response.items)
			return;
		
		let account = response.items[0];
		userCard.reputation += account.reputation;
		userCard.badges.gold += account.badge_counts.gold;
		userCard.badges.silver += account.badge_counts.silver;
		userCard.badges.bronze += account.badge_counts.bronze;
		
		if (userCard.siteUrls.length < 7) {
			var siteProfileCombo = account.link + '|<IMG>|' + fullSiteName;
			siteProfileCombo = siteProfileCombo.replace('<IMG>', getSiteIcon(siteName));
			userCard.siteUrls.push(siteProfileCombo);
		}
		if (userCard.username.length < 1)
			userCard.username = account.display_name;
		if (userCard.profileImageUrl.length < 1)
			userCard.profileImageUrl = account.profile_image;
		
		callback();
	});
}

/* Helper Functions */
function getSEWebServiceResponse(request, callback) {
	let apiRoot = 'https://api.stackexchange.com/2.2/';
	let key = 'key=s29XM)Eqn2x3YxhjLgFwBQ((';
	if (request.indexOf('?') >= 0)
		key = '&' + key;
	else
		key = '?' + key;
	
	getWebServiceResponse(apiRoot + request + key, function(response) { callback(response); });
}
function getWebServiceResponse(requestUrl, callback) {
	let request = new XMLHttpRequest();
	request.open('GET', requestUrl, true);
	request.onload = function() {
		if (request.status < 200 || request.status >= 400)
			callback("An unexpected error occurred.");
		else
			callback(JSON.parse(this.response));
	};
	request.send();
}
function sortAccountsByReputation(accounts) {
	return accounts.sort(function(a, b) { return b.reputation - a.reputation; });
}
function getSiteIcon(siteName) {
	if (siteName == "meta")
		return 'https://meta.stackexchange.com/content/Sites/stackexchangemeta/img/icon-48.png';
	
	return 'https://cdn.sstatic.net/Sites/' + siteName + '/img/apple-touch-icon.png';
}
/* Flair Styles */
.flair {
	position: relative;
	margin-top: 15px;
}
.flair > .se-tooltip {
	position: absolute;
	left: 50%;
	transform: translate(-50%);
	width: 250px;
	bottom: 50px;
	opacity: 0;
	background-color: #fff;
	color: #555;
	text-shadow: none;
	border-radius: 25px;
	padding: 5px 10px;
	box-shadow: 2px 2px 3px #0005;
}
.flair > .se-tooltip.active {
	bottom: 10px;
	opacity: 1;
}

/* Flair Wheel Styles */
.flair.wheel {
	width: 200px;
	height: 250px;
	display: flex;
	align-items: center;
	justify-content: center;
	flex-direction: column;
	text-shadow: 1px 1px 2px #0005;
}
.flair.wheel .user-image {
	width: 100px;
	height: 100px;
	border-radius: 50%;
	box-shadow: 2px 2px 3px #0005;
}
.flair.wheel .username {
	font-size: 30px;
	margin: 0;
}
.flair.wheel .badges > li > span { position: relative; }
.flair.wheel .badges > li:first-of-type > i { color: #5c9; }
.flair.wheel .badges > li:not(:first-of-type) > span::before {
	content: '';
	position: absolute;
	top: 50%;
	left: -15px;
	transform: translateY(-40%);
	width: 10px;
	height: 10px;
	border-radius: 50%;
}
.flair.wheel .badges > li:nth-child(2) > span::before { background-color: #fb3; }
.flair.wheel .badges > li:nth-child(3) > span::before { background-color: #aaa; }
.flair.wheel .badges > li:nth-child(4) > span::before { background-color: #c95; }

.flair.wheel .sites {
	position: absolute;
	top: 10px;
	left: 0;
	width: 100%;
	height: 55%;
}
.flair.wheel .sites > li { position: absolute; }
.flair.wheel .sites > li > a > img {
	width: 35px;
	height: 35px;
	background-color: #fffa;
	border-radius: 50%;
	padding: 2px;
	box-shadow: 2px 2px 3px #0005;
	cursor: pointer;
	transition: 0.3s cubic-bezier(0.5, -2.5, 1.0, 1.2) all;
}
.flair.wheel .sites > li > a:hover > img {
	width: 40px;
	height: 40px;
	background-color: #fff;
}
.flair.wheel .sites > li:nth-child(1) {
	top: -15px;
	left: 50%;
	transform: translate(-50%);
}
.flair.wheel .sites > li:nth-child(2) {
	top: 0px;
	left: 15%;
	transform: translate(-20%);
}
.flair.wheel .sites > li:nth-child(3) {
	top: 0px;
	left: 70%;
	transform: translate(-20%);
}
.flair.wheel .sites > li:nth-child(4) {
	top: 45%;
	left: 80%;
	transform: translate(-20%, -50%);
}
.flair.wheel .sites > li:nth-child(5) {
	top: 45%;
	left: -5px;
	transform: translateY(-50%);
}
.flair.wheel .sites > li:nth-child(6) {
	top: 79%;
	left: 3px;
	transform: translateY(-50%);
}
.flair.wheel .sites > li:nth-child(7) {
	top: 79%;
	right: 3px;
	transform: translateY(-50%);
}

/* Global Styles */
ul {
	padding: 0;
	listy-style-type: none;
}
ul > li {
	display: inline-block;
	padding: 0 10px;
}

/* Template Overrides */
html, body {
	background-color: #f10 !important;
	background-image: linear-gradient(45deg, #ff7, #f10) !important;
	background-image: -webkit-linear-gradient(45deg, #ff7, #f10) !important;
}
.primary-content {
	display: flex;
	flex-direction: column;
	align-items: center;
}
.primary-content > .lead { font-size: 25px; }
<link href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/2940219/PerpetualJ.css" rel="stylesheet"/>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>

<div id="primary-content" class="primary-content">
	<div id="user-flair-wheel"></div>
</div>

With regards to the callbacks that I've implemented, is my code difficult to follow here? If so, what are some things I could improve?

Note: If you decide to run the snippet, it's best viewed in the full page view as it isn't responsive yet. I also have it over on CodePen if you prefer.

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

I wouldn't say the code is difficult to follow but I do have some suggestions.

The first thing I notice is that some variables are declared with let. Many of those variables never get re-assigned. It is wise to use const for any value that shouldn't get re-assigned - even if it isn't a constant. This helps avoid accidental re-assignment.

The values in apiRoot and key are basically constants and could be moved to the top of the script. That way you won't have to go searching through the code in case you need to update the values. Also, a common convention of many c-based languages is to use ALL_CAPS for constants.

And while let and const aren't exactly features, the code could be simplified with other features like arrow functions, for...of loops, the fetch API (or a similar XHR library like axios, reqwest, etc.).

Instead of constructing HTML elements by building strings, you can use methods like document.createElement() and Node.appendchild().

I would also suggest using adding event handlers via JavaScript instead of inline in the HTML. For instance, instead of adding lines like this:

linkElement += ' onmouseover="showSitename(\'' + tooltipID + '\',\'' + siteLinkSplit[2] + '\')"';

Add event listeners to elements using element.addEventListener() - especially simple if using elements created via document.createElement() as mentioned in the previous section. You could also consider using event delegation on a parent element and handling events based on the element that was interacted with (e.g. based on class names, etc.). The advantage of this is that it keeps the logic (JS) out of the markup (HTML)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.