Project 11: Story Time Viewer with Box

In Project 7 I created a storybook sample app where a father and son could remotely read a bedtime story over AT&T’s Enhanced WebRTC. On the right side was sample content that was statically added. That content had pagination controls to flip through pages of a storybook.

storytime7

However, this approach was a little clunky. In order to support more rich and dynamic content, a system would have to be built to take content and convert it into a format to be displayed for the web.

In my eleventh project of my 15 projects in 30 days challenge, I’m going to use Box’s View API to show how dynamic content can be supported with relative ease.

Box

In order to use the Box View API, you need an API key. Sign up for a developer account and create a new application.

storytimeviewer

Setup

For this project, there are six files. app.js is the Node.js app which fetches document ids of content added into the system and fetches session ids from the Box API to display content. index.html and storytimeviewer.js in the public directory is the AngularJS app. Partial views include: story_list.html displays available stories; add_story.html adds stories; and view_story.html displays the story using the viewer by Box.

// Filename app.js

var BOX_API_KEY = '';
var PORT = 8080;
var MONGODB_ADDRESS = 'mongodb://127.0.0.1:27017/test';

var express = require('express');
var bodyParser = require('body-parser');
var request = require('request');
var cors = require('cors');
var mongoose = require('mongoose');

var app = express();
app.listen(PORT);
app.use(bodyParser.json());
app.use(cors());

mongoose.connect(MONGODB_ADDRESS);

var StoryModel = mongoose.model('Stories', {
  title: String,
  documentId: String
});

app.post('/api/addcontent', function(req, res) {
  var options = {
    url: 'https://view-api.box.com/1/documents',
    method: 'POST',
    headers: {
      'Authorization': 'Token '+BOX_API_KEY
    },
    json: {
      url: req.body.url
    }
  };

  request(options, function(error, response, body) {
    if(response.statusCode != 202) {
      res.send({error: 'Unable to add content'});
      console.log(response);
      return;
    }

    StoryModel.create({title: req.body.title, documentId: body.id}, function(error, doc) {
      if(error) {
        res.send({error: 'Unable to add content'});
        return;
      }

      res.send({documentId: body.id});
    });
  });
});

app.get('/api/getstories', function(req, res) {
  StoryModel.find({}, function(error, stories) {
    var results = [];

    for(var i in stories) {
      results.push({
        title: stories[i].title, 
        documentId: stories[i].documentId
      });
    }

    res.send(results);
  });
});

app.post('/api/getsession', function(req, res) {
  var options = {
    url: 'https://view-api.box.com/1/sessions',
    method: 'POST',
    headers: {
      'Authorization': 'Token '+BOX_API_KEY
    },
    json: {
      document_id: req.body.documentId
    }
  };

  request(options, function(error, response, body) {
    if(response.statusCode != 201) {
      res.send({error: 'Unable to get session'});
      return;
    }

    res.send({sessionId: body.id});
  });
});

app.use(express.static(__dirname + '/public'));

console.log('App listening on port '+PORT);
<!-- Filename: public/index.html -->

<html ng-app="StoryTimeApp">
  <head>
    <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/viewer.js/0.10.7/crocodoc.viewer.min.css" />
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
    <script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/viewer.js/0.10.7/crocodoc.viewer.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.16/angular.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-route.min.js"></script>
    <script src="storytimeviewer.js"></script>
  </head>
  <body style="font-family:Arial">
    <h1><a href="#/" style="text-decoration:none; color:black">Story Time Viewer</a></h1>
    
    <div ng-view></div>
  </body>
</html>
// Filename: public/storytimeviewer.js

var StoryTimeApp = angular.module('StoryTimeApp', ['ngRoute']);

StoryTimeApp.config(['$routeProvider', function($routeProvider) {
  $routeProvider.
    when('/', {
      templateUrl: 'story_list.html',
      controller: 'StoryListCtrl'
    }).
    when('/add', {
      templateUrl: 'add_story.html',
      controller: 'AddStoryCtrl'
    }).
    when('/read/:documentId', {
      templateUrl: 'view_story.html',
      controller: 'ViewStoryCtrl'
    }).    
    otherwise({
      redirectTo: '/'
    });
}]);

StoryTimeApp.controller('StoryListCtrl', ['$scope', '$http', function($scope, $http) {
  $http.get('/api/getstories').then(function(response) {
    $scope.stories = response.data;
  });
}]);

StoryTimeApp.controller('AddStoryCtrl', ['$scope', '$http', '$location', function($scope, $http, $location) {
  $scope.loadContent = function() {
    $http.post('/api/addcontent', {url: $scope.contentUrl, title: $scope.title}).then(function(response) {
      if(response.data.error) {
        alert(response.data.error);
        return;
      }

      $scope.documentId = response.data.documentId;  

      $location.path('/');
    });
  }
}]);

