Display a list of streamers and their status using the twitch.tv API. The user may add/remove/filter streamers from the list.
A Pen by Matthew Kuo on CodePen.
| <html ng-app="stream"> | |
| <body ng-controller="GlobalController as globalCtrl"> | |
| <div data-alert class="alert-box alert" ng-show="globalCtrl.alertMsg"> | |
| {{ globalCtrl.alertMsg }} | |
| <a href ng-click="globalCtrl.setAlert('')" class="close">×</a> | |
| </div> | |
| <div id="wrap" ng-controller="TabsController as tabs"> | |
| <section id="tabs"> | |
| <dl class="tabs"> | |
| <dd ng-class="{active:tabs.isSelected(1)}"><a href ng-click="tabs.selectTab(1)"><i class="fa fa-users"></i></a></dd> | |
| <dd ng-class="{active:tabs.isSelected(2)}"><a href ng-click="tabs.selectTab(2)"><i class="fa fa-user"></i></a></dd> | |
| <dd ng-class="{active:tabs.isSelected(3)}"><a href ng-click="tabs.selectTab(3)"><i class="fa fa-user-times"></i></a></dd> | |
| </dl> | |
| </section> | |
| <section id="streamList" ng-controller="StreamListController as streamList"> | |
| <div class="search clearfix" ng-controller="AddStreamController as addStreamCtrl"> | |
| <i class="fa fa-search"></i> | |
| <input type="text" placeholder="Search..." ng-model="streamList.searchText" ng-focus="addStreamCtrl.toggleAnimation(false)" ng-blur="addStreamCtrl.toggleAnimation(true)"> | |
| <a href class="add-streamer" ng-class="{activated:addStreamCtrl.isAddClicked}" ng-click="addStreamCtrl.onAddStreamClicked()"> | |
| <i class="fa fa-user-plus"></i> | |
| </a> | |
| <div class="bubble clearfix" ng-show="addStreamCtrl.isAddClicked"> | |
| <form ng-submit="addStreamCtrl.submitNewStreamer()"> | |
| <input type="text" placeholder="Add streamer..." ng-model="addStreamCtrl.newStreamer"/> | |
| <input type="submit" class="fa fa-plus" value=""/> | |
| </form> | |
| </div> | |
| </div> | |
| <ul> | |
| <li ng-show="tabs.isSelected(1) || tabs.isSelected(2) && streamer.online || tabs.isSelected(3) && !streamer.online" | |
| ng-repeat="streamer in streamList.streamers | filter:streamList.searchText | orderBy:['-online', 'display_name']" | |
| ng-controller="StreamerController as streamerCtrl" | |
| ng-mouseover="streamerCtrl.hover(true)" ng-mouseleave="streamerCtrl.hover(false)"> | |
| <a href="{{ streamer.url }}" target="_blank"> | |
| <div class="logo"> | |
| <img ng-src="{{streamer.logo}}"/> | |
| <div class="status-icon" ng-class="{online:streamer.online}"></div> | |
| </div> | |
| <div class="info"> | |
| <div class="display-name">{{ streamer.display_name }}</div> | |
| <div ng-show="streamer.online" class="status">{{ streamer.status }}</div> | |
| </div> | |
| </a> | |
| <div class="delete-streamer" ng-show="streamerCtrl.getHoveredState()" ng-click="streamList.removeStreamer(streamer)"> | |
| <i class="fa fa-times"></i> | |
| </div> | |
| </li> | |
| </ul> | |
| </section> | |
| </div> | |
| </body> | |
| </html> |
| (function() { | |
| var app = angular.module('stream', ['ngAnimate']); | |
| app.controller('GlobalController', ['$scope', '$timeout', function($scope, $timeout) { | |
| myThis = this; | |
| this.alertMsg = ''; | |
| this.setAlert = function(text) { | |
| this.alertMsg = text; | |
| }; | |
| // Emitted by StreamListController | |
| $scope.$on('getUserFailed', function(event, msg) { | |
| myThis.alertMsg = msg; | |
| $timeout(function() { | |
| myThis.alertMsg = ''; | |
| }, 8000); | |
| }); | |
| }]); | |
| app.controller('TabsController', function() { | |
| this.tab = 1; | |
| this.selectTab = function(selectedTab) { | |
| this.tab = selectedTab; | |
| }; | |
| this.isSelected = function(selectedTab) { | |
| return this.tab === selectedTab; | |
| }; | |
| }); | |
| app.controller('AddStreamController', ['$animate', '$scope', function($animate, $scope) { | |
| this.isAddClicked = false; | |
| this.newStreamer = ''; | |
| this.onAddStreamClicked = function() { | |
| $animate.enabled(true); | |
| this.isAddClicked = !this.isAddClicked; | |
| }; | |
| this.submitNewStreamer = function() { | |
| $scope.$emit('streamerSubmitted', this.newStreamer); | |
| this.newStreamer = ''; | |
| this.isAddClicked = false; | |
| }; | |
| this.toggleAnimation = function(isEnabled) { | |
| $animate.enabled(isEnabled); | |
| }; | |
| }]); | |
| app.controller('StreamListController', ['$http', '$scope', '$animate', function($http, $scope, $animate) { | |
| var myThis = this; | |
| this.streamers = []; | |
| var getStreamData = function(streamer) { | |
| var streamUrl = 'https://api.twitch.tv/kraken/streams/' + streamer + '?callback=JSON_CALLBACK'; | |
| $http.jsonp(streamUrl).success(function(data) { | |
| if (data && data.stream) { | |
| var channel = data.stream.channel; | |
| myThis.streamers.push({ | |
| 'online': true, | |
| 'display_name': channel.display_name, | |
| 'status': channel.status, | |
| 'game': channel.game, | |
| 'url': 'http://www.twitch.tv/' + streamer, | |
| 'logo': channel.logo || 'http://static-cdn.jtvnw.net/jtv_user_pictures/xarth/404_user_150x150.png' | |
| }); | |
| } else if (data.error) { | |
| $scope.$emit('getUserFailed', data.message); | |
| } else { | |
| getUserData(streamer); | |
| } | |
| }); | |
| }; | |
| var getUserData = function(streamer) { | |
| var userUrl = 'https://api.twitch.tv/kraken/users/' + streamer + '?callback=JSON_CALLBACK'; | |
| $http.jsonp(userUrl).success(function(data) { | |
| myThis.streamers.push({ | |
| 'online': false, | |
| 'display_name': data.display_name, | |
| 'url': 'http://www.twitch.tv/' + streamer, | |
| 'logo': data.logo || 'http://static-cdn.jtvnw.net/jtv_user_pictures/xarth/404_user_150x150.png' | |
| }); | |
| }).error(function(data) { | |
| $scope.$emit('getUserFailed', data.message); | |
| }); | |
| }; | |
| this.addStream = function(name) { | |
| getStreamData(name); | |
| }; | |
| defaultStreamers.forEach(function(streamer) { | |
| getStreamData(streamer); | |
| }); | |
| $scope.$on('streamerSubmitted', function(event, streamer) { | |
| myThis.addStream(streamer); | |
| }); | |
| this.removeStreamer = function(streamer) { | |
| for (var i = 0; i < this.streamers.length; i++) { | |
| if (this.streamers[i].$$hashKey === streamer.$$hashKey) { | |
| this.streamers.splice(i, 1); | |
| } | |
| } | |
| }; | |
| }]); | |
| app.controller('StreamerController', function($scope) { | |
| this.hovered = false; | |
| this.hover = function(isHovered) { | |
| this.hovered = isHovered; | |
| }; | |
| this.getHoveredState = function() { | |
| return this.hovered; | |
| }; | |
| }); | |
| var defaultStreamers = ["Doublelift", "Phantoml0rd", "TheOddOne", "medrybw", "Bjergson"]; | |
| })(); |
| <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.min.js"></script> | |
| <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular-animate.min.js"></script> |
| @import 'compass'; | |
| @import url(http://fonts.googleapis.com/css?family=Raleway); | |
| $light-grey: #ecf0f1; | |
| $flat-red: #c0392b; | |
| $flat-green: #27ae60; | |
| $flat-blue: #4aa3df; | |
| $darker-flat-blue: #2980b9; | |
| body { | |
| background: url('http://i1167.photobucket.com/albums/q633/mattkuo/website-backgrounds/escheresque_ste_zps4eq0cibc.png'); | |
| font-family: Raleway, Helvetica, Roboto, Arial, sans-serif; | |
| } | |
| .alert-box.alert { | |
| position: absolute; | |
| top: 0; | |
| text-align: center; | |
| width: 100%; | |
| padding: 0.5rem; | |
| &.ng-hide-add { | |
| @include animation(slideOutUp 1s); | |
| } | |
| &.ng-hide-remove { | |
| @include animation(slideInDown 1s); | |
| } | |
| } | |
| #wrap { | |
| width: 300px; | |
| margin: 0 auto 40px; | |
| position: relative; | |
| top: 40px; | |
| background: transparent; | |
| @include box-shadow(); | |
| } | |
| #streamList { | |
| & .search { | |
| position: relative; | |
| & > i.fa-search { | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| border-left: none; | |
| border-right: none; | |
| z-index: 1; | |
| } | |
| & > input { | |
| float: left; | |
| margin: 0; | |
| padding-left: 2rem; | |
| width: 80%; | |
| } | |
| & a.add-streamer { | |
| float: left; | |
| width: 20%; | |
| text-align: center; | |
| background: $flat-blue; | |
| color: white; | |
| & > i { | |
| line-height: 2.3125rem; | |
| } | |
| &.activated { | |
| background-color: #2980b9; | |
| } | |
| } | |
| & .bubble { | |
| opacity: 0.9; | |
| position: absolute; | |
| right: 3px; | |
| top: 30px; | |
| background-color: $light-grey; | |
| padding: 15px; | |
| margin: 1rem 0 3rem; | |
| z-index: 100; | |
| @include border-radius(4px); | |
| @include transition(opacity ease-in-out 0.2s); | |
| &.ng-hide-add { | |
| @include animation(fadeOutUp 0.5s); | |
| } | |
| &.ng-hide-remove { | |
| @include animation(flipInX 0.8s); | |
| } | |
| & input[type="text"] { | |
| float: left; | |
| width: 80%; | |
| } | |
| & input[type="submit"] { | |
| float: left; | |
| width: 20%; | |
| line-height: 2.3125rem; | |
| color: rgba(0, 0, 0, 0.75); | |
| cursor: pointer; | |
| } | |
| &:after { | |
| content: ''; | |
| position: absolute; | |
| width: 0; | |
| border-width: 0 15px 15px; | |
| border-style: solid; | |
| border-color: $light-grey transparent; | |
| top: -15px; | |
| right: 15px; | |
| } | |
| & input { | |
| margin: 0; | |
| padding: 0; | |
| background: transparent; | |
| border: 0; | |
| box-shadow: none; | |
| } | |
| } | |
| } | |
| ul { | |
| margin: 0; | |
| & li { | |
| position: relative; | |
| list-style: none; | |
| background: #fff; | |
| padding: 16px; | |
| border-bottom: 1px solid $light-grey; | |
| & .delete-streamer { | |
| position: absolute; | |
| width: 20%; | |
| height: 83px; | |
| background: $flat-red; | |
| top: 0; | |
| right: 0; | |
| text-align: center; | |
| cursor: pointer; | |
| & i { | |
| line-height: 83px; | |
| color: white; | |
| } | |
| &.ng-hide-add { | |
| @include animation(fadeOut 0.3s); | |
| } | |
| &.ng-hide-remove { | |
| @include animation(fadeIn 0.3s); | |
| } | |
| } | |
| &.ng-enter { | |
| @include animation(zoomInLeft 0.5s); | |
| } | |
| &.ng-leave { | |
| @include animation(zoomOutRight 0.5s); | |
| } | |
| & a { | |
| display: table; | |
| color: inherit; | |
| & > div { | |
| display: table-cell; | |
| vertical-align: middle; | |
| } | |
| & .logo { | |
| // float: left; | |
| position: relative; | |
| display: inline-block; | |
| & img { | |
| width: 50px; | |
| @include border-radius(50%); | |
| } | |
| & .status-icon { | |
| opacity: 0.9; | |
| position: absolute; | |
| width: 15px; | |
| height: 15px; | |
| background-color: $flat-red; | |
| right: 0; | |
| bottom: 0; | |
| @include border-radius(50%); | |
| &.online { | |
| background-color: $flat-green; | |
| } | |
| } | |
| } | |
| & .info { | |
| display: inline-block; | |
| width: 196px; | |
| margin-left: 16px; | |
| & .status { | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| font-size: 11px; | |
| color: $flat-blue; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| .tabs dd { | |
| width: (100% / 3); | |
| &:nth-child(2) a { | |
| color: $flat-green; | |
| } | |
| &:nth-child(3) a { | |
| color: $flat-red; | |
| } | |
| &.active a { | |
| @include box-shadow(0 5px 0 0 $flat-blue inset); | |
| } | |
| & a { | |
| text-align: center; | |
| font-size: 1.575rem; | |
| } | |
| } |
| <link href="http://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" /> | |
| <link href="https://cdn.jsdelivr.net/foundation/5.5.0/css/foundation.css" rel="stylesheet" /> | |
| <link href="//cdnjs.cloudflare.com/ajax/libs/animate.css/3.3.0/animate.min.css" rel="stylesheet" /> |
Display a list of streamers and their status using the twitch.tv API. The user may add/remove/filter streamers from the list.
A Pen by Matthew Kuo on CodePen.