LOADING...

Preview

Pen ID
Unlock Campus Themeforest adv

 

Code

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! Given a "follower" user and a "followee" user, this will attempt to find the shortest link between the two.

avatar

Followers

Following

avatar
Name
@username
Follows
avatar
Name
@username
CSS
html {
  box-sizing: border-box;
}

*,
*:after,
*:before {
  box-sizing: inherit;
}

body {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-orient: vertical;
  -webkit-box-direction: normal;
      -ms-flex-direction: column;
          flex-direction: column;
  height: 100vh;
  font-family: "Open Sans", sans-serif;
  color: #9492d5;
  background: #1E202F;
  padding: 16px;
}

a {
  color: #3F92DE;
  text-decoration: none;
}
a:hover {
  color: #5da3e3;
}

p {
  margin-bottom: 16px;
}

.intro {
  font-size: .85rem;
  margin-bottom: 32px;
  line-height: 1.6;
}

.lead {
  margin-top: 0;
  font-size: 1.2em;
  color: #cac9ea;
}

.page-wrap {
  margin: auto;
  width: 540px;
  -ms-flex-negative: 0;
      flex-shrink: 0;
}

.usercards-wrap {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  margin-bottom: 24px;
}

.card {
  -webkit-transition: height 0.25s cubic-bezier(.4,0,0,1);
  transition: height 0.25s cubic-bezier(.4,0,0,1);
  background: -webkit-linear-gradient(#323151, #282741);
  background: linear-gradient(#323151, #282741);
  padding: 24px;
  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.25);
  border-radius: 4px;
  overflow: hidden;
}
.card .card {
  box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
  background: -webkit-linear-gradient(#38365b, #323151);
  background: linear-gradient(#38365b, #323151);
}

.usercard {
  margin: auto;
  -ms-flex-preferred-size: 50%;
      flex-basis: 50%;
  position: relative;
}
.usercard:first-of-type {
  margin-right: 24px;
}

.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%;
  -webkit-transform: translate(-50%, -50%) scale(0);
          transform: translate(-50%, -50%) scale(0);
  -webkit-transition: -webkit-transform 0.145s cubic-bezier(.4, 0, 0, 1.425);
  transition: -webkit-transform 0.145s cubic-bezier(.4, 0, 0, 1.425);
  transition: transform 0.145s cubic-bezier(.4, 0, 0, 1.425);
  transition: transform 0.145s cubic-bezier(.4, 0, 0, 1.425), -webkit-transform 0.145s cubic-bezier(.4, 0, 0, 1.425);;
}
.usercard__avatar.active {
  background-color: white;
  -webkit-transform: translate(-50%, -50%) scale(1);
          transform: translate(-50%, -50%) scale(1);
}

.usercard__back {
  background: none;
  display: none;
  position: absolute;
  top: 24px;
  left: 24px;
  padding: 0;
  border: 0;
  stroke: #9492d5;
  opacity: .35;
  cursor: pointer;
}
.usercard__back.active {
  display: block;
}
.usercard__back:hover {
  opacity: .75;
}
.usercard__back:focus {
  opacity: 1;
  outline: none;
  stroke: #3F92DE;
}
.usercard__back:active {
  stroke: #9492d5;
}

.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.prepped,
.usercard__username.prepped {
  -webkit-transform: translateY(160px);
          transform: translateY(160px);
  opacity: 0;
}
.usercard__name.active,
.usercard__username.active {
  opacity: 1;
  -webkit-transition: all 0.1s cubic-bezier(0, 0, .2, 1);
  transition: all 0.1s cubic-bezier(0, 0, .2, 1);;
  -webkit-transform: translateY(0);;
          transform: translateY(0);
}

.usercard__form.hidden {
  display: none;
}

.usercard__input-wrap {
  display: -webkit-box;
  display: -ms-flexbox;
  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: #DBE6EC;
  border: 0;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  fill: #282741;
}
.usercard__random svg {
  margin: auto;
  height: 100%;
  width: auto;
}
.usercard__random:focus {
  outline: none;
  fill: #3F92DE;
}
.usercard__random:hover {
  fill: #9492d5;
}
.usercard__random:active {
  fill: #282741;
}

.offscreen-thingo {
  opacity: 1;
  -webkit-transform: translateY(0);
          transform: translateY(0);
  -webkit-transition: all 0.25s cubic-bezier(0, 0, .2, 1);
  transition: all 0.25s cubic-bezier(0, 0, .2, 1);;
}
.offscreen-thingo:not(.hiding) {
  -webkit-transition-delay: .25s;
          transition-delay: .25s;
}
.offscreen-thingo.down {
  opacity: 0;
  -webkit-transform: translateY(80px);
          transform: translateY(80px);
}
.offscreen-thingo.hiding {
  opacity: 0;
}
.offscreen-thingo.hidden {
  display: none;
}

.usercard__input {
  font-size: .9rem;
  width: 100%;
  padding: 4px;
  background: #F0F5F8;
  border: 2px solid #DBE6EC;
  border-radius: 2px;
  color: inherit;
  color: #282741;
}
.usercard__input:focus {
  outline: none;
  border-color: #3F92DE;
}
.usercard__input::-webkit-input-placeholder {
  color: #b8b7e3;
}
.usercard__input::-moz-placeholder {
  color: #b8b7e3;
}
.usercard__input:-ms-input-placeholder {
  color: #b8b7e3;
}
.usercard__input::placeholder {
  color: #b8b7e3;
}
.usercard__input.invalid {
  border-color: #F73D63;
}

.templates {
  display: none;
}

.usercard__submit {
  -ms-flex-negative: 1;
      flex-shrink: 1;
  padding: 4px;
  height: 32px;
  font-size: .9rem;
  display: block;
  border: 1px solid #1f6bb2;
  color: #fff;
  letter-spacing: .02em;
  font-weight: bold;
  border-radius: 2px;
  width: 100%;
  background: #3F92DE;
}
.usercard__submit:focus {
  outline-width: 2px;
  outline-color: #62a6e4;
}
.usercard__submit:hover:not(:active) {
  background: #509fe7;
}

.usercard__userstats {
  display: none;
  -webkit-transform: translateY(160px);
          transform: translateY(160px);
  opacity: 0;
  -webkit-transition: all 0.1s cubic-bezier(0, 0, .2, 1);
  transition: all 0.1s cubic-bezier(0, 0, .2, 1);;
}
.usercard__userstats.prepped {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
}
.usercard__userstats.active {
  opacity: 1;
  -webkit-transform: translateY(0);
          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: #3F92DE;
}
.pseudo-underline--huge:after {
  margin-top: 26px;
  width: 80px;
}

.pseudo-overline:before {
  content: "";
  display: block;
  margin: 0 auto;
  height: 2px;
  margin-bottom: 8px;
  width: 32px;
  background: #3F92DE;
}

.userstats__divider {
  text-align: center;
  -webkit-box-flex: 1;
      -ms-flex-positive: 1;
          flex-grow: 1;
}

.userstats__stat {
  color: white;
  font-weight: bold;
  font-size: 1.5rem;
}

#nprogress .bar {
  background: #9492d5;
}

#nprogress .spinner-icon {
  border-top-color: #9492d5;
  border-left-color: #9492d5;
}