StoryTimeApp.controller('ViewStoryCtrl', ['$scope', '$http', '$routeParams', function($scope, $http, $routeParams) {
  $scope.documentId = $routeParams.documentId;

  var viewer;

  $scope.previousPage = function() {
    $scope.currentPage--;
    viewer.scrollTo($scope.currentPage);
  };

  $scope.nextPage = function() {
    $scope.currentPage++;
    viewer.scrollTo($scope.currentPage);
  };

  $http.post('/api/getsession', {documentId: $scope.documentId}).then(function(response) {
    if(response.data.error) {
      alert('Unable to get session');
      return;
    }    

    $scope.sessionId = response.data.sessionId;

    viewer = Crocodoc.createViewer('#viewer', {
      url: 'https://view-api.box.com/1/sessions/'+response.data.sessionId+'/assets/',
      layout: Crocodoc.LAYOUT_PRESENTATION
    });

    viewer.on('ready', function(event) {
      console.log(event);
      $scope.$apply(function() {
        $scope.currentPage = 1;
        $scope.totalPageCount = event.data.numPages;
      });
    });

    viewer.load();
  });
}]);
<!-- Filename: public/story_list.html -->

<h2>Available Stories</h2>

<div ng-hide="stories.length">No stories have been added!</div>

<ul>
  <li ng-repeat="story in stories"><a href="#/read/{{story.documentId}}">{{story.title}}</a></li>
</ul>

<a href="#/add">Add a story</a>
<!-- Filename: public/view_story.html -->

<div ng-hide="totalPageCount">
  Loading story...
</div>

<div id="viewer" style="width:800px; height:600px"></div>

<div ng-show="totalPageCount">
  <input type="button" ng-click="previousPage()" ng-disabled="currentPage == 1" value="Previous Page" /> Page {{currentPage}} of {{totalPageCount}}
  <input type="button" ng-click="nextPage()" ng-disabled="currentPage == totalPageCount" value="Next Page" />
</div>
<p>Enter a URL to a PDF file:<br />
  <input type="text" ng-model="contentUrl" />
</p>
<p>Name the story:<br />
  <input type="text" ng-model="title" />
</p>

<input type="button" ng-click="loadContent()" value="Add Story" />
// package.json
{
  "name": "story-time",
  "description": "Story Time application for Node.js",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "express": "*",
    "body-parser": "*",
    "request": "*",
    "cors": "*",
    "mongoose": "*"
  }
}

In app.js, add your Box API key to BOX_API_KEY.

If you haven’t installed Node.js or MongoDB, please follow the instructions in my other project.

To install Node.js dependencies, run the following command:

npm install

And run the app:

nodejs app.js

Story Time Viewer

Story Time Viewer isn’t really useful without some content. First we need to find a good story to read, preferably in PDF format. Click on Add Story and paste the URL into the first input box. Give the story a title and then click on Add Story.

storytimeviewer2 storytimeviewer3

Story Time contact the Box API, provide the content URL, get a document id for this content, add it to the story list in MongoDB, and then redirect the browser back to the story list. You should see a new story in the list.

storytimeviewer4

Click on the story in the list. The AngularJS app will request a session id from the Node.js app, which will contact the Box API. The session id is a unique token that provides access to this document for a set amount of time (by default, 60 minutes).

When the session id is returned to the AngularJS app, it will load up the Box viewer and pass it the session id. The Box viewer displays the content.

storytimeviewer5

The previous and next buttons call the Box viewer’s methods to change the page. These event handlers can be modified to use WebSockets like Project 7 did to change the page in all participant’s browsers. This is left as an exercise for the reader.

storytimeviewer6

That’s it for this project. Here are some things that can be done:

  • This project adds more functionality to Project 7, where I built a story time demo to connect a father and son remotely for bedtime story. Replace the static content with this dynamic content alternative.
  • Add authentication so that stories added are associated with just that user (see Project 3 where I used Parse).

Post Mortem

I’ve followed Box’s View API since it was first announced last year and have always admired the grace at which it displays file formats that are not so browser friendly. While I didn’t show it, Story Time Viewer works really smoothly with Word documents.

I would have liked if the Box viewer provided access to an event callback for clicks. It would be cool to add sound effects when parts of the content are clicked on.

15 Projects in 30 Days Challenge

This blog post is part of my 15 projects in 30 days challenge. I’m hacking together 15 projects with different APIs, services, and technologies that I’ve had little to no exposure to. If my code isn’t completely efficient or accurate, please understand it isn’t meant to be complete and bulletproof. When something is left out, I try to mention it. Reach out to me and kindly teach me if I go towards the dark side. ?

This challenge serves a couple of purposes. First, I’ve always enjoyed hacking new things together and using APIs. And I haven’t had the chance (more like a reason) to dive in head first with things like AngularJS, Node.js, MongoDB, and Mailgun. This project demonstrated AngularJS, Node.js, MongoDB, and Mailgun.

This entry was posted in 15 Projects in 30 days. Bookmark the permalink.