<div class="container">
    <header class="box my-transition">
      <h1>Twitch App</h1>
      <p>A simple Twitch app displaying channel streaming status by
        <a class="my-transition" href="http://about.phamvanlam.com">Lam Pham</a>.
      </p>
      <div class="filter">
        <span class="option my-transition all active">All</span>
        <span class="option my-transition online">Online</span>
        <span class="option my-transition offline">Offline</span>
      </div>
      <i class="fa fa-refresh refresh my-transition"></i>
    </header>

    <section class="channels">
    </section>
    
    <section class="add">
      <form>
        <div class="input_wrapper box my-transition">
          <input type="text" name="add_channel" placeholder="Enter twitchtv's username to follow..." required>
        </div>
        <button class="box my-transition">ADD</button>
      </form>
    </section>
  </div>
* {box-sizing: border-box;}
body {
  margin: 0;
  padding: 15px;
  color: #444;
  background-color: #efefef;
  font: normal normal normal 1rem/1.6 Nunito Sans, Helvetica, Arial, sans-serif;
}
.container {
  width: 100%;
  margin: 0 auto;
}
header {
  background-color: #6441A4;
  color: #fff;
  padding: 15px;
  position: relative;
}
header .refresh {
  position: absolute;
  top: 15px;
  right: 15px;
  color: #ccc;
  cursor: pointer;
}
header .refresh:hover {
  color: #fff;
}
header h1 {
  margin: 0px;
  font-size: 2.5rem;
}
header p {
  margin-top: 0px;
  font-size: 0.8rem;
  text-align: justify;
  color: #eee;
}
header a {color: #68efad;}
header a:hover{color: #5fd89e;}
.item a{color: #6441A4;}
.item a:hover{color: #593a94;}
.my-transition {
  transition: all 0.35s ease-in-out;
  -webkit-transition: all 0.35s ease-in-out;
  -moz-transition: all 0.35s ease-in-out;
}
.box{
  box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
  -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
  -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
}
.box:hover{
  box-shadow: 0 2px 5px rgba(0,0,0,0.12), 0 3px 5px rgba(0,0,0,0.24);
  -webkit-box-shadow: 0 2px 5px rgba(0,0,0,0.12), 0 3px 5px rgba(0,0,0,0.24);
  -moz-box-shadow: 0 2px 5px rgba(0,0,0,0.12), 0 3px 5px rgba(0,0,0,0.24);
}
.channels .item {
  background-color: #fff;
  margin-top: 15px;
  padding: 15px;
  display: flex;
  display: -webkit-flex;
  align-items: center;
}
.channels .item.hidden {
  display: none;
}
.channels .item .logo-name-wrapper .logo img{
  border: 5px solid #6441A4;
  border-radius: 50%;
}
.channels .item.offline { border-left: 5px solid #fe4a49;}
.channels .item.online { border-left: 5px solid #68efad;}
.channels .item.closed { border-left: 5px solid #999;}
.channels .item .main {width: calc(100% - 10px);}
.channels .item .remove {opacity: 0;}
.channels .item:hover .remove {opacity: 1;}
.channels .item:hover .remove:hover {cursor: pointer;}
.channels .item .logo-name-wrapper {
  display: flex;
  display: -webkit-flex;
  align-items: center;
}
.channels .item .logo-name-wrapper .name {
  padding-left: 15px;
  font-size: 0.85rem;
}
.channels .item .status { font-size: 0.85rem; }
.channels .item .logo-name-wrapper img {
  width: 60px;
  height: 60px;
}
.add form {
  width: 100%;
  display: flex;
  display: -webkit-flex;
  align-items: center;
  justify-content: space-between;
}
.add .input_wrapper {
  padding: 0px 15px;
  margin-top: 15px;
  height: 60px;
  width: calc(100% - 115px);
  background-color: #fff;
  display: flex;
  display: -webkit-flex;
  align-items: center;
}
.add input {
  padding: 7px 15px;
  width: 100%;
}
.add button {
  margin-top: 15px;
  padding: 0px 15px;
  width: 100px;
  height: 60px;
  color: #444;
  background-color: #fff;
  border: none;
  font-weight: 600;
}
.add button:hover {
  cursor: pointer;
}
.add button:focus, .add input:focus {outline: none;}
.add button:active {background-color: #444;}
.add input:active{
  outline: none;
  background-color: none;
}
.filter {text-align: right;}
.filter .option { 
  margin-left: 10px; 
  padding: 2px;
  display: inline-block;
}
.filter .option:not(.active):hover {
  cursor: pointer;
  border-bottom: 3px solid #999;
}
.filter .option.active { border-bottom: 3px solid #fff;}
.filter .online {color: #68efad;}
.filter .offline {color: #fe4a49;}
@media (min-width: 768px) {
  .container {
    width: 70%;
    min-width: 700px;
  }
  .channels .item .main {
    display: flex;
    display: -webkit-flex;
    align-items: center;
  }
  .channels .item .main .logo-name-wrapper { flex: 1; }
  .channels .item .main .status { flex: 1; }
  .channels .item .logo-name-wrapper .name {font-size: 1rem;
  }
  .channels .item .status { font-size: 1rem; }
  .channels .item .logo-name-wrapper img {
    width: 80px;
    height: 80px;
  }
  .add button {
    width: 120px;
    font-size: 1.05rem;
  }
  .add .input_wrapper { width: calc(100% - 135px); }
}

@media (min-width: 992px) {
  .channels .item .main .logo-name-wrapper { flex: 3; }
  .channels .item .main .status { flex: 5; }
}
$(document).ready(() => {
  const FAVORITE_TWITCHTV_CHANNELS = "favorite-twitchtv-channels";
  
  let default_channels = {
    "freecodecamp" : "freecodecamp",
    "funfunfunction" : "funfunfunction",
    "noopkat" : "noopkat",
    "ferossity" : "ferossity",
    "kentcdodds" : "kentcdodds",
    "radicalfishgames" : "radicalfishgames",
    "scinos" : "scinos",
    "rthor" : "rthor",
    "simalexan" : "simalexan",
    "failarmy" : "failarmy"
  };
  
  let $container = $(".channels");
  let $btnAdd = $(".add button");
  let $inpName = $(".add input[type=text]");
  let $form = $(".add form");
  let $options = $(".filter .option");
  let $refresh = $("header .refresh");

  let storageHandler = new AppStorage("localStorage");
  let channels = getChannels() || default_channels;
  setChannels(channels);
  render(channels);
  
  $btnAdd.on("click", (event) => {
    event.preventDefault();
    let channel = $inpName.val().toLowerCase();
    $form[0].reset();
    
    if (!channels[channel]) {
      channels[channel] = channel;
      setChannels(channels);
      getChannelInfo(channel); 
    }
    else alert(`Channel '${channel}' already exists!`);
  });
  
  $options.on("click", (event) => {
    let $target = $(event.target);
    
    if ($target.hasClass("active") == false) {
      $target.siblings().removeClass("active");
      $target.addClass("active");
      
      if ($target.hasClass("all")) filter("all");
      else if ($target.hasClass("online")) filter("online");
      else if ($target.hasClass("offline")) filter("offline");
    }
  });
  
  $refresh.on("click", (event) => {
    channels = default_channels;
    setChannels(channels);
    refreshPage();
  });
  
  function refreshPage(){
    window.location.href = window.location.href;
  }
  
  function render(channels) {
    for (let key in channels) {
      getChannelInfo(key);
    }
  }
  
  function filter(state) {
    if(state == 'all') {
      $container.find(`.item`).removeClass("hidden");
    }
    else if(state == 'online' || state == 'offline') {
      $container.find('.item').addClass("hidden");
      $container.find(`.item.${state}`).removeClass("hidden");
    }
  }

  function getChannelInfo(channel) {
    $.getJSON(makeURL("channels", channel))
    .done(data => onGetChannelInfoSuccess(channel, data))
    .fail(error => console.log(error));
  }

  function onGetChannelInfoSuccess(channel, data) {
    if (!data.error) {
      if (data.status === null) data.status = "Offline";
      
      let tmpl = itemTemplate(data);
      $container.append($(tmpl));
      $container.find(`.item[name="${channel}"] .remove`).on("click", onBtnRemoveClicked);
      
      getStreamInfo(channel);
    }
    else {
      delete channels[channel];
      setChannels(channels);
      alert(`${data.status} - ${data.message}`);
    }
  }
  
  function onBtnRemoveClicked(event) {
    let $item = $(event.target).closest(".item");
    let name = $item.attr("name");
    
    delete channels[name];
    setChannels(channels);
    $item.remove();
  }

  function getStreamInfo(channel) {
    $.getJSON(makeURL("streams", channel))
      .done(data => onGetStreamInfoSuccess(channel, data))
      .fail(error => console.log(error));
  }

  function onGetStreamInfoSuccess(channel, data) {
    let state;
    if (data.stream === null) state = "offline";
    else if (data.stream === undefined) state = "closed";
    else state = "online";

    updateChannelState(channel, state);
  }

  function updateChannelState(channel, state) {
    let $item = $container.find(`.item[name="${channel}"]`);
    $item.addClass(state);
  }

  function makeURL(type, name) {
    return `https://wind-bow.gomix.me/twitch-api/${type}/${name}?callback=?`;
  };
  
  function setChannels(channels) {
    storageHandler.set(FAVORITE_TWITCHTV_CHANNELS, JSON.stringify(channels));
  }
  
  function getChannels() {
    let channels = storageHandler.get(FAVORITE_TWITCHTV_CHANNELS);
    if (channels) return JSON.parse(channels);
  }

  function itemTemplate(data) {
    return `
      <div class="item box my-transition" name="${data.name}">
        <div class="main">
          <div class="logo-name-wrapper">
            <div class="logo">
              <a href="${data.url}" target="_blank">
                <img src="${data.logo}" alt="${data.display_name}'s log">
              </a>
            </div>
            <div class="name">
              <a href="${data.url}" target="_blank">${data.display_name}</a>
            </div>
          </div>
          <div class="status">${data.status}</div>
        </div>
        <i class="fa fa-remove remove my-transition"></i>
      </div>
    `;
  }
});
Run Pen

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js
  2. https://codepen.io/completejavascript/pen/WJyPBv.js