.start {
  display: block;
  border: 1px solid #3dab3d;
  color: #fff;
  letter-spacing: .02em;
  font-weight: bold;
  border-radius: 2px;
  width: 100%;
  background: #6aca6a;
  padding: 8px;
  width: 66%;
  margin: 0 auto;
}
.start:focus {
  outline-width: 2px;
  outline-color: #88d588;
}
.start:hover:not(:active) {
  background: #78d578;
}

.head-to-head {
  background: #38365b;
  margin: -24px;
  margin-bottom: 24px;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
}

.head-to-head__user {
  -ms-flex-preferred-size: 50%;
      flex-basis: 50%;
  padding: 16px 8px;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-orient: horizontal;
  -webkit-box-direction: reverse;
      -ms-flex-direction: row-reverse;
          flex-direction: row-reverse;
}
.head-to-head__user:nth-of-type(2) {
  -webkit-box-orient: horizontal;
  -webkit-box-direction: normal;
      -ms-flex-direction: row;
          flex-direction: row;
}

.head-to-head__avatar {
  display: block;
  height: 56px;
  width: 56px;
  border-radius: 50%;
}

.head-to-head__text-wrap {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
      -ms-flex-pack: center;
          justify-content: center;
  -webkit-box-orient: vertical;
  -webkit-box-direction: normal;
      -ms-flex-direction: column;
          flex-direction: column;
  text-align: right;
  margin: 0 16px;
}
.head-to-head__user:nth-of-type(2) .head-to-head__text-wrap {
  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: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  margin-top: 16px;
  font-size: .85rem;
}

.profile-pair__follows {
  margin: auto 8px;
  font-size: .65rem;
}

.profile-pair__profile {
  -webkit-box-flex: 1;
      -ms-flex-positive: 1;
          flex-grow: 1;
  padding: 8px;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  width: 0;
}

.profile-pair__arrow {
  height: 10px;
  width: auto;
  stroke: #9492d5;
  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: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-orient: vertical;
  -webkit-box-direction: normal;
      -ms-flex-direction: column;
          flex-direction: column;
  -webkit-box-pack: center;
      -ms-flex-pack: center;
          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 {
  -webkit-transition: height 0.35s cubic-bezier(.4,0,0,1);
  transition: height 0.35s cubic-bezier(.4,0,0,1);
  text-align: center;
}

footer {
  position: fixed;
  font-size: .85rem;
  -webkit-transform: scale(0.85);
          transform: scale(0.85);
  -webkit-transform-origin: bottom right;
          transform-origin: bottom right;
  padding: 16px;
  bottom: 0;
  right: 0;
  opacity: .5;
  text-align: right;
  -webkit-transition: all 0.25s cubic-bezier(.4,0,0,1);
  transition: all 0.25s cubic-bezier(.4,0,0,1);
}
footer:hover {
  -webkit-transform: scale(1);
          transform: scale(1);
  opacity: 1;
}
JS
/**
* > 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; // number of allowed requests before throttling kicks in
    this.pendingRequests = []; // current number of pending requests
    this.queuedRequests = []; // current number of queued requests
  }

  /**
  * 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 = $("");
    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;
  }
}

/**
* ...This is what happens when you don't use a framework.
* ...
* ...Use a framework.
*/
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/)) {
  $("footer").hide();
}

Description

Six degrees of separation is the theory that everyone in the world is connected by six or fewer steps.
Term
Mon, 11/27/2017 - 21:26

Related Codes

Pen ID
Pen ID
Pen ID
Square Adv