Documenting the #propifyyourlife App

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

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 = "<div class='link-asset-list'><h3>Other Links</h3><p ng-show='LinkListStatus'>{{LinkListStatus}}</p><div class='yoga-asset link-asset media' ng-repeat='link in linkList' data-target='{{link.locator}}' ng-click='view(link)'>" +
    					"<div class='media-left media-image'><img src='{{link.image}}' alt='{{link.title}}'/></div>" +
    					"<div class='media-body'>" + 
    						"<h5><a href='{{link.url}}' target='_blank'>{{link.title}}</a></h5>" +
    						"<p>{{link.description}}<br/><strong>{{link.domain}}</strong></p>" +
    					"</div>" +
    				"</div>";
    
    	return {
    		restrict: 'EC',
    		replace: true,
    		template: tpl,
    		scope: false,
    		link: function($scope, $el, attrs, $rootScope ) {
    
    		}
    	}
    }]);
    				

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 = "<div class='my-yoga-instafeed'>" +
    				"<h3>From Instagram</h3>" +
    				"<div class='row'><div class='col-xs-12'><ul class='block-grid-xs-3 block-grid-sm-4'>" +
    					"<li ng-repeat='yogagram in yogagramList'>" +
    						"<span href='{{yogagram.link}}' ng-click='view(yogagram)' target='_blank'>" +
    							"<img class='img-responsive' src='{{yogagram.media.obj.low_resolution.url}}'/>" +
    						"</span>" +
    					"</li>" +
    				"</ul></div></div>" + 
    
    			"</div>";
    
    	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("<div class='clearfix' style='margin-top: 10px;'><p><a href='https://www.instagram.com/explore/tags/" + $scope.pose.sanskritName.replace(' ', '') + "' target='_blank'>See Instagram posts tagged as '" + $scope.pose.sanskritName + "'</a></p></div>");
    			});
    		}
    	}
    }]);
    

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 = "<div class='video-asset-list'><h3>{{videoListStatus}}</h3><div class='my-yoga-video-list'>" +
    				"<div class='yoga-asset video-asset media' ng-repeat='video in videoList' data-target='{{video.locator}}' ng-click='view(video)'>" +
    					"<div class='media-left media-image'><img src='{{video.img}}' alt='{{title}}'/></div>" +
    					"<div class='media-body'>" + 
    						"<h5>{{video.title}}</h5>" +
    						"<p><a href='https://youtu.be/{{video.locator}}'><strong>{{video.title}}</strong></a></p>" +
    						"<div class='duration'>{{video.duration}}</div>" +
    					"</div>" +
    				"</div>" +
    			"</div></div>";
    	return {
    		restrict: 'AC',
    		replace: true,
    		template: rootTpl,
    		scope: false,
    		link: function($scope, $el, attrs, $rootScope ) {
    
    			var videoTpl = "<div class='yoga-asset video-asset media' ng-repeat='video in videoList' data-target='{{video.locator}}' ng-click='view(video)'>" +
    					"<div class='media-left media-image'><img src='{{video.img}}' alt='{{title}}'/></div>" +
    					"<div class='media-body'>" + 
    						"<h5>{{video.title}}</h5>" +
    						"<p><a href='https://youtu.be/{{video.locator}}'><strong>{{video.title}}</strong></a></p>" +
    						"<div class='duration'>{{video.duration}}</div>" +
    					"</div>" +
    				"</div>";
    			$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: '<div class=""><div class="media-content"><img src="{{pose.image.url}}" style="max-width: 200px;"/></div><div>',
    		link: function($scope, $el, $attrs) {
    			//scope.watch mediaObj
    
    			var contentTpl= {
    				youtube: function(obj) {
    					console.log(obj);
    					var html = '<h4>'+obj.title+'</h4><div class="embed-responsive embed-responsive-4by3"><iframe class="" src="https://www.youtube.com/embed/'+obj.locator+'" frameborder="0" allowfullscreen></iframe></div>';
    					if(obj.tags && obj.tags.length) {
    						html += '<p>Tags: ';
    						for(var i = 0; i < obj.tags.length; i++) {
    							html += '<a href="#">' + obj.tags[i].tagName + '</a>';
    							if(i < obj.tags.length - 1)
    								html += ', ';
    						}
    						html += '</p>';
    					}
    					html += '</div>'
    					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 = "<div class='media'><div class='media-left'><a href='https://www.instagram.com/"+user.name+"'><img class='instagram-profile-pic' src='"+user.pic+"'/></a></div> ";
    					titleEL += "<div class='media-body'><h4 class='media-heading'><a href='https://www.instagram.com/"+user.name+"'>"+user.name+"</a></h4>Published:"+date+"</div></div>";
    
    					if(type == "video") {
    						console.log('showVideo');
    						mediaRow = "<div class='instagram-video'></div>";
    					}
    					else {
    						mediaRow = "<div class='instagram-image'><a href='"+media.link+"' target='_blank'><img class='img-responsive' src='"+media.obj.standard_resolution.url+"'/></a></div>";
    					}
    
    					captionRow = "<div class='instagramCaption'><h4>Caption</h4><p>"+caption+"</p></div>";
    					if(obj.myTags && obj.myTags.length) {
    						captionRow += "<div class='instagramCaption'><h4>Internal Tags:</h4><p>";
    						for(var i = 0; i < obj.myTags.length; i++) {
    							captionRow += '<a href="#">' + obj.myTags[i].tagName + '</a>';
    							if(i < obj.myTags.length - 1)
    								captionRow += ', ';
    						}
    						captionRow += '</p></div>';
    					}
    
    					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 = '<img src="'+newVal+'"/>';
    						}
    						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 = "<a href='"+newVal+"' target='_blank'>"+newVal+"</a>";
    					}
    					$el.find('.media-content').html(content);
    					console.log('showing', newVal);
    					$scope.$emit('newMedia', newVal)
    				}
    			}, true);
    		}
    	}
    });
    

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 + ',';
      }
      
      					

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);
    		});
    	}
    	
    }]);