Skip to content

Instantly share code, notes, and snippets.

@TheCoderRaman
Created March 31, 2020 05:47
Show Gist options
  • Save TheCoderRaman/5f9503b930bfa8908e436dbb5bd5cbdc to your computer and use it in GitHub Desktop.
Save TheCoderRaman/5f9503b930bfa8908e436dbb5bd5cbdc to your computer and use it in GitHub Desktop.
Six Degrees of CodePen
<div class="page-wrap">
<div id="start-screen" class="offscreen-thingo">
<div class="intro pseudo-underline pseudo-underline--huge">
<p class="lead">
Six degrees of separation is the theory that everyone in the world is connected by six or fewer steps.
</p>
<p>As it turns out, it's mostly true for CodePen! Given a "follower" user and a "followee" user, this will attempt to find the shortest link between the two.</p>
</div>
<div class="usercards-wrap"></div>
<button class="start">Calculate</button>
</div>
<div id="result-screen" class="down hidden offscreen-thingo">
<div class="card">
<div class="head-to-head">
<div class="head-to-head__user">
<img id="head-to-head__follower-avatar"class="head-to-head__avatar" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/186499/default-avatar.png" alt="avatar" />
<div class="head-to-head__text-wrap">
<h2 class="head-to-head__name" id="head-to-head__follower-nicename">
Name
</h2>
<div class="head-to-head__username" id="head-to-head__follower-username">
@username
</div>
</div>
</div>
<div class="head-to-head__user">
<img id="head-to-head__followee-avatar" class="head-to-head__avatar" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/186499/default-avatar.png" alt="avatar" />
<div class="head-to-head__text-wrap">
<h2 class="head-to-head__name" id="head-to-head__followee-nicename">
Name
</h2>
<div class="head-to-head__username" id="head-to-head__followee-username">
@username
</div>
</div>
</div>
</div>
<div class="result-wrapper" id="result-wrapper">
<div class="counter offscreen-thingo" id="counter">
<div class="heading">Doin' Work...</div>
<div class="counter__number pseudo-underline pseudo-overline">0</div>
<div class="heading">profiles checked so far</div>
</div>
<div class="offscreen-thingo hidden down" id="success-screen">
<h3 class="heading important-heading pseudo-underline">Match found!</h3>
<div id="pair-wrap"></div>
<p><a href="#0" class="js-reset"><strong>Again?</strong></a></p>
</div>
<div class="offscreen-thingo hidden down" id="fail-screen">
<h3 class="heading important-heading pseudo-underline">Awkward.</h3>
<p>No match found. <a class="js-reset" href="#0">Give it another go?</a></p>
</div>
</div>
</div>
</div>
</div>
<footer>
<p>Powered by the<br>CodePen Community ❤</p>
<a target="_blank" href="https://codepen.io/natewiley/">Nate Wiley</a> // <a target="_blank" href="http://cpv2api.com/">cpv2api</a><br>
<a target="_blank" href="https://codepen.io/jackrugile/">Jack Rugile</a> // <a target="_blank" href="https://jackrugile.com/jrumble/">jRumble</a><br>
<a target="_blank" href="https://codepen.io/rstacruz/">Rico Sta. Cruz</a> // <a target="_blank" href="https://ricostacruz.com/nprogress/">NProgress</a>
</footer>
<div class="templates">
<div id="usercard-template">
[[[https://codepen.io/alexzaworski/pen/c2e7f450229661ce3c1cf24013b95dad]]]
</div>
<div id="profile-pair">
[[[https://codepen.io/alexzaworski/pen/0f3eeacf34490d174de653e33979bef3]]]
</div>
</div>
/**
* > try out this es2015 thing
* > get 10 hours into project
* > read that `class` is bad practice
* > try not to cry
* > cry a lot
*/
/**
* Minimal wrapper class for Nate Wiley's cpv2api (http://cpv2api.com/).
* It's super specific to this app and pretty fragile, I wouldn't recommend using it elsewhere.
*/
class CPV2 {
constructor() {
this.apiURL = "https://cpv2api.herokuapp.com/";
this.throttler = new AjaxThrottler();
}
/**
* Gets a user's profile
* @param {string} user - username of user to be retrieved
* @param {function} callback
*/
getUser(user, callback) {
this.throttler.addRequest({
url: this.apiURL + "profile/" + user,
success: r => {
callback(r);
}
});
}
/**
* Gets a follow count (whether following or followers) for a user.
* @param {string} user - username of user to be checked
* @param {string} key - key of data to be returned ('following' or 'followers')
* @param {function} callback - uses the found count as arg
*/
getCount(user, key, callback) {
this.getUser(user, r => {
if (r.success) {
callback(parseInt(r.data[key]));
}
// User not found. Mostly (only?) happens with teams, which would need
// to be handled seperately. Currently, teams aren't supported by cpv2api
else {
callback(0);
}
});
}
/**
* Recursively loops through a grid and returns all pages of data
* @param {number} [page = 1] - current page of grid
* @param {string} endpoint - endpoint of grid to be looked up
* @param {function} eachPage - fired after each page has been retrieved
* @param {function} onComplete - fired once all pages have been retrieved
*/
getAllPages({page = 1, endpoint, eachPage, onComplete}) {
let data = new Set();
const url = this.apiURL + endpoint + "?page=" + page;
this.throttler.addRequest({
url: url,
error: (e) => {
console.error("Something broke, try again? (" + e.statusText + ")"); // ...lol
},
success: (response) => {
if (response.success) {
response.data.forEach(user => {
data.add(user);
});
eachPage(response, data);
if (this.bailEarly) {
this.bailEarly = false;
return;
} else {
this.getAllPages({
page: page + 1,
endpoint: endpoint,
eachPage: eachPage,
onComplete: onComplete,
});
}
} else {
onComplete();
}
}
});
}
}
/**
* Just in case there's some situation where hundreds of requests
* are trying to fire at once, this ensures that only a somewhat
* sane amount are actually going off at the same time.
*
* In extreme situations the browser can actually just crash without this.
*/
class AjaxThrottler {
constructor() {
this.requestsBeforeThrottle = 10;
this.pendingRequests = [];
this.queuedRequests = [];
}
/**
* Adds a request to the queue
* @param {object} requestSettings - The settings to be used for the ajax request (uses $.ajax)
*/
addRequest(requestSettings) {
requestSettings.beforeSend = jqXHR => {
this.pendingRequests.push(jqXHR);
};
requestSettings.complete = jqXHR => {
const index = this.pendingRequests.indexOf(jqXHR);
this.pendingRequests.splice(index, 1);
this.processNext();
};
if (this.pendingRequests.length < this.requestsBeforeThrottle) {
$.ajax(requestSettings);
} else {
this.queuedRequests.push(requestSettings);
}
}
processNext() {
const request = this.queuedRequests.shift();
if (request) {
$.ajax(request);
}
}
}
/**
* Loops over a set of items, calling an asynchronous function
* using each item as an argument.
*
* Once those functions have all completed, executes a callback.
*
* @param {function} loopFunc - the fuction to be executed. All items queued will be passed as an argument.
* @param {funciton} killFunc - function executed after the queue has been terminated manually
* @param {function} callback - executed once *all* items have been processed
*
* This whole mess needs to be refactored 'cause right now you need to pass in
* random functions that should really be contained in the class.
*/
class AsyncQueue {
constructor({loopFunc, callback, killFunc}) {
this.items = new Set();
this.loopFunc = loopFunc;
this.callback = callback;
this.killFunc = killFunc;
}
/**
* Adds an item to the queue
* @param {*} item - item to be added to the queue. Will be passed as an argument to loopFunc
*/
addItem(item) {
this.items.add(item);
}
/**
* Removes an item from the queue. If no items remain, fires the queue's callback.
* @param {*} item - item to be removed from the queue
*/
removeItem(item) {
this.items.delete(item);
if (this.items.size === 0) {
this.callback();
}
}
/**
* Removes everything from the queue and executes the kill function
*/
kill() {
this.items.forEach(item => { this.removeItem(item); });
this.removeItem = () => { return; };
this.killFunc();
}
/**
* Loops through all items currently in the queue.
*/
process() {
this.items.forEach(item => {
this.loopFunc(item);
});
}
}
/**
* Basically just a tree node. But calling them nodes feels so impersonal :(
*/
class User {
constructor({username, nicename, avatar, parent = null}) {
this.username = username;
this.nicename = nicename;
this.avatar = avatar;
this.parent = parent;
this.link = $("<a target=\"_blank\">");
this.link.text("@" + this.username);
this.link.attr("href", "https://codepen.io/" + this.username);
}
}
/**
* Stores users. Keeps track of all current usernames
* in a set for super fast searching, as well as keeping
* an array of User instances to track which level
* of the tree each user came from.
*/
class UserStore {
constructor() {
this.userSet = new Set();
this.levels = [];
this.addLevel();
}
/**
* @param {User} user - the user to store
*/
addUser(user) {
if (this.userSet.has(user.username)) {
return;
}
this.userSet.add(user.username);
this.currentLevel.push(user);
}
/**
* Creates an empty array and inserts it into the array of levels
*/
addLevel() {
this.currentLevel = [];
this.levels.push(this.currentLevel);
}
}
/**
* Just pretend this is somewhere in the codebase that makes
* any sort of logical sense thx~
*
* (it's the ticker for counting profiles searched)
*/
class Counter {
constructor(el) {
this.el = el;
this.queue = 0;
this.totalCount = 0;
this.animating = false;
}
add() {
this.queue++;
if (!this.animating) {
this.animating = true;
this.raf = requestAnimationFrame(()=> {this.animate();});
}
}
animate() {
if (this.queue > 0) {
const diff = Math.ceil(this.queue / 10);
this.queue -= diff;
this.totalCount += diff;
this.el.text(this.totalCount);
this.raf = requestAnimationFrame(()=> {this.animate();});
} else {
cancelAnimationFrame(this.raf);
this.animating = false;
}
}
}
/**
* Overall tree of users. Does all the user stuff. users.
* @param {string} username - the parent of this tree
* @param {string} endpoint - the endpoint this string will use to add new levels ('followers' or 'following')
*/
class UserTree {
constructor(user, endpoint, counter) {
this.endpoint = endpoint;
this.userStore = new UserStore();
this.userStore.addUser(user);
this.counter = counter;
}
/**
* Adds a new level to the tree
* @param {function} callback
* @param {function} [killEarlyTest] - checks if the loop should bail early
*/
addLevel(callback, killEarlyTest) {
// Sets up a new queue that will loop through *all* users in the current level,
// and add *their* followers/followees (depending on the tree's endpoint)
// to a new level
const queue = new AsyncQueue({
loopFunc: user => {
cpv2.getAllPages({
endpoint: this.endpoint + "/" + user.username,
eachPage: (response, data) => {
data.forEach(newUser => {
this.counter.add();
this.userStore.addUser(new User({
username: newUser.username,
nicename: newUser.nicename,
avatar: newUser.avatar,
parent: user
}));
});
if (killEarlyTest()) {
queue.kill();
}
},
onComplete: () => {
queue.removeItem(user);
}
});
},
killFunc: () => { cpv2.bailEarly = true; },
callback: callback
});
this.userStore.currentLevel.forEach(user => {
queue.addItem(user);
});
this.userStore.addLevel();
queue.process();
}
/**
* Gets the number of users in the next level without actually traversing it.
* This is useful because there is huge time saving potential -- often times
* checking ~50 users can save us from having to loop through hundreds if not thousands.
* @param {function} callback - will be called with the found amount as arg
*/
getAmountInNextLevel(callback) {
const level = this.userStore.currentLevel;
// If we've already looped through this level we don't
// need to waste time doing so again
if (this.cachedLevel && this.cachedLevel.level == level) {
callback(this.cachedLevel.count);
return;
}
// Okay this is dumb as hell but basically we're just gonna skip this
// step if the number of users we'd have to loop through is too high.
// If we don't we'll waste a ton of time counting everything and frankly
// if there's this large of a level it's gonna take a while anyways.
//
// Choosing 60 is totally arbitrary. The right way to do this is probably
// to set up some sorta timeout but if everything else wasn't evidence
// enough, I'm clearly on a mission to do things the most painful way possible.
if (level.length > 60) {
callback(99999);
return;
}
// Otherwise if there's a sane amount of users to loop through
// we'll go ahead and do it.
let count = 0;
const queue = new AsyncQueue({
loopFunc: user => {
cpv2.getCount(user, this.endpoint, currentCount => {
count += currentCount;
queue.removeItem(user);
this.counter.add();
});
},
callback: () => {
this.cachedLevel = {
level: level,
count: count
};
callback(count);
}
});
level.forEach(user => {
queue.addItem(user.username);
});
queue.process();
}
}
/**
* Handles all of the logic between comparing two users and is ultimately
* responsible for finding a match if one exists.
* @param {string} followee - username of the tree that will be built of followees
* @param {string} follower - username of the tree that will be built of followers
* @param {function} success - function called if a match is found. Uses the match as arg
* @param {function} failure - function to be called if no match exists
* @param {Counter} counter - counter to increment each time a user is looked up
*/
class TreeComparer {
constructor({follower, followee, success, failure, counter}) {
this.followerTreeParent = this.follower;
this.followingTreeParent = this.followee;
this.success = success;
this.failure = failure;
this.followerTree = new UserTree(followee, "followers", counter);
this.followingTree = new UserTree(follower, "following", counter);
this.loop();
}
/**
* Main comparison loop. If a match is found or a tree is dead, stops the loop.
* Otherwise keeps this party goin'
*/
loop() {
const match = this.checkForMatches();
if (match) {
this.buildMatchChain(match);
} else if (this.followerTree.userStore.currentLevel.length === 0 ||
this.followingTree.userStore.currentLevel.length === 0) {
this.giveUp();
} else {
this.advanceTreeWithShorterLevel();
}
}
giveUp() {
this.failure();
}
/**
* Looks at each tree to determine the size of the *next* level, and then adds
* a level to the smaller of the two. Calls loop() when it's done.
*
* Basically this allows us to make as few requests as possible.
* For example, consider two users: A (following 5k people) and B (10 followers).
* If I want to know if A is following B, it's a lot faster to check
* _B's followers_ rather than _A's followees_.
*
* Over time this can save us *hundreds* of requests.
*/
advanceTreeWithShorterLevel() {
this.followingTree.getAmountInNextLevel(followingCount => {
this.followerTree.getAmountInNextLevel(followerCount => {
let treeToAdvance;
if (followingCount < followerCount) {
treeToAdvance = this.followingTree;
} else {
treeToAdvance = this.followerTree;
}
treeToAdvance.addLevel(() => { this.loop(); }, () => { return this.checkForMatches(); });
});
});
}
/**
* Looks for matches across the two trees
* @returns {User|Boolean} match - returns the matched user if found, otherwise returns false
*/
checkForMatches() {
return (this.checkForDirectMatch() || this.checkForIndirectMatch() || false);
}
/**
* Checks whether the opposing user set contains the target username
* (if it does, we found them directly and don't need to loop through arrays looking for an indirect match)
* @returns {User|Boolean} match - returns the matched user if found, otherwise returns false
*/
checkForDirectMatch() {
// Bail early if we can 'cause this is faster to calculate than looping through arrays
if (!this.followingTree.userStore.userSet.has(this.followerTreeParent) &&
!this.followerTree.userStore.userSet.has(this.followingTreeParent)) {
return false;
}
// Once we know we have a match we still need to go and find it. Not sure
// if it's faster to search the smaller level first or vice versa,
// might be worth benchmarking at some point.
const followingTreeCurrent = this.followingTree.userStore.currentLevel;
for (let i = 0; i < followingTreeCurrent.length; i++) {
if (followingTreeCurrent[i].username == this.followerTreeParent) {
return {followingMatch: followingTreeCurrent[i]};
}
}
const followerTreeCurrent = this.followerTree.userStore.currentLevel;
for (let i = 0; i < followerTreeCurrent.length; i++) {
if (followerTreeCurrent[i].username == this.followingTreeParent) {
return {followerMatch: followerTreeCurrent[i]};
}
}
}
/**
* Checks for overlap across the most recent levels of each tree
* @returns {User|Boolean} match - returns the matched user if found, otherwise returns false
*/
checkForIndirectMatch() {
const followingTreeCurrent = this.followingTree.userStore.currentLevel;
const followerTreeCurrent = this.followerTree.userStore.currentLevel;
for (let i = 0; i < followingTreeCurrent.length; i++) {
for (let j = 0; j < followerTreeCurrent.length; j++) {
if (followingTreeCurrent[i].username == followerTreeCurrent[j].username) {
return ({followingMatch: followingTreeCurrent[i], followerMatch: followerTreeCurrent[j]});
}
}
}
}
/**
* Constructs an ordered array from follower to followee.
* @param {User} [followingMatch=null] - found match on the following side
* @param {user} [followerMatch=null] - found match on the followee side
*/
buildMatchChain({followingMatch = null, followerMatch = null}) {
let matchArray = [];
if (followingMatch) {
while (followingMatch.parent) {
matchArray.unshift(followingMatch);
followingMatch = followingMatch.parent;
}
matchArray.unshift(followingMatch);
}
if (followerMatch) {
// if the array is empty (ie no following match) this won't do anything,
// if it's not empty it'll trim the inevitable duplicate
matchArray.splice(-1, 1);
while (followerMatch.parent) {
matchArray.push(followerMatch);
followerMatch = followerMatch.parent;
}
matchArray.push(followerMatch);
}
// Lets the app know there's a successful match
// (and passes it the data so it can build out the UI)
this.success(matchArray);
}
}
/**
* Handles the initial selection of a user
* @param {string} type - whether the user is a follower or followee
*/
class UserUI {
constructor(type) {
this.type = type;
this.template = $("#usercard-template").find(".usercard").clone();
this.id = this.type.toLowerCase();
this.template.attr("id", this.id);
this.setupElements();
this.bindEvents();
this.ready = false;
$(".usercards-wrap").append(this.template);
// thx Jake https://codepen.io/jakealbaugh/pen/vOVVqG/ (who got it from Nate Wiley I guess? either way xoxo)
this.randomUserList = ["thebabydino","tmrDevelops","pixelass","WhiteWolfWizard","natewiley","oknoblich","bennettfeely","jackrugile","hugo","LukyVj","towc","yoksel","dudleystorey","lukerichardville","Hornebom","netsi1964","nakome","berdejitendra","kenjiSpecial","katydecorah","rlemon","abergin","chrisgannon","ge1doot","lbebber","loktar00","Sonick","FWeinb","juanbrujo","andreasstorm","zadvorsky","tholman","Mombasa","fixcl","judag","MyXoToD","EduardoLopes","Zeaklous","HugoGiraudel","suez","grayghostvisuals","satchmorun","rileyjshaw","dope","gbnikolov","jakealbaugh","hakimel","elrumordelaluz","Mamboleoo","rachsmith","chrisota","kevinjannis","Kseso","TimPietrusky","ImagineProgramming","enxaneta","SitePoint","joshnh","KyleDavidE","brbcoding","TimLamber","raurir","rafaelcastrocouto","joe-watkins","alexsafayan","leemark","samarkandiy","desandro","the_ruther4d","sdras","DonKarlssonSan","carpenumidium","ettrics","pmk","jakob-e","lonekorean","pouretrebelle","jshawl","wontem","tystrong","ScottMarshall","akwright","laviperchik","sol0mka","Pesca","mariusbalaj","chriscoyier","scottkellum","donovanh","GreenSock","code_dependant","zerospree","grgrdvrt","markmurray","Thibaut","unmeshpro","davatron5000","jorgeatgu","trhino","Dreamdealer","maggiben","soulwire","32bitkid","waddington","rickyeckhardt","yukulele","egrucza","Francext","winkerVSbecks","nicolazj","schoenwaldnils","pcameron","indyplanets", "mariemosley", "dervondenbergen","fbrz","seanseansean","mikehobizal","joshbader","zachernuk","nicoptere","noahblon","daneden","cx20","codeandcam","roborich","gastonfig","simeydotme","vineethtr","gpyne","Ruddy","wenbin5243","shubhra","hynden","acarva1","martinwolf","SaschaSigl","kaliedarik","stefanjudis","Xanmia","noeldelgado","Michiel","simurai","timohausmann","GabbeV","boltaway","pixelthing","airnan","MichaelArestad","ykob","Metty","DeptofJeffAyer","atelierbram","jjmartucci","creme","soulrider911","designcouch","ZevanRosser","dehash","geoffyuen","davilious","msurguy","hans","dissimulate","edankwan","satcy","chinchang","trajektorijus","mladen___","johnie","kman","tjoen","chrisnager","AmeliaBR","yusufbkr","jonitrythall","rachelwong","beesandtrees","jpod","cchambers","auginator","frytyler","jessenwells","robertmesserle","Oka","naoyashiga","mariosmaselli","XDBoy018","larrygeams","BrianDGLS","moklick","andytran","CrocoDillon","msval","pankajparashar","jonigiuro","MarcMalignan","jeroens","ludviglindblom","sakri","keithclark","ajerez","mallendeo","alexdevero","jurbank","brownerd","jcoulterdesign","potatoDie","shakdaniel","marian-cojoc-ro","rachelnabors","uriuriuriu","virgilpana","zachacole","bronsrobin","daless14","Elbone","ZCKVNS","vsync","pirrera","matt-west","long-lazuli" ,"frxnz","Lewitje","amcharts","yy","Aldlevine","jxnblk","icebob","PageOnline", "terrymun","icutpeople","prowebix","bali_balo","fusco","jaflo","boylett","adamjld","brandonbrule","chris-creditdesign","nickmoreton","mknadler","igcorreia","scrimothy","rss","run-time","jlong","macreart","achudars","ssh","cjgammon","ControlledChaos","monstersaurous","christian-fei","captainbrosset","Funsella","kevingimbel","onediv","s","rlacorne","Yakudoo","drew_mc","shshaw","michaellee","ThisIsJohnBrown","chrislaarman","jotavejv","tdevine33","ionic","pwsm50","shadeed","georgehastings","ademilter","keithwyland","khadkamhn","rikschennink","bphillips201","zitrusfrisch","jhamon","andersschmidt","Rplus","chrishutchinson","Zaku","jsbrown","kowlor","paintbycode", "quezo", "souporserious","Guilh", "alexzaworski"];
}
reset() {
this.ready = false;
this.template.after(this.templateBackup);
this.template.remove();
this.template = this.templateBackup;
this.templateBackup = this.template.clone();
this.setupElements();
this.bindEvents();
}
setupElements() {
this.templateBackup = this.template.clone(true);
this.form = this.template.find(".usercard__form");
this.avatar = this.template.find(".usercard__avatar");
this.heading = this.template.find(".usercard__name");
this.subhead = this.template.find(".usercard__username");
this.heading.text(this.type);
this.subhead.text("@" + this.id);
this.input = this.template.find(".usercard__input");
this.inputRow = this.template.find(".usercard__input-wrap");
this.stats = this.template.find(".usercard__userstats");
this.back = this.template.find(".usercard__back");
this.random = this.template.find(".usercard__random");
this.followingStat = this.template.find(".js-following-stat");
this.followerStat = this.template.find(".js-follower-stat");
this.inputRow.jrumble({
x: 3,
y: 0,
rotation: 0,
});
}
bindEvents() {
this.form.on("submit", e => { this.handleFormSubmit(e); });
this.back.click(() => { this.reset(); });
this.random.click(() => {
const index = Math.floor(Math.random() * this.randomUserList.length);
this.input.val(this.randomUserList[index]);
this.input.trigger("input");
});
}
handleFormSubmit(e) {
e.preventDefault();
NProgress.configure({parent: "#" + this.id});
NProgress.start();
cpv2.getUser(this.input.val(), r => {
if (r.success && r.data.username) {
this.handleUser(r.data);
} else {
this.handleInvalidUser();
}
});
}
handleInvalidUser() {
this.input.addClass("invalid");
this.inputRow.trigger("startRumble");
setTimeout(() => {
this.inputRow.trigger("stopRumble");
}, 400);
this.input.off("input");
this.input.one("input", () => {
this.input.removeClass("invalid");
});
NProgress.done();
}
handleUser(userData) {
this.user = new User({
username: userData.username,
nicename: userData.nicename,
avatar: userData.avatar
});
// grab the image first since that's the only thing that
// actually has to download
this.avatar.attr("src", userData.avatar);
this.avatar.one("load error", () => {
NProgress.done();
this.subhead.html(this.user.link);
this.heading.text(this.user.nicename);
this.followerStat.text(userData.followers);
this.followingStat.text(userData.following);
this.prepUser();
this.ready = true;
});
}
prepUser() {
this.stats.addClass("prepped");
this.heading.addClass("prepped");
this.avatar.addClass("prepped");
this.subhead.addClass("prepped");
this.revealUser();
}
revealUser() {
this.form.addClass("hidden");
// just kinda feels right if it's delayed a bit
setTimeout(()=> { this.back.addClass("active"); }, 100);
// forces repaint so animations all fire
this.template.find(".prepped").hide().show(0).addClass("active");
}
isReady() {
if (!this.ready) {
this.handleInvalidUser();
return false;
}
return true;
}
}
class App {
constructor() {
this.cache();
this.init();
}
cache() {
this.cache = $("body").clone();
}
init() {
this.follower = new UserUI("Follower");
this.followee = new UserUI("Followee");
this.startButton = $(".start");
this.startScreen = $("#start-screen");
this.resultScreen = $("#result-screen");
this.successScreen = $("#success-screen");
this.resultWrap = $("#result-wrapper");
this.counter = $("#counter");
this.failScreen = $("#fail-screen");
this.resetButton = $(".js-reset");
this.bindEvents();
}
restoreCache() {
$("body").replaceWith(this.cache.clone());
this.init();
}
// I am the undisputed champion of
// semantic naming conventions
hideOffscreenThingo(thingo, callback) {
thingo.addClass("hiding");
thingo.one("transitionend", () => {
thingo.addClass("hidden down").removeClass("hiding");
if (callback) {
callback();
}
});
}
showOffscreenThingo(thingo, callback) {
thingo.removeClass("hidden");
thingo.hide().show(0).removeClass("down");
if (callback) {
callback();
}
}
swapOffscreenThingos(currentThingo, newThingo, callback) {
this.hideOffscreenThingo(currentThingo, ()=> {
this.showOffscreenThingo(newThingo, () => {
if (callback) {
callback();
}
});
});
}
getHiddenThingoHeight(thingo) {
thingo.removeClass("hidden");
const height = thingo.outerHeight(true);
thingo.addClass("hidden");
return height;
}
prepResultScreen() {
NProgress.configure({
parent: "#" + this.resultScreen.attr("id"),
trickleSpeed: 800
});
NProgress.start();
$("#head-to-head__follower-avatar").attr("src", this.follower.user.avatar);
$("#head-to-head__follower-username").html(this.follower.user.link.clone());
$("#head-to-head__follower-nicename").text(this.follower.user.nicename);
$("#head-to-head__followee-avatar").attr("src", this.followee.user.avatar);
$("#head-to-head__followee-username").html(this.followee.user.link.clone());
$("#head-to-head__followee-nicename").text(this.followee.user.nicename);
}
bindEvents() {
this.resetButton.click(() => {
this.restoreCache();
});
this.startButton.click(() => {
if (this.follower.isReady() && this.followee.isReady()) {
this.swapOffscreenThingos(this.startScreen, this.resultScreen, () => {
this.prepResultScreen();
const treeComparer = new TreeComparer({
follower: this.follower.user,
followee: this.followee.user,
counter: new Counter($(".counter__number")),
success: (matchArray) => {
NProgress.done();
this.handleMatch(matchArray);
},
failure: () => {
NProgress.done();
this.handleNoMatch();
}
});
});
}
});
}
handleMatch(matchArray) {
const wrap = $("#pair-wrap");
const template = $("#profile-pair").find(".profile-pair");
// ... lol
if (matchArray.length == 1) {
matchArray.push(matchArray[0]);
template.find(".follows-text").text("is literally");
}
for (let i = 0; i < matchArray.length - 1; i++) {
let pair = template.clone();
const follower = pair.find(".profile-pair__follower");
const followee = pair.find(".profile-pair__followee");
follower.find(".profile-pair__avatar").attr("src", matchArray[i].avatar);
follower.find(".profile-pair__details__name").text(matchArray[i].nicename);
follower.find(".profile-pair__details__username").html(matchArray[i].link.clone());
followee.find(".profile-pair__avatar").attr("src", matchArray[i + 1].avatar);
followee.find(".profile-pair__details__name").text(matchArray[i + 1].nicename);
followee.find(".profile-pair__details__username").html(matchArray[i + 1].link.clone());
wrap.append(pair);
}
this.displayResults(this.successScreen);
}
handleNoMatch() {
this.displayResults(this.failScreen);
}
displayResults(resultScreen) {
// ... I don't know why this needs 8 added pls help
this.resultWrap.height(this.resultWrap.outerHeight(true) + 8);
this.resultWrap.height(this.getHiddenThingoHeight(resultScreen));
this.hideOffscreenThingo(this.counter, () => {
this.showOffscreenThingo(resultScreen);
});
}
}
// init stuff
NProgress.configure({
speed: 140,
});
const cpv2 = new CPV2();
const app = new App();
// footer was covering UI/looked bad in CodePen grids
if (!!window.location.pathname.match(/fullcpgrid/i)) {
$("footer").hide();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0/jquery.min.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/186499/jquery.jrumble.1.3.min.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/186499/nprogress.js"></script>

Six Degrees of CodePen

Six degrees of separation is the theory that everyone in the world is connected by six or fewer steps.

As it turns out, it's mostly true for CodePen -- we're a pretty connected bunch :) Given a "follower" user and a "followee" user, this will attempt to find the shortest link between the two.

So far, the largest successful chain I have ever found is 7. Most unsuccessful chains are caused by the initial users having 0-2 followers/followees. I have yet to find a Chris Coyier number (ie Chris Coyier as the follower) higher than 3. Dude follows a lot of people.


Powered by the CodePen Community ❤

Nate Wiley // cpv2api
Jack Rugile // jRumble
Rico Sta. Cruz // NProgress

A Pen by Alex Zaworski on CodePen.

License.

$xltgray: #F0F5F8;
$ltgray: #DBE6EC;
$brtblue: #3F92DE;
$brtgreen: #6aca6a;
$ltpurple: #9492d5;
$dkpurple: #282741;
$xdkpurple: #1E202F;
$reddish: #F73D63;
$offscreen-distance: 160px;
$bezier: "cubic-bezier(.4,0,0,1)";
$bezier-bounce: "cubic-bezier(.4, 0, 0, 1.425);";
$bezier-in: "cubic-bezier(0, 0, .2, 1);";
$bezier-out: "cubic-bezier(.4, 0, 1, 1);";
$revealSpeed: .1s;
@mixin button($color) {
display: block;
border: 1px solid darken($color, 15%);
color: #fff;
letter-spacing: .02em;
font-weight: bold;
border-radius: 2px;
width: 100%;
background: $color;
&:focus {
outline-width: 2px;
outline-color: lighten($color, 8%);
}
&:hover:not(:active) {
background: saturate(lighten($color, 5%), 5%);
}
}
html {
box-sizing: border-box;
}
*,
*:after,
*:before {
box-sizing: inherit;
}
body {
display: flex;
flex-direction: column;
height: 100vh;
font-family: "Open Sans", sans-serif;
color: $ltpurple;
background: $xdkpurple;
padding: 16px;
}
a {
color: $brtblue;
text-decoration: none;
&:hover {
color: lighten($brtblue, 7%);
}
}
p {
margin-bottom: 16px;
}
.intro {
font-size: .85rem;
margin-bottom: 32px;
line-height: 1.6;
}
.lead {
margin-top: 0;
font-size: 1.2em;
color: lighten($ltpurple, 15%);
}
.page-wrap {
margin: auto;
width: 540px;
flex-shrink: 0;
}
.usercards-wrap {
display: flex;
margin-bottom: 24px;
}
.card {
transition: height .25s #{$bezier};
background: linear-gradient(lighten($dkpurple, 5%), $dkpurple);
padding: 24px;
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.25);
border-radius: 4px;
overflow: hidden;
.card {
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
background: linear-gradient(lighten($dkpurple, 8%), lighten($dkpurple, 5%));
}
}
.usercard {
&:first-of-type {
margin-right: 24px;
}
margin: auto;
flex-basis: 50%;
position: relative;
}
.usercard__avatar-wrap {
width: 130px;
height: 130px;
background-size: cover;
display: block;
border-radius: 50%;
margin: 0 auto;
margin-bottom: 16px;
background-image: url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/186499/default-avatar.png");
position: relative;
}
.usercard__avatar {
display: block;
width: 100%;
height: 100%;
position: absolute;
top: 50%;
left: 50%;
border-radius: 50%;
transform: translate(-50%, -50%) scale(0);
transition: transform $revealSpeed*1.45 #{$bezier-bounce};
&.active {
background-color: white;
transform: translate(-50%, -50%) scale(1);
}
}
.usercard__back {
background: none;
display: none;
position: absolute;
top: 24px;
left: 24px;
padding: 0;
border: 0;
stroke: $ltpurple;
opacity: .35;
cursor: pointer;
&.active {
display: block;
}
&:hover {
opacity: .75;
}
&:focus {
opacity: 1;
outline: none;
stroke: $brtblue;
}
&:active {
stroke: $ltpurple;
}
}
.usercard__username {
font-size: .9rem;
text-align: center;
margin-bottom: 16px;
}
.usercard__name {
font-size: 1rem;
margin-top: 0;
margin-bottom: 4px;
text-align: center;
color: white;
}
.usercard__name,
.usercard__username {
&.prepped {
transform: translateY($offscreen-distance);
opacity: 0;
}
&.active {
opacity: 1;
transition: all $revealSpeed #{$bezier-in};
transform: translateY(0);
}
}
.usercard__form {
&.hidden {
display: none;
}
}
.usercard__input-wrap {
display: flex;
height: 32px;
margin-bottom: 16px;
position: relative;
}
.usercard__random {
position: absolute;
top: 2px;
right: 2px;
width: 28px;
height: calc(100% - 4px);
padding: 4px;
padding-right: 2px;
background: $ltgray;
border: 0;
display: flex;
fill: $dkpurple;
svg {
margin: auto;
height: 100%;
width: auto;
}
&:focus {
outline: none;
fill: $brtblue;
}
&:hover {
fill: $ltpurple;
}
&:active {
fill: $dkpurple;
}
}
.offscreen-thingo {
opacity: 1;
transform: translateY(0);
transition: all .25s #{$bezier-in};
&:not(.hiding) {
// for some reason just setting transition-delay: 0
// on `.hiding` wasn't working.
transition-delay: .25s;
}
&.down {
opacity: 0;
transform: translateY($offscreen-distance/2);
}
&.hiding {
opacity: 0;
}
&.hidden {
display: none;
}
}
.usercard__input {
font-size: .9rem;
width: 100%;
padding: 4px;
background: $xltgray;
border: 2px solid $ltgray;
border-radius: 2px;
color: inherit;
color: $dkpurple;
&:focus {
outline: none;
border-color: $brtblue;
}
&::placeholder {
color: lighten($ltpurple, 10%);
}
&.invalid {
border-color: $reddish;
}
}
.templates {
display: none;
}
.usercard__submit {
flex-shrink: 1;
padding: 4px;
height: 32px;
font-size: .9rem;
@include button($brtblue);
}
.usercard__userstats {
display: none;
transform: translateY($offscreen-distance);
opacity: 0;
transition: all $revealSpeed #{$bezier-in};
&.prepped {
display: flex;
}
&.active {
opacity: 1;
transform: translateY(0);
}
}
.heading {
font-size: .75rem;
letter-spacing: .02em;
font-weight: bold;
text-transform: uppercase;
margin-bottom: 8px;
}
.important-heading {
margin-bottom: 24px;
margin-top: 16px;
color: white;
}
.pseudo-underline {
&:after {
content: "";
display: block;
margin: 0 auto;
height: 2px;
margin-top: 8px;
width: 32px;
background: $brtblue;
}
&--huge {
&:after {
margin-top: 26px;
width: 80px;
}
}
}
.pseudo-overline {
&:before {
content: "";
display: block;
margin: 0 auto;
height: 2px;
margin-bottom: 8px;
width: 32px;
background: $brtblue;
}
}
.userstats__divider {
text-align: center;
flex-grow: 1;
}
.userstats__stat {
color: white;
font-weight: bold;
font-size: 1.5rem;
}
#nprogress .bar {
background: $ltpurple;
}
#nprogress .spinner-icon {
border-top-color: $ltpurple;
border-left-color: $ltpurple;
}
.start {
@include button($brtgreen);
padding: 8px;
width: 66%;
margin: 0 auto;
}
.head-to-head {
background: lighten($dkpurple, 8%);
margin: -24px;
margin-bottom: 24px;
display: flex;
}
.head-to-head__user {
flex-basis: 50%;
padding: 16px 8px;
display: flex;
flex-direction: row-reverse;
&:nth-of-type(2) {
flex-direction: row;
}
}
.head-to-head__avatar {
display: block;
height: 56px;
width: 56px;
border-radius: 50%;
}
.head-to-head__text-wrap {
display: flex;
justify-content: center;
flex-direction: column;
text-align: right;
margin: 0 16px;
.head-to-head__user:nth-of-type(2) & {
text-align: left;
}
}
.head-to-head__name {
color: white;
font-weight: bold;
font-size: .8rem;
margin: 0;
}
.head-to-head__username {
font-size: .8rem;
}
.counter {
text-align: center;
}
.counter__number {
font-size: 4rem;
font-weight: bold;
margin: 16px 0;
line-height: 1.15;
color: white;
}
.profile-pair {
display: flex;
margin-top: 16px;
font-size: .85rem;
}
.profile-pair__follows {
margin: auto 8px;
font-size: .65rem;
}
.profile-pair__profile {
flex-grow: 1;
padding: 8px;
display: flex;
width: 0;
}
.profile-pair__arrow {
height: 10px;
width: auto;
stroke: $ltpurple;
display: block;
margin: 8px auto 4px auto;
}
.profile-pair__avatar {
float: left;
height: 48px;
width: 48px;
display: block;
border-radius: 50%;
margin-right: 8px;
}
.profile-pair__details {
text-align: left;
margin: auto 0;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
white-space: nowrap;
}
.profile-pair__details__name {
font-weight: bold;
color: white;
overflow: hidden;
text-overflow: ellipsis;
}
.profile-pair__details__username {
overflow: hidden;
text-overflow: ellipsis;
}
.result-wrapper {
transition: height .35s #{$bezier};
text-align: center;
}
footer {
position: fixed;
font-size: .85rem;
transform: scale(.85);
transform-origin: bottom right;
padding: 16px;
bottom: 0;
right: 0;
opacity: .5;
text-align: right;
transition: all .25s #{$bezier};
&:hover {
transform:scale(1);
opacity: 1;
}
}
<link href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/186499/nprogress.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment