Yesterday I blogged about a new project I'm building to demonstrate both Ionic and IBM Bluemix. I've made some progress on the project so I thought I'd share what I've built so far. My thinking is that as this project goes on I'll continue to share these updates so folks can see how I approach projects. Feel free to comment, criticize and make suggestions!

Before I begin, note that I've checked in my code to the Github repo: https://github.com/cfjedimaster/SauceDB. You can find the bits I'm covering today in the mobile folder.

This release focuses on the front end. I've decided to try to get as much of the mobile app done as possible. By using services in my Angular app, in theory, I can mock everything up with fake data and then - in theory - just replace with calls to the Node.js server. I'm not going to pretend that I think this will happen perfectly, but I find that if I focus on one aspect of the project at a time I can be a bit more productive. So let's look at the screens done so far.

Login

iOS Simulator Screen Shot Jul 15, 2015, 2.51.51 PM

This has a lot of wasted space - and I'd imagine some nice logo above the button - but as I don't have a logo yet (any volunteers) I've let it simple. The Facebook button is going to use the OpenFB library I mentioned before. Let's take a look at the controller.

.controller('LoginCtrl', function($scope, $ionicPlatform, ngFB, $rootScope, $state, $ionicHistory) {
	console.log('LoginCtrl');
  
  //used to skip auth when testing
  var skipAuth=true;
  
  $ionicPlatform.ready(function() {

    ngFB.init({appId: '467761313406666'});
    
    $scope.doLogin = function() {

      if(skipAuth) {
        $ionicHistory.nextViewOptions({
          disableBack: true
        });
        $state.go('Home');        
        return;
      }

      ngFB.login({scope: 'email'}).then(function(response) {          
          console.log('Facebook login succeeded', response.authResponse.accessToken);
          $rootScope.accessToken = response.authResponse.accessToken;
          ngFB.api({
            path:'/me',
            params: { fields: 'id,name'}
          }).then(
            function(user) {
              console.log(user);
              $rootScope.user = user;
              $ionicHistory.nextViewOptions({
                disableBack: true
              });
              $state.go('Home');
            }, function(err) {
              
          });
      },function(error) {
          alert('Facebook login failed: ' + error);
      });
                 
    }
    	
  });
	
})

The first thing you'll notice is a "skipAuth" flag. As you can guess, this lets me skip login while testing. Since I'm focused on building out my views and basic integration, I didn't want to have to relogin every time the page reloaded. When that little hack isn't enabled, you can see how I use OpenFB to both authenticate and then call the API to get information about the current user. My thinking here is that I want some basic details about you (email, name, profile picture) so I can greet you by name.

Feed

Next up is the "Feed"/Stream/etc view. This is meant to show all the most recent sauce reviews. Here is how it looks - and yes - it definitely needs some love. This also handles the "Add Review" logic of writing your own sauce reviews. For now though we'll focus on the feed.

iOS Simulator Screen Shot Jul 15, 2015, 3.40.26 PM

Let's take a look at the code. First, the controller.

.controller('HomeCtrl', function($scope,dataService,$ionicLoading,$ionicModal,$state) {
  console.log('HomeCtrl');
  $ionicLoading.show({template:"Loading feed..."});
  
  dataService.getFeed().then(function(res) {
    $ionicLoading.hide();
    $scope.feed = res;
  });

  $ionicModal.fromTemplateUrl('partials/findsauce.html', {
    scope: $scope
  }).then(function(modal) {
    $scope.modal = modal;
  });
  
  $scope.doSearch = function(term) {
    console.log('search for: '+term);
    dataService.searchSauce(term).then(function(res) {
      console.log('back from search with '+JSON.stringify(res));
      $scope.results = res;
    });
  }
  
  $scope.doLoad = function(id) {
    $scope.modal.hide();
    $state.go("AddReview", {id:id});
  }
  
  $scope.addReviewForm = function() {
    $scope.modal.show();
  }

})

The part that gets the feed simply speaks to the dataService I mentioned earlier. I'll talk more about 'Add Review' in a bit. Here's the service method handling the feed:

var getFeed = function() {
	var deferred = $q.defer();
		
	//fake it till we make it
	var feed = [];
	var promises = [];
		
	for(var i=1;i<5;i++) {
		promises.push($http.get('http://api.randomuser.me/?x='+Math.random()));
	}

	$q.all(promises).then(function(data) {
		for(var i=0;i<data.length;i++) {
			var user = data[i].data.results[0].user;
			var item = {
				id:i,
				posted:"July 14, 2015 at 2:32 PM",
				sauce:{
					name:"Sauce "+i,
					company:"Company "+i
				},
				rating:Math.round(Math.random()*5) + 1,
				avgrating:Math.round(Math.random()*5) + 1,
				text:"This sauce was rather spicy with a nice aftertaste...",
				user:{
					img:user.picture.thumbnail,
					name:user.name.first+' '+user.name.last
				}
			}
			feed.push(item);			
		}
		deferred.resolve(feed);
	});
		
	return deferred.promise;
}

For absolutely no good reason, I decided I'd make use of the Random User Generator API for my mocked data. On reflection that seems kind of stupid and a waste of time, but it was fun to check out the service and get 'real' pictures/names in the feed. Plus, it made the service a bit slow which felt a bit more life like. Each feed item consists of a sauce object (just the name, company) and a review object (user, rating, etc). As I write this, I see that "avgrating" really should be part of the Sauce object (something I naturally did in other code) so I'll have to fix that soon.

Clicking on Add Review pops open a modal. The idea for the modal is to let you search for a sauce to see if it exists. If it does, you can select it and then just write your review. Otherwise you need to include the sauce name and company when writing the review. I built a very simple "search as you type" service in the modal window:

iOS Simulator Screen Shot Jul 15, 2015, 3.52.45 PM

I showed the search code at the controller layer up above. The search service itself is hard coded:

var searchSauce = function(term) {
	var deferred = $q.defer();
	term = term.toLowerCase();
		
	//use hard coded set of names 
	var names = [
		"Alpha","Amma","Anna","Anno","Alphabet","Alcazam"
	]
	var results = [];
	for(var i=0;i<names.length;i++) {
		if(names[i].toLowerCase().indexOf(term) >= 0) results.push({id:1,label:names[i]});	
	}
	deferred.resolve(results);
	return deferred.promise;
	
}

Adding a Review

My static list is short, but useful enough for testing. For now, I don't support adding new sauces, so I just bound the list to a new review form:

iOS Simulator Screen Shot Jul 15, 2015, 3.55.02 PM

The fancy star widget there came from https://github.com/fraserxu/ionic-rating. It's rather easy to use. You can see it in the view below:

<!--
	I'll be used for adding a new review to an existing sauce and
	for adding a review to a new sauce.
-->
<ion-view title="Add Review">
	
	<ion-content class="padding">
	
		<div ng-if="existingSauce">
			Sauce: {{sauce.name}}<br/>
			Company: {{sauce.company}}
		</div>
		
		<!-- your text -->
		<div class="list list-inset">
		  <label class="item item-input">
			  <textarea placeholder="Your Review" ng-model="review.text"></textarea>
		  </label>
		</div>

		Rating: <rating ng-model="review.rate" max="max"></rating>		

		<button class="button button-assertive button-full" ng-click="addReview()">Add Rating</button>

	</ion-content>

</ion-view>

For now, clicking the Add Review button just returns the user to the sauce.

Sauce View

Speaking of - you can also view a sauce and all its reviews. This really needs some formatting love:

iOS Simulator Screen Shot Jul 15, 2015, 3.59.17 PM

For yet another completely silly reason, I changed the user pics here to kittens. Because kittens. Here's the service method that "fakes" a sauce retrieval:

var getSauce = function(id) {
	var deferred = $q.defer();
	//so a review is the Sauce object + array of reviews
	//to keep it simpler, we'll skip the fancy randomuser integration
	var sauce = {
		name:"Sauce "+id,
		company:"Company "+id,
		avgrating:Math.round(Math.random()*5) + 1,
		reviews:[]
	}
	for(var i=0;i<Math.round(Math.random()*10) + 1;i++) {
		var item = {
			id:i,
			posted:"July 14, 2015 at 2:32 PM",
			rating:Math.round(Math.random()*5) + 1,
			text:"This sauce was rather spicy with a nice aftertaste...",
			user:{
				img:"http://placekitten.com/g/40/40",
				name:"Joe Blow"
			}
		}
		sauce.reviews.push(item);
	}
	deferred.resolve(sauce);
	return deferred.promise;
}

So there ya go. It isn't pretty - but it is coming together. Tomorrow I'm going to switch to the server side and start setting up both my Cloudant database and the Node.js application.