Summary
The purpose of the #propifyyourlife app is to present resources and images of yoga poses that promote knowledge and awareness of poses, as well as the promotion of the yoga community as a whole. More specifically, I wish to present alternative forms of practicing yoga to express my deep conviction that yoga is for every body, regardless of ability. Additionally, by building and maintaining this application, I am furthering my own knowledge of yoga and website development.
This initial verson of the app is a collection bin of blog posts, images, and videos of individual yoga poses. Future development will include tagging these web resources as it relates to the health benefits they provide.
Takeaways
- Full Stack Development – For this project, I identified the data I wanted to save, designed the database for the data, built my own API so I could store and retrieve the data, and used Bootstrap to style the data into interfaces
- The difference between Services and Factories
- File Upload with AngularJS
- AngularJS Directives including restrictions, scopes, collaborating directives
The Code
Directives
Pose List View
- Installs mouse event handlers onto the view for the Pose List
- View The Code
angular.module('YogaPoseApp').controller('PoseListCtrl', ['$scope', '$location', 'poseListService', function ($scope, $location, appService ) { // DATA INIT $scope.poses = []; console.log('PoseListCtrl()'); $scope.status = "Initiate"; $scope.filtering = false; $scope.activeTags = ""; $scope.tags = {}; $scope.isReady = false; $scope.defaultImage = $scope.activePoseImg = 'http://placehold.it/350x350'; // GET ALL POSES appService.getPoses().then(function(response) { console.log(response); $scope.poseList = response.data; $scope.isReady = true; }, function(response) { $scope.status = "Error getting poses"; console.log('Error fetching poses', response); }); // LOAD TAGS // Specifically, we load the pose types. Every pose belongs to at least one of these Pose Types. // The user can filter the pose list with these specific tags appService.loadPoseTypes().then(function(response) { angular.forEach(response.data, function(data) { $scope.tags[data.ID] = {ID: data.ID, tagName: data.tagName, filterOn: false}; }) }, function(response) { $scope.status = "Error getting tags"; console.log('Error fetching tags', response); }); // Custom filter to filter the poses by pose type. $scope.filterList = function(pose) { if(!$scope.filtering) return true; else { return $scope.activeTags.replace(/\s*,\s*/g, ',').split(',').every(function(cat) { return pose.tags.some(function(objTag){ return objTag.ID.indexOf(cat) !== -1; }); }); } }; // Manages the ActiveTags (string) scope variable $scope.toggleTag = function(tag) { if($scope.activeTags.indexOf(tag) >= 0) $scope.activeTags = $scope.activeTags.replace(tag+',', ''); else $scope.activeTags += tag + ','; } // Changes the View. $scope.go = function(path) { $location.path(path); } }]);
yogaLinkFeed
- Serves as a view for the LinkList model.
- View The Code
angular.module('YogaPoseApp').directive('yogaLinkFeed', ['$compile', function($compile) { console.log('yogaInstafeed()'); var tpl = "
Other Links
{{LinkListStatus}}
" + "" + ""; return { restrict: 'EC', replace: true, template: tpl, scope: false, link: function($scope, $el, attrs, $rootScope ) { } } }]);" + "" + "{{link.title}}
" + "{{link.description}}
" + "
{{link.domain}}yogaInstaFeed
- Serves as a view for the Yogagram model.
- View The Code
angular.module('YogaPoseApp').directive('yogaInstaFeed', ['$compile', function($compile) { console.log('yogaInstafeed()'); var tpl = "
" + ""; return { restrict: 'EC', replace: true, template: tpl, scope: false, link: function($scope, $el, attrs, $rootScope ) { $scope.$watch(function() { return $scope.pose; }, function(newVal) { if(newVal) $el.append(""); }); } } }]);From Instagram
" + "" + "- " +
"
- " + "" + "" + "" + " " + "
yogaVideoList
- Serves as a view for the VideoList model.
- View The Code
angular.module('YogaPoseApp').directive('yogaVideoList', ['$compile', function($compile) { console.log('yogaVideoList()'); var rootTpl = "
"; return { restrict: 'AC', replace: true, template: rootTpl, scope: false, link: function($scope, $el, attrs, $rootScope ) { var videoTpl = "{{videoListStatus}}
" + "" + "" + "" + "" + "" + "" + "" + ""; $scope.$watch(function(scope) { return scope.videoList; }, function(newVal, oldVal) { if(newVal.length && false) { var newItem = newVal[newVal.length-1]; var html = videoTpl; html = html.replace('%title%', newItem.title, 'g'); html = html.replace('%img%', newItem.img, 'g'); html = html.replace('%locator%', newItem.locator, 'g'); html = html.replace('%duration%', newItem.duration, 'g'); $el.append($compile(html)($scope)); } }, true); } } }]);
yogaMediaViewer
- Element to facilitate the display of each pose asset.
- View The Code
angular.module('YogaPoseApp').directive('yogaMediaViewer', function() { return { restrict: 'AC', /*scope: { pose: '=', mediaObj: '=' },*/ scope: false, replace: true, template: '
', link: function($scope, $el, $attrs) { //scope.watch mediaObj var contentTpl= { youtube: function(obj) { console.log(obj); var html = '' return html; }, instagram: function(obj) { var html, user, type, media, date, caption; var titleEL, mediaRow, captionRow; type = obj.type; user = obj.user, media = obj.media, date = obj.date caption = obj.caption; titleEL = "'+obj.title+'
'; if(obj.tags && obj.tags.length) { html += 'Tags: '; for(var i = 0; i < obj.tags.length; i++) { html += '' + obj.tags[i].tagName + ''; if(i < obj.tags.length - 1) html += ', '; } html += '
'; } html += '"; titleEL += ""; if(type == "video") { console.log('showVideo'); mediaRow = ""; } else { mediaRow = ""; } captionRow = ""+user.name+"
Published:"+date+""; if(obj.myTags && obj.myTags.length) { captionRow += "Caption
"+caption+"
'; } return titleEL + mediaRow + captionRow; } } $scope.$watch('mediaObj', function(newVal, oldVal) { if(newVal) { var content = ''; if((typeof newVal) == "string") { if(newVal.match(/\.(?:jpg|gif|png)$/)) { content = ''; } else if(newVal.length == 11) { content = contentTpl.youtube(newVal); } } else if((typeof newVal) == "object") { if((newVal.link && newVal.link.indexOf('instagram') > -1) || newVal.asset_type == 4) content = contentTpl.instagram(newVal); else if(newVal.locator) content = contentTpl.youtube(newVal); } else { content = ""+newVal+""; } $el.find('.media-content').html(content); console.log('showing', newVal); $scope.$emit('newMedia', newVal) } }, true); } } });Internal Tags:
"; for(var i = 0; i < obj.myTags.length; i++) { captionRow += '' + obj.myTags[i].tagName + ''; if(i < obj.myTags.length - 1) captionRow += ', '; } captionRow += '
Services
PoseListService
- Supports the Pose List View by getting the list of poses, and the types of poses to facilitate filtering.
- View The Code
angular.module('YogaPoseApp').service('poseListService', ['$http', function($http, $q) { var poseData = false; return ({ getPoses: function() { var request; if(!poseData) { request = $http.get('./api/getAllPoses', {cache: true}); return (request.then(function(response) { return response.data; }, function(response) { if(!angular.isObject(response.data) || !response.data.message) return ($q.reject("An uknown error has occured")); return ($q.reject(response.data)); })); } }, loadPoseTypes: function () { var request = $http.get('./api/loadTags?query=poseTypes', {cache: true}); return (request.then(function(response) { return response.data; }, function(response) { if(!angular.isObject(response.data) || !response.data.message) return ($q.reject("An uknown error has occured")); return ($q.reject(response.data)); }) ); } }); }])
SinglePoseService
- Supports the Single Pose View by getting Pose info, getting it’s assets, then going out to the web to find more data about those assets.
- View The Code
angular.module('YogaPoseApp').service('singleoseService', ['$http', function ($http, $q) { return ({ getPose: getPose, getAssets: getAssets, getInstagrams: getInstagrams, getSingleInstagram: getSingleInstagram, getVideoInfo: getVideoInfo, getPageInfo: function(url) { var request = $http.get('./api/getPageInfo?url='+url, {cache: true}); return (request.then(handleSuccess, handleError)); }, }); function getPose(name) { var request, requestObj; if(Number.isInteger(name)) { requestObj = {'ID': name}; } else requestObj = {'sanskritName': name}; request = $http.get('./api/getPose/'+name, {cache: true}); return (request.then(handleSuccess, handleError)); } function getAssets(poseID) { var request = $http.get('./api/getAssets/'+poseID, {cache: true}); return (request.then(handleSuccess, handleError)); } function getInstagrams(tag) { var base = 'https://api.instagram.com/v1/tags/' + tag + '/media/recent?access_token='; var token = '211521125.467ede5.87ba7f407a0c40b191db7899a75b116c'; var end = "&callback=JSON_CALLBACK"; var request = $http.jsonp(base + token + end); return (request.then(handleSuccess, handleError)); }; function getSingleInstagram(shortcode) { var base = 'https://api.instagram.com/v1/media/shortcode/'+shortcode+'?access_token='; var token = '211521125.467ede5.87ba7f407a0c40b191db7899a75b116c'; var end = "&callback=JSON_CALLBACK"; var request = $http.jsonp(base + token + end); return (request.then(handleSuccess, handleError)); }; function getVideoInfo(videoID) { var key = "AIzaSyBVBe0KqROzVUDK31iOC2Tjpc4ViSVeUVI"; var url = "https://www.googleapis.com/youtube/v3/videos?id="+videoID+"&key="+key+"&part=snippet,contentDetails"; return $http.get(url, {cache: true}); } function handleSuccess(response) { return response.data; } function handleError(response) { if(!angular.isObject(response.data) || !response.data.message) return ($q.reject("An uknown error has occured")); return ($q.reject(response.data)); } }]);
Controllers
Pose List Ctrl
- Initializes retrieval of data from the service.
- Key Functions:
-
filterList() : A custom Filter
$scope.filterList = function(pose) { if(!$scope.filtering) return true; else { return $scope.activeTags.replace(/\s*,\s*/g, ',').split(',').every(function(cat) { return pose.tags.some(function(objTag){ return objTag.ID.indexOf(cat) !== -1; }); }); } };
-
toggleTag(): Manages the $scope.activeTags variable
// Manages the ActiveTags (string) scope variable $scope.toggleTag = function(tag) { if($scope.activeTags.indexOf(tag) >= 0) $scope.activeTags = $scope.activeTags.replace(tag+',', ''); else $scope.activeTags += tag + ','; }
-
filterList() : A custom Filter
Single Pose View
- Initializes retrival of data for a single pose. Once the data is returned from the service, the controller retrives any assets saved for that pose.
- Controls the models used in the Single Pose View. Models include
- LinkList
- Yogagrams (Instagram posts)
- VideoList
- View The Code
angular.module('YogaPoseApp').controller('SinglePoseCtrl', ['$scope', '$routeParams', 'singlePoseService', '$q', '$rootScope', function($scope, $routeParams, appService, $q, $rootScope) { console.log('SinglePoseCtrl()'); var poseName = $routeParams.sanskritName; var camelize = function (str) { return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) { return index == 0 ? letter.toLowerCase() : letter.toUpperCase(); }).replace(/\s+/g, ''); } $scope.pose = null; $scope.viewportStatus = 'Init'; $scope.videoList = []; $scope.videoListStatus = "Videos"; $scope.linkList = []; $scope.LinkListStatus = false; $scope.imgAssetList = []; $scope.yogagramList = []; $scope.instagrams = []; $scope.mediaObj = null; $scope.myTags = []; /* $scope.loadTags = function(query) { console.log(query); return appService.loadTags(query); }; $scope.saveTags = function() { console.log($scope.myTags); appService.saveTags({tags: $scope.myTags, poseID: $scope.pose.ID}).then(function(response) { console.log('success saving tags'); }, function(response) { console.log("an error has occured while saving tags", response); }) } */ appService.getPose(poseName).then(function(response) { var poseID, tagName; $scope.pose = response.data; poseID = $scope.pose['ID']; tagName = camelize($scope.pose['sanskritName']); getAssets(poseID); $rootScope.pageTitle = $scope.pose['sanskritName'] + ' | ' + $scope.pose['englishName']; }, function(response) { $scope.status = "Error getting pose"; console.log('Error fetching pose: ' + poseName, response); }); $scope.getVideoList = function() { return $scope.videoList; }; $scope.view = function(asset) { $scope.mediaObj = asset; return false; } $scope.getPageInfo = function(asset) { var url = asset.locator, myAsset = asset; appService.getPageInfo(url).then(function(response) { for(var attr in response.data) { myAsset[attr] = response.data[attr]; } $scope.linkList.push(myAsset); $scope.LinkListStatus = false; }, function(response) { $scope.status = "Error getting page"; console.log('Error fetching page: ' + url, response); }); } var getAssets = function(poseID) { $scope.videoListStatus = "Videos Loading..."; $scope.LinkListStatus = "Links Loading..."; appService.getAssets(poseID).then(function(response) { var data = response.data; for(var i = 0; i < data.length; i++) { switch(data[i].fk_type) { case '1': //image $scope.imgAssetList.push(data[i]) break; case '2': //video pushVideoItem({locator: data[i].locator, tags: (data[i].tags || []), ID: data[i].ID, notes: data[i].notes}); break; case '3': $scope.getPageInfo(data[i]); break; case '4': pushYogagram({locator: data[i].locator, tags: (data[i].tags || []), ID: data[i].ID, notes: data[i].notes}); break; default: break; } } if(!$scope.videoList.length) { $scope.videoListStatus = ""; } $scope.$broadcast('yogaAssetsLoaded'); }, function(response) { $scope.status = "Error getting pose asset"; console.log('Error getting pose assets', poseID, response); }); }; var getInstagrams = function(tag) { /*var deferred = $q.defer(); deferred.notify(); */ appService.getInstagrams(tag).then(function(response) { var data = response.data; console.log(data); //$scope.instagrams = data; }, function(response) { $scope.status = "Error getting pose asset"; console.log('Error getting pose assets', tag, response); }); }; var pushVideoItem = function(vidObj) { var obj = angular.copy(vidObj); var response = appService.getVideoInfo(obj.locator).then(function(response) { var data = response.data; obj.duration= (function(t) { var t = t.replace("PT",'').replace('M',':').replace('S', ''); var explode = t.split(":"); if(!explode[1] || explode[1] == "") explode[1] = "00"; return explode[0]+":"+explode[1].replace(/(^|[^0-9])([0-9]{1})($|[^0-9])/, '0$2'); })(data.items[0].contentDetails.duration); obj.title = data.items[0].snippet.title; obj.img = data.items[0].snippet.thumbnails.default.url; //console.log('pushing', obj); obj.asset_type = 2, $scope.videoList.push(obj); }, function(response) { $scope.status = "Error getting video info"; console.log('Error getting video info', videoID, response); }); } var pushYogagram = function(yogObj) { var myYogObj = angular.copy(yogObj); var response = appService.getSingleInstagram(myYogObj.locator).then(function(response) { var obj, user, type, media, date, date_str, caption; var data = response.data; if(response.meta.code == 200) { type = data.type; user = { name: data.user.username, pic: data.user.profile_picture }; media = { obj: data[type+'s'], link: data.link }; date = new Date(data.created_time * 1000); date = date.toString('MMMM d, yyyy'); caption = data.caption.text.parseInstagramHashtag(); yogagram = { type: type, user: user, media: media, caption: caption, myTags: myYogObj.tags, notes: (myYogObj.notes == "-1") ? false : myYogObj.notes, ID: myYogObj.ID, date: date, asset_type: 4 }; $scope.yogagramList.push(yogagram); } else console.log('no instagram data', data); }, function(response) { $scope.status = "Error getting yogagram"; console.log('Error getting yogagram', yogObj, response); }); } }]);