1
\$\begingroup\$

So 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\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.