├── app ├── css │ ├── .gitignore │ ├── Ubuntu.ttf │ ├── Ubuntu.font │ └── app.css ├── img │ ├── .gitignore │ ├── 100x100.gif │ ├── 150x150.gif │ ├── customer_bkg.jpg │ ├── restaurants │ │ ├── angular.jpg │ │ ├── beans.jpg │ │ ├── beijing.jpg │ │ ├── bhangra.jpg │ │ ├── cancun.jpg │ │ ├── curryup.jpg │ │ ├── dragon.jpg │ │ ├── esthers.jpg │ │ ├── flavia.jpg │ │ ├── jeeves.jpg │ │ ├── luigis.jpg │ │ ├── north.jpg │ │ ├── pedros.jpg │ │ ├── pizza76.jpg │ │ ├── sakura.jpg │ │ ├── sallys.jpg │ │ ├── satay.jpg │ │ ├── saucy.jpg │ │ ├── thick.jpg │ │ ├── zardoz.jpg │ │ ├── babythai.jpg │ │ ├── burgerama.jpg │ │ ├── carthage.jpg │ │ ├── khartoum.jpg │ │ ├── kohlhaus.jpg │ │ ├── shandong.jpg │ │ ├── taqueria.jpg │ │ ├── bateaurouge.jpg │ │ ├── currygalore.jpg │ │ ├── czechpoint.jpg │ │ ├── littlepigs.jpg │ │ ├── littleprague.jpg │ │ ├── naansequitur.jpg │ │ ├── robatayaki.jpg │ │ ├── speisewagen.jpg │ │ ├── superwonton.jpg │ │ ├── tofuparadise.jpg │ │ ├── wheninrome.jpg │ │ └── wholetamale.jpg │ └── glyphicons-halflings.png ├── views │ ├── .gitignore │ ├── who-we-are.html │ ├── thank-you.html │ ├── how-it-works.html │ ├── help.html │ ├── customer.html │ ├── menu.html │ ├── restaurants.html │ └── checkout.html ├── lib │ └── angular │ │ ├── version.txt │ │ ├── angular-cookies.min.js │ │ ├── angular-loader.min.js │ │ ├── angular-resource.min.js │ │ ├── angular-sanitize.min.js │ │ ├── angular-cookies.js │ │ ├── angular-loader.js │ │ ├── angular-resource.js │ │ └── angular-sanitize.js ├── favicon.ico ├── js │ ├── services │ │ ├── alert.js │ │ ├── localStorage.js │ │ ├── Restaurant.js │ │ ├── customer.js │ │ └── cart.js │ ├── directives │ │ ├── fmDeliverTo.html │ │ ├── fmDeliverTo.js │ │ ├── fmCheckboxList.js │ │ └── fmRating.js │ ├── controllers │ │ ├── ThankYouController.js │ │ ├── NavbarController.js │ │ ├── MenuController.js │ │ ├── CustomerController.js │ │ ├── CheckoutController.js │ │ └── RestaurantsController.js │ ├── filters │ │ ├── dollars.js │ │ └── stars.js │ └── app.js └── index.html ├── .gitignore ├── server ├── start.js ├── storage.js ├── model.js ├── index.js └── data │ ├── restaurants.csv │ ├── menus.csv │ └── restaurants.json ├── package.json └── README.md /app/css/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/lib/angular/version.txt: -------------------------------------------------------------------------------- 1 | 1.0.2 2 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/js/services/alert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.value('alert', window.alert); 4 | -------------------------------------------------------------------------------- /app/css/Ubuntu.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/css/Ubuntu.ttf -------------------------------------------------------------------------------- /app/img/100x100.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/100x100.gif -------------------------------------------------------------------------------- /app/img/150x150.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/150x150.gif -------------------------------------------------------------------------------- /app/img/customer_bkg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/customer_bkg.jpg -------------------------------------------------------------------------------- /app/js/services/localStorage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.value('localStorage', window.localStorage); 4 | -------------------------------------------------------------------------------- /app/img/restaurants/angular.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/angular.jpg -------------------------------------------------------------------------------- /app/img/restaurants/beans.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/beans.jpg -------------------------------------------------------------------------------- /app/img/restaurants/beijing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/beijing.jpg -------------------------------------------------------------------------------- /app/img/restaurants/bhangra.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/bhangra.jpg -------------------------------------------------------------------------------- /app/img/restaurants/cancun.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/cancun.jpg -------------------------------------------------------------------------------- /app/img/restaurants/curryup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/curryup.jpg -------------------------------------------------------------------------------- /app/img/restaurants/dragon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/dragon.jpg -------------------------------------------------------------------------------- /app/img/restaurants/esthers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/esthers.jpg -------------------------------------------------------------------------------- /app/img/restaurants/flavia.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/flavia.jpg -------------------------------------------------------------------------------- /app/img/restaurants/jeeves.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/jeeves.jpg -------------------------------------------------------------------------------- /app/img/restaurants/luigis.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/luigis.jpg -------------------------------------------------------------------------------- /app/img/restaurants/north.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/north.jpg -------------------------------------------------------------------------------- /app/img/restaurants/pedros.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/pedros.jpg -------------------------------------------------------------------------------- /app/img/restaurants/pizza76.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/pizza76.jpg -------------------------------------------------------------------------------- /app/img/restaurants/sakura.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/sakura.jpg -------------------------------------------------------------------------------- /app/img/restaurants/sallys.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/sallys.jpg -------------------------------------------------------------------------------- /app/img/restaurants/satay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/satay.jpg -------------------------------------------------------------------------------- /app/img/restaurants/saucy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/saucy.jpg -------------------------------------------------------------------------------- /app/img/restaurants/thick.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/thick.jpg -------------------------------------------------------------------------------- /app/img/restaurants/zardoz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/zardoz.jpg -------------------------------------------------------------------------------- /app/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /app/img/restaurants/babythai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/babythai.jpg -------------------------------------------------------------------------------- /app/img/restaurants/burgerama.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/burgerama.jpg -------------------------------------------------------------------------------- /app/img/restaurants/carthage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/carthage.jpg -------------------------------------------------------------------------------- /app/img/restaurants/khartoum.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/khartoum.jpg -------------------------------------------------------------------------------- /app/img/restaurants/kohlhaus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/kohlhaus.jpg -------------------------------------------------------------------------------- /app/img/restaurants/shandong.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/shandong.jpg -------------------------------------------------------------------------------- /app/img/restaurants/taqueria.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/taqueria.jpg -------------------------------------------------------------------------------- /app/img/restaurants/bateaurouge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/bateaurouge.jpg -------------------------------------------------------------------------------- /app/img/restaurants/currygalore.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/currygalore.jpg -------------------------------------------------------------------------------- /app/img/restaurants/czechpoint.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/czechpoint.jpg -------------------------------------------------------------------------------- /app/img/restaurants/littlepigs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/littlepigs.jpg -------------------------------------------------------------------------------- /app/img/restaurants/littleprague.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/littleprague.jpg -------------------------------------------------------------------------------- /app/img/restaurants/naansequitur.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/naansequitur.jpg -------------------------------------------------------------------------------- /app/img/restaurants/robatayaki.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/robatayaki.jpg -------------------------------------------------------------------------------- /app/img/restaurants/speisewagen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/speisewagen.jpg -------------------------------------------------------------------------------- /app/img/restaurants/superwonton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/superwonton.jpg -------------------------------------------------------------------------------- /app/img/restaurants/tofuparadise.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/tofuparadise.jpg -------------------------------------------------------------------------------- /app/img/restaurants/wheninrome.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/wheninrome.jpg -------------------------------------------------------------------------------- /app/img/restaurants/wholetamale.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritarenee15/foodme-platzi/HEAD/app/img/restaurants/wholetamale.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | nbproject 3 | manifest.mf 4 | build.xml 5 | 6 | .project 7 | .settings 8 | .idea/* 9 | node_modules/ 10 | -------------------------------------------------------------------------------- /app/js/services/Restaurant.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.factory('Restaurant', function($resource) { 4 | return $resource('/api/restaurant/:id', {id: '@id'}); 5 | }); 6 | -------------------------------------------------------------------------------- /app/css/Ubuntu.font: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Ubuntu'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Ubuntu'), url(Ubuntu.ttf) format('truetype'); 6 | } 7 | -------------------------------------------------------------------------------- /app/js/directives/fmDeliverTo.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /app/views/who-we-are.html: -------------------------------------------------------------------------------- 1 |
2 |

Who we are

3 |
4 |

We are purple unicorns jockeys and knitters of woolen socks!

5 |
6 |
7 | -------------------------------------------------------------------------------- /app/js/controllers/ThankYouController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.controller('ThankYouController', function ThankYouController($scope, $routeParams) { 4 | $scope.orderId = $routeParams.orderId; 5 | }); 6 | -------------------------------------------------------------------------------- /app/views/thank-you.html: -------------------------------------------------------------------------------- 1 |
2 |

Thank you for your order!

3 | 4 |

Our chefs are getting your food ready. It will be on its way shortly.

5 | 6 |

Your order ID is {{orderId}}.

7 |
8 | -------------------------------------------------------------------------------- /server/start.js: -------------------------------------------------------------------------------- 1 | var PORT = process.env.PORT || 3000; 2 | var STATIC_DIR = __dirname + '/../app'; 3 | var TEST_DIR = __dirname + '/../test'; 4 | var DATA_FILE = __dirname + '/data/restaurants.json'; 5 | 6 | require('./index').start(PORT, STATIC_DIR, DATA_FILE, TEST_DIR); 7 | -------------------------------------------------------------------------------- /app/js/controllers/NavbarController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.controller('NavbarController', function NavbarController($scope, $location) { 4 | 5 | $scope.routeIs = function(routeName) { 6 | return $location.path() === routeName; 7 | }; 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /app/js/controllers/MenuController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.controller('MenuController', 4 | function MenuController($scope, $routeParams, Restaurant, cart) { 5 | 6 | $scope.restaurant = Restaurant.get({id: $routeParams.restaurantId}); 7 | $scope.cart = cart; 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /app/js/filters/dollars.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.filter('dollars', function() { 4 | var DOLLARS = { 5 | 1: '$', 6 | 2: '$$', 7 | 3: '$$$', 8 | 4: '$$$$', 9 | 5: '$$$$$' 10 | } 11 | 12 | return function(dollarCount) { 13 | return DOLLARS[dollarCount] 14 | }; 15 | }); 16 | -------------------------------------------------------------------------------- /app/js/directives/fmDeliverTo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.directive('fmDeliverTo', function() { 4 | return { 5 | restrict: 'E', 6 | templateUrl: 'js/directives/fmDeliverTo.html', 7 | scope: {}, 8 | controller: function FmDeliverToController($scope, customer) { 9 | $scope.customer = customer; 10 | } 11 | }; 12 | }); 13 | -------------------------------------------------------------------------------- /app/js/filters/stars.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.filter('stars', function() { 4 | var STARS = { 5 | 1: '\u2605', 6 | 2: '\u2605\u2605', 7 | 3: '\u2605\u2605\u2605', 8 | 4: '\u2605\u2605\u2605\u2605', 9 | 5: '\u2605\u2605\u2605\u2605\u2605' 10 | } 11 | 12 | return function(dollarCount) { 13 | return STARS[dollarCount] 14 | }; 15 | }); 16 | -------------------------------------------------------------------------------- /app/views/how-it-works.html: -------------------------------------------------------------------------------- 1 |
2 |

How it works

3 |
4 |

It's simple:

5 |
    6 |
  1. Enter delivery address
  2. 7 |
  3. Pick a great restaurant
  4. 8 |
  5. Select yummy food
  6. 9 |
  7. Enter delivery time
  8. 10 |
  9. Enter payment details
  10. 11 |
  11. And the food will be on the way!
  12. 12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /app/views/help.html: -------------------------------------------------------------------------------- 1 |
2 |

Help

3 |
4 |

Until how late do you deliver?

5 |

We deliver as late as 9pm from all restaurants.

6 | 7 |

What payment methods do you accept?

8 |

We gladly accept all major credit cards.

9 | 10 |

I got an extra chocolate muffin with my order, what is this about?

11 |

That's just a small thank-you gift from FoodMe. Enjoy it!

12 |
13 |
14 | -------------------------------------------------------------------------------- /app/js/controllers/CustomerController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.controller('CustomerController', 4 | function CustomerController($scope, customer, $location) { 5 | 6 | $scope.customerName = customer.name; 7 | $scope.customerAddress = customer.address; 8 | 9 | 10 | $scope.findRestaurants = function(customerName, customerAddress) { 11 | customer.name = customerName; 12 | customer.address = customerAddress; 13 | 14 | $location.url('/'); 15 | }; 16 | }); 17 | -------------------------------------------------------------------------------- /app/js/services/customer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.factory('customer', function($rootScope, localStorage) { 4 | 5 | var LOCAL_STORAGE_ID = 'fmCustomer', 6 | customerString = localStorage[LOCAL_STORAGE_ID]; 7 | 8 | var customer = customerString ? JSON.parse(customerString) : { 9 | name: undefined, 10 | address: undefined 11 | }; 12 | 13 | $rootScope.$watch(function() { return customer; }, function() { 14 | localStorage[LOCAL_STORAGE_ID] = JSON.stringify(customer); 15 | }, true); 16 | 17 | return customer; 18 | }); 19 | -------------------------------------------------------------------------------- /app/js/controllers/CheckoutController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.controller('CheckoutController', 4 | function CheckoutController($scope, cart, customer, $location) { 5 | 6 | $scope.cart = cart; 7 | $scope.restaurantId = cart.restaurant.id; 8 | $scope.customer = customer; 9 | $scope.submitting = false; 10 | 11 | 12 | $scope.purchase = function() { 13 | if ($scope.submitting) return; 14 | 15 | $scope.submitting = true; 16 | cart.submitOrder().then(function(orderId) { 17 | $location.path('thank-you').search({orderId: orderId}); 18 | }); 19 | }; 20 | }); 21 | -------------------------------------------------------------------------------- /server/storage.js: -------------------------------------------------------------------------------- 1 | var MemoryStorage = function() { 2 | var storage = []; 3 | 4 | this.getAll = function() { 5 | return storage; 6 | }; 7 | 8 | this.add = function(item) { 9 | storage.push(item); 10 | }; 11 | 12 | this.getById = function(id) { 13 | for (var i = 0; i < storage.length; i++) { 14 | if (storage[i].id === id) { 15 | return storage[i]; 16 | } 17 | } 18 | 19 | return null; 20 | }; 21 | 22 | this.deleteById = function(id) { 23 | for (var i = 0; i < storage.length; i++) { 24 | if (storage[i].id === id) { 25 | storage.splice(i, 1); 26 | return true; 27 | } 28 | } 29 | 30 | return false; 31 | }; 32 | }; 33 | 34 | exports.Memory = MemoryStorage; 35 | -------------------------------------------------------------------------------- /app/lib/angular/angular-cookies.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.2 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(m,f,l){'use strict';f.module("ngCookies",["ng"]).factory("$cookies",["$rootScope","$browser",function(d,c){var b={},g={},h,i=!1,j=f.copy,k=f.isUndefined;c.addPollFn(function(){var a=c.cookies();h!=a&&(h=a,j(a,g),j(a,b),i&&d.$apply())})();i=!0;d.$watch(function(){var a,e,d;for(a in g)k(b[a])&&c.cookies(a,l);for(a in b)e=b[a],f.isString(e)?e!==g[a]&&(c.cookies(a,e),d=!0):f.isDefined(g[a])?b[a]=g[a]:delete b[a];if(d)for(a in e=c.cookies(),b)b[a]!==e[a]&&(k(e[a])?delete b[a]:b[a]=e[a])});return b}]).factory("$cookieStore", 7 | ["$cookies",function(d){return{get:function(c){return f.fromJson(d[c])},put:function(c,b){d[c]=f.toJson(b)},remove:function(c){delete d[c]}}}])})(window,window.angular); 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FoodMe", 3 | "version": "0.0.5", 4 | "description": "Super awesome tutorial", 5 | "keywords": [], 6 | "homepage": "http://angularjs.org/", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/angular/foodme.git" 10 | }, 11 | "author": "AngularJS Team", 12 | "contributors": [ 13 | "Igor Minár =0.2.1", 24 | "express": ">=3.0.0", 25 | "morgan": "^1.10.0", 26 | "open": "^8.4.2", 27 | "pino": "^8.7.0" 28 | }, 29 | "engines" : { 30 | "node": ">=20.14.0" 31 | }, 32 | "license": "MIT" 33 | } 34 | -------------------------------------------------------------------------------- /app/lib/angular/angular-loader.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.2 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(i){'use strict';function d(c,b,e){return c[b]||(c[b]=e())}return d(d(i,"angular",Object),"module",function(){var c={};return function(b,e,f){e&&c.hasOwnProperty(b)&&(c[b]=null);return d(c,b,function(){function a(a,b,d){return function(){c[d||"push"]([a,b,arguments]);return g}}if(!e)throw Error("No module: "+b);var c=[],d=[],h=a("$injector","invoke"),g={_invokeQueue:c,_runBlocks:d,requires:e,name:b,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"), 7 | value:a("$provide","value"),constant:a("$provide","constant","unshift"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:h,run:function(a){d.push(a);return this}};f&&h(f);return g})}})})(window); 8 | -------------------------------------------------------------------------------- /app/js/directives/fmCheckboxList.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.directive('fmCheckboxList', function() { 4 | return { 5 | require: 'ngModel', 6 | link: function(scope, elm, attr, ngModel) { 7 | 8 | // model -> view 9 | ngModel.$render = function() { 10 | var values = ngModel.$modelValue || []; 11 | angular.forEach(elm.find('input'), function(input) { 12 | input.checked = values.indexOf(input.getAttribute('value')) !== -1; 13 | }); 14 | }; 15 | 16 | // view -> model 17 | elm.bind('click', function(e) { 18 | if (angular.lowercase(e.target.nodeName) === 'input') { 19 | scope.$apply(function() { 20 | var values = []; 21 | 22 | angular.forEach(elm.find('input'), function(input) { 23 | if (input.checked) { 24 | values.push(input.getAttribute('value')); 25 | } 26 | }); 27 | 28 | ngModel.$setViewValue(values); 29 | }); 30 | } 31 | }); 32 | } 33 | }; 34 | }); 35 | -------------------------------------------------------------------------------- /app/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var foodMeApp = angular.module('foodMeApp', ['ngResource']); 4 | 5 | foodMeApp.config(function($routeProvider) { 6 | 7 | $routeProvider. 8 | when('/', { 9 | controller: 'RestaurantsController', 10 | templateUrl: 'views/restaurants.html' 11 | }). 12 | when('/menu/:restaurantId', { 13 | controller: 'MenuController', 14 | templateUrl: 'views/menu.html' 15 | }). 16 | when('/checkout', { 17 | controller: 'CheckoutController', 18 | templateUrl: 'views/checkout.html' 19 | }). 20 | when('/thank-you', { 21 | controller: 'ThankYouController', 22 | templateUrl: 'views/thank-you.html' 23 | }). 24 | when('/customer', { 25 | controller: 'CustomerController', 26 | templateUrl: 'views/customer.html' 27 | }). 28 | when('/who-we-are', { 29 | templateUrl: 'views/who-we-are.html' 30 | }). 31 | when('/how-it-works', { 32 | templateUrl: 'views/how-it-works.html' 33 | }). 34 | when('/help', { 35 | templateUrl: 'views/help.html' 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /app/views/customer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 36 |
37 | -------------------------------------------------------------------------------- /app/views/menu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 |
8 | 9 |
10 | 11 |
12 |

{{restaurant.name}}

13 | 14 |
15 |
16 |
{{restaurant.address}}
17 | 18 | 19 | 20 | 21 |
22 |
23 |
{{restaurant.description}}
24 |
25 |
26 |
27 |
28 |
29 | 30 | 31 |
32 | 33 |
34 |

Menu

35 | 36 | 37 | 46 | 47 |
48 | 49 |
50 |

Your order

51 |
52 |
    53 |
  • 54 | 55 | {{item.qty}} × {{item.name}} 56 |
  • 57 |
58 |
59 | Checkout 60 |
61 |

62 | Total: {{cart.total() | currency}} 63 |

64 |
65 |
66 | 67 |
68 | -------------------------------------------------------------------------------- /server/model.js: -------------------------------------------------------------------------------- 1 | var idFromName = function(name) { 2 | return name && name.toLowerCase().replace(/\W/g, ''); 3 | }; 4 | 5 | var isString = function(value) { 6 | return typeof value === 'string'; 7 | }; 8 | 9 | var DAYS = { 10 | Su: 0, 11 | Mo: 1, 12 | Tu: 2, 13 | We: 3, 14 | Th: 4, 15 | Fr: 5, 16 | Sa: 6 17 | }; 18 | 19 | var parseDays = function(str) { 20 | return str.split(',').map(function(day) { 21 | return DAYS[day]; 22 | }); 23 | }; 24 | 25 | 26 | var Restaurant = function(data) { 27 | // defaults 28 | this.days = [1, 2, 3, 4, 5, 6]; 29 | this.menuItems = []; 30 | this.price = 0; 31 | this.rating = 0; 32 | 33 | this.update(data); 34 | 35 | this.id = this.id || idFromName(this.name); 36 | }; 37 | 38 | Restaurant.prototype.update = function(data) { 39 | Object.keys(data).forEach(function(key) { 40 | if (key === 'price' || key === 'rating' && isString(data[key])) { 41 | this[key] = parseInt(data[key], 10); 42 | } else { 43 | this[key] = data[key]; 44 | } 45 | }, this); 46 | 47 | this.menuItems = this.menuItems.map(function(data) { 48 | return new MenuItem(data); 49 | }); 50 | }; 51 | 52 | Restaurant.prototype.validate = function(errors) { 53 | if (!this.name) { 54 | errors.push('Invalid: "name" is a mandatory field!'); 55 | } 56 | 57 | return errors.length === 0; 58 | }; 59 | 60 | Restaurant.fromArray = function(data) { 61 | return new Restaurant({ 62 | id: data[1], 63 | name: data[0], 64 | cuisine: data[2], 65 | opens: data[3], 66 | closes: data[4], 67 | days: parseDays(data[5]), 68 | price: parseInt(data[6], 10), 69 | rating: parseInt(data[7], 10), 70 | location: data[8], 71 | description: data[9] 72 | }); 73 | }; 74 | 75 | 76 | var MenuItem = function(data) { 77 | this.name = data.name; 78 | this.price = data.price; 79 | }; 80 | 81 | MenuItem.fromArray = function(data) { 82 | return new MenuItem({ 83 | name: data[1], 84 | price: parseFloat(data[2]) 85 | }); 86 | }; 87 | 88 | exports.Restaurant = Restaurant; 89 | exports.MenuItem = MenuItem; 90 | -------------------------------------------------------------------------------- /app/views/restaurants.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 |

Filter Restaurants

8 | 9 |
10 | 11 |
Rating
12 | 13 | 14 | 15 | 16 |
Price
17 | 18 | 19 | 20 | 21 |
Cuisine
22 | 23 |
24 | 27 |
28 |
29 |
30 | 31 | 32 |
33 |

34 | 38 | 39 |

40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 55 | 58 | 61 | 62 |
Name {{sortIconFor('name')}}Price {{sortIconFor('price')}}Rating {{sortIconFor('rating')}}
49 | 50 | 51 | {{restaurant.name}} 52 | 53 |

{{restaurant.description}}

54 |
56 | 57 | 59 | 60 |
63 |
64 |
65 | -------------------------------------------------------------------------------- /app/js/directives/fmRating.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.directive('fmRating', function() { 4 | return { 5 | restrict: 'E', 6 | scope: { 7 | symbol: '@', 8 | max: '@', 9 | readonly: '@' 10 | }, 11 | require: 'ngModel', 12 | link: function(scope, element, attrs, ngModel) { 13 | 14 | attrs.max = scope.max = parseInt(scope.max || 5, 10); 15 | 16 | if (!attrs.symbol) { 17 | attrs.symbol = scope.symbol = '\u2605'; 18 | } 19 | 20 | var styles = []; 21 | scope.styles = styles; 22 | 23 | 24 | for(var i = 0; i < scope.max; i ++) { 25 | styles.push({ 'fm-selected': false, 'fm-hover': false }); 26 | } 27 | 28 | scope.enter = function(index) { 29 | if (scope.readonly) return; 30 | angular.forEach(styles, function(style, i) { 31 | style['fm-hover'] = i <= index; 32 | }); 33 | }; 34 | 35 | scope.leave = function(index) { 36 | if (scope.readonly) return; 37 | angular.forEach(styles, function(style, i) { 38 | style['fm-hover'] = false; 39 | }); 40 | }; 41 | 42 | 43 | // view -> model 44 | scope.select = function(index) { 45 | if (scope.readonly) return; 46 | 47 | ngModel.$setViewValue((index == null) ? null : index + 1); 48 | udpateSelectedStyles(index); 49 | }; 50 | 51 | 52 | // model -> view 53 | ngModel.$render = function() { 54 | udpateSelectedStyles(ngModel.$viewValue - 1); 55 | }; 56 | 57 | function udpateSelectedStyles(index) { 58 | if (index == null) index = -1; 59 | 60 | angular.forEach(styles, function(style, i) { 61 | style['fm-selected'] = i <= index; 62 | }); 63 | } 64 | }, 65 | template: 66 | '' + 72 | 'clear' 73 | }; 74 | }); 75 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | // Paste New Relic Browser agent here 7 | 8 | FoodMe 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/js/controllers/RestaurantsController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.controller('RestaurantsController', 4 | function RestaurantsController($scope, customer, $location, Restaurant) { 5 | 6 | if (!customer.address) { 7 | $location.url('/customer'); 8 | } 9 | 10 | var filter = $scope.filter = { 11 | cuisine: [], 12 | price: null, 13 | rating: null 14 | }; 15 | 16 | var allRestaurants = Restaurant.query(filterAndSortRestaurants); 17 | $scope.$watch('filter', filterAndSortRestaurants, true); 18 | 19 | function filterAndSortRestaurants() { 20 | $scope.restaurants = []; 21 | 22 | // filter 23 | angular.forEach(allRestaurants, function(item, key) { 24 | if (filter.price && filter.price !== item.price) { 25 | return; 26 | } 27 | 28 | if (filter.rating && filter.rating !== item.rating) { 29 | return; 30 | } 31 | 32 | if (filter.cuisine.length && filter.cuisine.indexOf(item.cuisine) === -1) { 33 | return; 34 | } 35 | 36 | $scope.restaurants.push(item); 37 | }); 38 | 39 | 40 | // sort 41 | $scope.restaurants.sort(function(a, b) { 42 | if (a[filter.sortBy] > b[filter.sortBy]) { 43 | return filter.sortAsc ? 1 : -1; 44 | } 45 | 46 | if (a[filter.sortBy] < b[filter.sortBy]) { 47 | return filter.sortAsc ? -1 : 1; 48 | } 49 | 50 | return 0; 51 | }); 52 | }; 53 | 54 | 55 | $scope.sortBy = function(key) { 56 | if (filter.sortBy === key) { 57 | filter.sortAsc = !filter.sortAsc; 58 | } else { 59 | filter.sortBy = key; 60 | filter.sortAsc = true; 61 | } 62 | }; 63 | 64 | 65 | $scope.sortIconFor = function(key) { 66 | if (filter.sortBy !== key) { 67 | return ''; 68 | } 69 | 70 | return filter.sortAsc ? '\u25B2' : '\u25BC'; 71 | }; 72 | 73 | 74 | $scope.CUISINE_OPTIONS = { 75 | african: 'African', 76 | american: 'American', 77 | barbecue: 'Barbecue', 78 | cafe: 'Cafe', 79 | chinese: 'Chinese', 80 | 'czech/slovak': 'Czech / Slovak', 81 | german: 'German', 82 | indian: 'Indian', 83 | japanese: 'Japanese', 84 | mexican: 'Mexican', 85 | pizza: 'Pizza', 86 | thai: 'Thai', 87 | vegetarian: 'Vegetarian' 88 | }; 89 | 90 | }); 91 | -------------------------------------------------------------------------------- /app/lib/angular/angular-resource.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.2 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(A,f,u){'use strict';f.module("ngResource",["ng"]).factory("$resource",["$http","$parse",function(v,w){function g(b,c){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(c?null:/%20/g,"+")}function l(b,c){this.template=b+="#";this.defaults=c||{};var a=this.urlParams={};j(b.split(/\W/),function(c){c&&b.match(RegExp("[^\\\\]:"+c+"\\W"))&&(a[c]=!0)});this.template=b.replace(/\\:/g,":")}function s(b,c,a){function f(d){var b= 7 | {};j(c||{},function(a,x){var m;a.charAt&&a.charAt(0)=="@"?(m=a.substr(1),m=w(m)(d)):m=a;b[x]=m});return b}function e(a){t(a||{},this)}var y=new l(b),a=r({},z,a);j(a,function(d,g){var l=d.method=="POST"||d.method=="PUT"||d.method=="PATCH";e[g]=function(a,b,c,g){var i={},h,k=o,p=null;switch(arguments.length){case 4:p=g,k=c;case 3:case 2:if(q(b)){if(q(a)){k=a;p=b;break}k=b;p=c}else{i=a;h=b;k=c;break}case 1:q(a)?k=a:l?h=a:i=a;break;case 0:break;default:throw"Expected between 0-4 arguments [params, data, success, error], got "+ 8 | arguments.length+" arguments.";}var n=this instanceof e?this:d.isArray?[]:new e(h);v({method:d.method,url:y.url(r({},f(h),d.params||{},i)),data:h}).then(function(a){var b=a.data;if(b)d.isArray?(n.length=0,j(b,function(a){n.push(new e(a))})):t(b,n);(k||o)(n,a.headers)},p);return n};e.bind=function(d){return s(b,r({},c,d),a)};e.prototype["$"+g]=function(a,b,d){var c=f(this),i=o,h;switch(arguments.length){case 3:c=a;i=b;h=d;break;case 2:case 1:q(a)?(i=a,h=b):(c=a,i=b||o);case 0:break;default:throw"Expected between 1-3 arguments [params, success, error], got "+ 9 | arguments.length+" arguments.";}e[g].call(this,c,l?this:u,i,h)}});return e}var z={get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}},o=f.noop,j=f.forEach,r=f.extend,t=f.copy,q=f.isFunction;l.prototype={url:function(b){var c=this,a=this.template,f,b=b||{};j(this.urlParams,function(e,d){f=g(b[d]||c.defaults[d]||"",!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+");a=a.replace(RegExp(":"+d+"(\\W)"),f+"$1")});var a= 10 | a.replace(/\/?#$/,""),e=[];j(b,function(a,b){c.urlParams[b]||e.push(g(b)+"="+g(a))});e.sort();a=a.replace(/\/*$/,"");return a+(e.length?"?"+e.join("&"):"")}};return s}])})(window,window.angular); 11 | -------------------------------------------------------------------------------- /app/views/checkout.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Menu

4 | 5 |
6 |
7 |
Deliver To:
8 |
{{customer.name}}
9 |
{{customer.address}}
10 | Change 11 |
12 |
13 |
Payment:
14 |
15 | 23 |
24 |
25 | 30 |
31 | 32 | 36 | 37 | 38 | 42 | 43 |
44 |
45 | 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
QtyDescriptionPriceSubtotal
{{item.name}}{{item.price | currency}}{{item.price * item.qty | currency}}
Total:{{ cart.total() | currency }}
70 | 71 |
72 | Back to Menu 73 | Clear Cart 74 | 77 |
78 |
79 |
80 |
81 | -------------------------------------------------------------------------------- /app/js/services/cart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foodMeApp.service('cart', function Cart(localStorage, customer, $rootScope, $http, alert) { 4 | var self = this; 5 | 6 | 7 | self.add = function(item, restaurant) { 8 | if (!self.restaurant || !self.restaurant.id) { 9 | self.restaurant = { 10 | id: restaurant.id, 11 | name: restaurant.name, 12 | description: restaurant.description 13 | }; 14 | } 15 | 16 | if (self.restaurant.id == restaurant.id) { 17 | self.items.forEach(function(cartItem) { 18 | if (item && cartItem.name == item.name) { 19 | cartItem.qty ++; 20 | item = null; 21 | } 22 | }); 23 | if (item) { 24 | item = angular.copy(item); 25 | item.qty = 1; 26 | self.items.push(item); 27 | } 28 | } else { 29 | alert('Can not mix menu items from different restaurants.'); 30 | } 31 | }; 32 | 33 | 34 | self.remove = function(item) { 35 | var index = self.items.indexOf(item); 36 | if (index >= 0) { 37 | self.items.splice(index, 1); 38 | } 39 | if (!self.items.length) { 40 | self.restaurant = {}; 41 | } 42 | } 43 | 44 | 45 | self.total = function() { 46 | return self.items.reduce(function(sum, item) { 47 | return sum + Number(item.price * item.qty); 48 | }, 0); 49 | }; 50 | 51 | 52 | self.submitOrder = function() { 53 | if (self.items.length) { 54 | return $http.post('/api/order', { 55 | items: self.items, 56 | restaurant: self.restaurant, 57 | payment: self.payment, 58 | deliverTo: customer 59 | }).then(function(response) { 60 | /**************************************** 61 | /* Custom Attributes * 62 | /**************************************** 63 | self.items.forEach( 64 | function(item) { 65 | newrelic.addPageAction('orderItem', { 66 | restaurant: self.restaurant.name, 67 | item: item.name, 68 | qty: item.qty 69 | }); 70 | }); 71 | /****************************************/ 72 | self.reset(); 73 | return response.data.orderId; 74 | }); 75 | } 76 | } 77 | 78 | 79 | self.reset = function() { 80 | self.items = []; 81 | self.restaurant = {}; 82 | }; 83 | 84 | 85 | createPersistentProperty('items', 'fmCartItems', Array); 86 | createPersistentProperty('restaurant', 'fmCartRestaurant', Object); 87 | self.payment = {}; // don't keep CC info in localStorage 88 | 89 | 90 | function createPersistentProperty(localName, storageName, Type) { 91 | var json = localStorage[storageName]; 92 | 93 | self[localName] = json ? JSON.parse(json) : new Type; 94 | 95 | $rootScope.$watch( 96 | function() { return self[localName]; }, 97 | function(value) { 98 | if (value) { 99 | localStorage[storageName] = JSON.stringify(value); 100 | } 101 | }, 102 | true); 103 | } 104 | }); 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Platzi Challenge - Food Delivery Service App 2 | 3 | ## The Task 4 | For the next 3 weeks, you'll be creating your very own food delivery application, using the FoodMe open source application. 5 | This application has been around for a long time, and looks like it needs some updates. Using your tech skills, you're going to be able to modernize this application and make it your ideal food delivery service app. 6 | 7 | Haven't used a food delivery application before? Some examples include Rappi and UberEats if you'd like to get some ideas for design and features. 8 | 9 | Remember: This challenge is meant for developers of all levels, and some of the functionality has been built in to give you a great start. Use the documentation from Angular, Bootstrap, Node, and your favorite resources on HTML, CSS, JavaScript to help you. 10 | 11 | ## Requirements 12 | - The application must have one restaurant that features food from your country. 13 | - The application must have some design changes from the original application (colors, font, photos for example). 14 | - The application cannot accept American Express cards. 15 | - You must use GitHub Copilot to help you build the application. 16 | - You must instrument your application with New Relic using the Browser Monitoring Guided Install and the Node.js Guided Install. 17 | 18 | ## Run the application 19 | `npm run start` 20 | 21 | ## Stop the application 22 | Press `Ctrl` + `C` 23 | 24 | ## Bonus tasks 25 | - Throughout the challenge, [Rita](https://www.linkedin.com/in/rita-hill/) will appear with bonus tasks for the challenge. These are not required to complete the challenge, but we encourage you to give them a try. 26 | 27 | ## Additional resources 28 | - [Angular Documentation](https://angular.dev/overview). 29 | 30 | ### Want to learn more? 31 | More information about this application [here](http://goo.gl/Xa0Ea) 32 | 33 | The app is build on top of [angular-seed](http://github.com/angular/angular-seed), 34 | check out seed's README to understand what the scripts under `scripts/` are doing. 35 | 36 | ## Bonus tasks 37 | **13 June 2024** - Last year I went ot Bogotà, Colombia for the first time and I learned about Rappi 🛵, a food delivery service app that brought all of the delicious Colombian food that I wanted to my apartment. I used Rappi because while I don't speak a lot of Spanish, I know my numbers very well, so giving them the code to confirm my identity was easy. I ordered empanadas almost everyday, and ate my empanadas with the best coffee I had in Colombia from Libertario. 38 | 39 | For your first bonus task, I want you to add a restaurant to your application that sells coffee and empanadas! 40 | 41 | ** 26 June 2024** - Two years ago, I went to Mexico City, and on my first day in the city, I visited Cafebrería El Péndulo. It was the largest bookstore I had ever seen! My friend, who was Mexican, showed me many books by different Spanish speaking poets that she loved. We spent an hour looking at the books before having lunch on the top floor. 42 | 43 | For your next bonus task, I want you to create your own Cafebrería, and name each food item on the menu after a book from a Spanish speaking writer. 44 | -------------------------------------------------------------------------------- /app/lib/angular/angular-sanitize.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.2 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(I,g){'use strict';function i(a){var d={},a=a.split(","),b;for(b=0;b=0;e--)if(f[e]==b)break;if(e>=0){for(c=f.length-1;c>=e;c--)d.end&&d.end(f[c]);f.length= 7 | e}}var c,h,f=[],j=a;for(f.last=function(){return f[f.length-1]};a;){h=!0;if(!f.last()||!q[f.last()]){if(a.indexOf("<\!--")===0)c=a.indexOf("--\>"),c>=0&&(d.comment&&d.comment(a.substring(4,c)),a=a.substring(c+3),h=!1);else if(B.test(a)){if(c=a.match(r))a=a.substring(c[0].length),c[0].replace(r,e),h=!1}else if(C.test(a)&&(c=a.match(s)))a=a.substring(c[0].length),c[0].replace(s,b),h=!1;h&&(c=a.indexOf("<"),h=c<0?a:a.substring(0,c),a=c<0?"":a.substring(c),d.chars&&d.chars(k(h)))}else a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+ 8 | f.last()+"[^>]*>","i"),function(b,a){a=a.replace(D,"$1").replace(E,"$1");d.chars&&d.chars(k(a));return""}),e("",f.last());if(a==j)throw"Parse Error: "+a;j=a}e()}function k(a){l.innerHTML=a.replace(//g,">")}function u(a){var d=!1,b=g.bind(a,a.push);return{start:function(a,c,h){a=g.lowercase(a);!d&&q[a]&&(d=a);!d&&v[a]== 9 | !0&&(b("<"),b(a),g.forEach(c,function(a,c){var e=g.lowercase(c);if(G[e]==!0&&(w[e]!==!0||a.match(H)))b(" "),b(c),b('="'),b(t(a)),b('"')}),b(h?"/>":">"))},end:function(a){a=g.lowercase(a);!d&&v[a]==!0&&(b(""));a==d&&(d=!1)},chars:function(a){d||b(t(a))}}}var s=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,r=/^<\s*\/\s*([\w:-]+)[^>]*>/,A=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,C=/^/g, 10 | E=//g,H=/^((ftp|https?):\/\/|mailto:|#)/,F=/([^\#-~| |!])/g,p=i("area,br,col,hr,img,wbr"),x=i("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),y=i("rp,rt"),o=g.extend({},y,x),m=g.extend({},x,i("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),n=g.extend({},y,i("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")), 11 | q=i("script,style"),v=g.extend({},p,m,n,o),w=i("background,cite,href,longdesc,src,usemap"),G=g.extend({},w,i("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,span,start,summary,target,title,type,valign,value,vspace,width")),l=document.createElement("pre");g.module("ngSanitize",[]).value("$sanitize",function(a){var d=[]; 12 | z(a,u(d));return d.join("")});g.module("ngSanitize").directive("ngBindHtml",["$sanitize",function(a){return function(d,b,e){b.addClass("ng-binding").data("$binding",e.ngBindHtml);d.$watch(e.ngBindHtml,function(c){c=a(c);b.html(c||"")})}}]);g.module("ngSanitize").filter("linky",function(){var a=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/,d=/^mailto:/;return function(b){if(!b)return b;for(var e=b,c=[],h=u(c),f,g;b=e.match(a);)f=b[0],b[2]==b[3]&&(f="mailto:"+f),g=b.index, 13 | h.chars(e.substr(0,g)),h.start("a",{href:f}),h.chars(b[0].replace(d,"")),h.end("a"),e=e.substring(g+b[0].length);h.chars(e);return c.join("")}})})(window,window.angular); 14 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | // Uncomment after adding New Relic agent to project 2 | // const newrelic = require('newrelic'); 3 | 4 | const express = require('express'); 5 | const logger = require('pino')(); 6 | const morgan = require('morgan'); 7 | const bodyParser = require('body-parser'); 8 | 9 | const fs = require('fs'); 10 | const open = require('open'); 11 | 12 | const RestaurantRecord = require('./model').Restaurant; 13 | const MemoryStorage = require('./storage').Memory; 14 | 15 | const API_URL = '/api/restaurant'; 16 | const API_URL_ID = API_URL + '/:id'; 17 | const API_URL_ORDER = '/api/order'; 18 | 19 | var removeMenuItems = function(restaurant) { 20 | var clone = {}; 21 | 22 | Object.getOwnPropertyNames(restaurant).forEach(function(key) { 23 | if (key !== 'menuItems') { 24 | clone[key] = restaurant[key]; 25 | } 26 | }); 27 | 28 | return clone; 29 | }; 30 | 31 | exports.start = function(PORT, STATIC_DIR, DATA_FILE, TEST_DIR) { 32 | var app = express(); 33 | var storage = new MemoryStorage(); 34 | 35 | // log requests 36 | app.use(morgan('combined')); 37 | 38 | // serve static files for demo client 39 | app.use(express.static(STATIC_DIR)); 40 | 41 | // create application/json parser 42 | var jsonParser = bodyParser.json(); 43 | 44 | // API 45 | app.get(API_URL, function(req, res, next) { 46 | res.send(200, storage.getAll().map(removeMenuItems)); 47 | }); 48 | 49 | app.post(API_URL, function(req, res, next) { 50 | var restaurant = new RestaurantRecord(req.body); 51 | var errors = []; 52 | 53 | if (restaurant.validate(errors)) { 54 | storage.add(restaurant); 55 | return res.send(201, restaurant); 56 | } 57 | 58 | return res.send(400, {error: errors}); 59 | }); 60 | 61 | app.post(API_URL_ORDER, jsonParser, function(req, res, next) { 62 | logger.info(req.body, 'checkout'); 63 | 64 | /************************************* 65 | /* Custom attributes * 66 | /************************************* 67 | var order = req.body; 68 | var itemCount = 0; 69 | var orderTotal = 0; 70 | order.items.forEach(function(item) { 71 | itemCount += item.qty; 72 | orderTotal += item.price * item.qty; 73 | }); 74 | 75 | newrelic.addCustomAttributes({ 76 | 'customer': order.deliverTo.name, 77 | 'restaurant': order.restaurant.name, 78 | 'itemCount': itemCount, 79 | 'orderTotal': orderTotal 80 | }); 81 | /*************************************/ 82 | 83 | return res.send(201, { orderId: Date.now()}); 84 | }); 85 | 86 | app.get(API_URL_ID, function(req, res, next) { 87 | var restaurant = storage.getById(req.params.id); 88 | if (restaurant) { 89 | return res.send(200, restaurant); 90 | } 91 | return res.send(400, {error: 'No restaurant with id "' + req.params.id + '"!'}); 92 | }); 93 | 94 | app.put(API_URL_ID, function(req, res, next) { 95 | var restaurant = storage.getById(req.params.id); 96 | var errors = []; 97 | 98 | if (restaurant) { 99 | restaurant.update(req.body); 100 | return res.send(200, restaurant); 101 | } 102 | 103 | restaurant = new RestaurantRecord(req.body); 104 | if (restaurant.validate(errors)) { 105 | storage.add(restaurant); 106 | return res.send(201, restaurant); 107 | } 108 | 109 | return res.send(400, {error: errors}); 110 | }); 111 | 112 | app.del(API_URL_ID, function(req, res, next) { 113 | if (storage.deleteById(req.params.id)) { 114 | return res.send(204, null); 115 | } 116 | return res.send(400, {error: 'No restaurant with id "' + req.params.id + '"!'}); 117 | }); 118 | 119 | // read the data from json and start the server 120 | fs.readFile(DATA_FILE, function(err, data) { 121 | JSON.parse(data).forEach(function(restaurant) { 122 | storage.add(new RestaurantRecord(restaurant)); 123 | }); 124 | 125 | app.listen(PORT, function() { 126 | open('http://localhost:' + PORT + '/'); 127 | // console.log('Go to http://localhost:' + PORT + '/'); 128 | }); 129 | }); 130 | 131 | // Windows and Node.js before 0.8.9 would crash 132 | // https://github.com/joyent/node/issues/1553 133 | try { 134 | process.on('SIGINT', function() { 135 | // save the storage back to the json file 136 | fs.writeFile(DATA_FILE, JSON.stringify(storage.getAll()), function() { 137 | process.exit(0); 138 | }); 139 | }); 140 | } catch (e) {} 141 | 142 | }; 143 | -------------------------------------------------------------------------------- /app/css/app.css: -------------------------------------------------------------------------------- 1 | /**************************************** 2 | * general purpose css-only components * 3 | ****************************************/ 4 | 5 | .fm-panel { 6 | background-color: #fff; 7 | 8 | border: 1px solid black; 9 | 10 | -webkit-border-top-left-radius: 3px; 11 | -moz-border-radius-topleft: 3px; 12 | border-top-left-radius: 3px; 13 | 14 | -webkit-border-top-right-radius: 3px; 15 | -moz-border-radius-topright: 3px; 16 | border-top-right-radius: 3px; 17 | 18 | margin-bottom: 20px; 19 | padding: 10px 10px 20px 10px; 20 | 21 | overflow: hidden; 22 | } 23 | 24 | .fm-panel > h4 { 25 | background-color: #E5E5E5; 26 | margin: -10px -10px 10px -10px; 27 | padding: 10px 10px 10px 10px; 28 | } 29 | 30 | 31 | .fm-right { 32 | text-align: right; 33 | } 34 | 35 | 36 | 37 | /**************************************** 38 | * angular components * 39 | ****************************************/ 40 | 41 | .fm-rating { 42 | color: #a9a9a9; 43 | margin: 0; 44 | padding: 0; 45 | 46 | } 47 | 48 | ul.fm-rating { 49 | display: inline-block; 50 | } 51 | 52 | .fm-rating-pointer { 53 | cursor: pointer; 54 | } 55 | 56 | .fm-rating li { 57 | list-style-type: none; 58 | display: inline-block; 59 | padding: 1px; 60 | text-align: center; 61 | font-weight: bold; 62 | } 63 | 64 | .fm-rating .fm-selected { 65 | color: #000000; 66 | } 67 | 68 | .fm-rating .fm-hover { 69 | text-shadow: white 0 0 2px, 70 | white 0 0 2px, 71 | white 0 0 2px, 72 | white 0 0 2px, 73 | blue 0 0 10px; 74 | } 75 | 76 | fm-rating a { 77 | visibility: hidden; 78 | padding-left: 7px; 79 | } 80 | 81 | fm-rating:hover a { 82 | visibility: visible; 83 | } 84 | 85 | 86 | .fm-deliver-to { 87 | display: block; 88 | width: 450px; 89 | } 90 | 91 | 92 | input.ng-invalid.ng-dirty { 93 | border-color: #E9322D; 94 | -webkit-box-shadow: 0 0 6px #F8B9B7; 95 | -moz-box-shadow: 0 0 6px #f8b9b7; 96 | box-shadow: 0 0 6px #F8B9B7; 97 | } 98 | 99 | 100 | 101 | 102 | /**************************************** 103 | * route specific css * 104 | ****************************************/ 105 | 106 | 107 | /* customer */ 108 | 109 | .fm-customer-bkg { 110 | background-image: url(../img/customer_bkg.jpg); 111 | opacity: 0.4; 112 | height: 705px; 113 | } 114 | 115 | 116 | /* restaurant list */ 117 | 118 | .fm-restaurant-list { 119 | min-height: 646px !important; 120 | } 121 | 122 | .fm-restaurant-list table { 123 | margin: -10px -20px -20px -10px; 124 | width: 700px; 125 | } 126 | 127 | .fm-restaurant-list th:first-child { 128 | width: 500px; 129 | } 130 | 131 | .fm-restaurant-list th { 132 | width: 100px; 133 | } 134 | 135 | .fm-restaurant-list img { 136 | margin-right: 10px; 137 | width: 100px; 138 | height: 100px; 139 | } 140 | 141 | 142 | /* menu */ 143 | 144 | .fm-restaurant { 145 | margin-bottom: 20px; 146 | } 147 | 148 | .fm-restaurant h3 { 149 | margin-top: 0; 150 | } 151 | 152 | .fm-restaurant img { 153 | width: 150px; 154 | height: 150px; 155 | } 156 | 157 | .fm-restaurant .fm-rating { 158 | display: block; 159 | } 160 | 161 | .fm-menu-list li { 162 | list-style-type: none; 163 | } 164 | 165 | .fm-menu-list li span { 166 | padding-right: 20px; 167 | } 168 | 169 | .fm-menu-list a { 170 | color: inherit; 171 | } 172 | 173 | .fm-menu-list a:hover { 174 | text-decoration: none; 175 | color: inherit; 176 | } 177 | 178 | .fm-menu-list .icon-plus-sign { 179 | visibility: hidden; 180 | } 181 | 182 | .fm-menu-list li:hover .icon-plus-sign { 183 | visibility: visible; 184 | } 185 | 186 | .fm-menu-list li:hover { 187 | color: black; 188 | } 189 | 190 | .fm-menu-list ul li { 191 | text-align: center; 192 | } 193 | 194 | .fm-cart li { 195 | text-overflow: ellipsis; 196 | white-space: nowrap; 197 | overflow: hidden; 198 | } 199 | 200 | .fm-cart .icon-remove-sign { 201 | visibility: hidden; 202 | } 203 | 204 | .fm-cart li:hover .icon-remove-sign { 205 | visibility: visible; 206 | } 207 | 208 | .fm-cart form { 209 | padding-left: 2px; 210 | } 211 | 212 | .fm-cart p { 213 | padding-left: 20px; 214 | padding-top: 5px; 215 | } 216 | 217 | 218 | /* checkout */ 219 | 220 | .fm-checkout table { 221 | width: 960px; 222 | margin-left: -10px; 223 | } 224 | 225 | .fm-checkout table th:nth-child(2), 226 | .fm-checkout table td:nth-child(2) { 227 | width: 100%; 228 | text-align: left; 229 | } 230 | 231 | .fm-checkout table td:nth-child(1) input { 232 | width: 30px; 233 | } 234 | 235 | .fm-checkout table th, 236 | .fm-checkout table td { 237 | text-align: right; 238 | } 239 | 240 | .fm-checkout table tr:last-child th { 241 | text-align: right; 242 | } 243 | 244 | input.ng-invalid { 245 | border: 1px solid red; 246 | } 247 | -------------------------------------------------------------------------------- /app/lib/angular/angular-cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.2 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) { 7 | 'use strict'; 8 | 9 | /** 10 | * @ngdoc overview 11 | * @name ngCookies 12 | */ 13 | 14 | 15 | angular.module('ngCookies', ['ng']). 16 | /** 17 | * @ngdoc object 18 | * @name ngCookies.$cookies 19 | * @requires $browser 20 | * 21 | * @description 22 | * Provides read/write access to browser's cookies. 23 | * 24 | * Only a simple Object is exposed and by adding or removing properties to/from 25 | * this object, new cookies are created/deleted at the end of current $eval. 26 | * 27 | * @example 28 | */ 29 | factory('$cookies', ['$rootScope', '$browser', function ($rootScope, $browser) { 30 | var cookies = {}, 31 | lastCookies = {}, 32 | lastBrowserCookies, 33 | runEval = false, 34 | copy = angular.copy, 35 | isUndefined = angular.isUndefined; 36 | 37 | //creates a poller fn that copies all cookies from the $browser to service & inits the service 38 | $browser.addPollFn(function() { 39 | var currentCookies = $browser.cookies(); 40 | if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl 41 | lastBrowserCookies = currentCookies; 42 | copy(currentCookies, lastCookies); 43 | copy(currentCookies, cookies); 44 | if (runEval) $rootScope.$apply(); 45 | } 46 | })(); 47 | 48 | runEval = true; 49 | 50 | //at the end of each eval, push cookies 51 | //TODO: this should happen before the "delayed" watches fire, because if some cookies are not 52 | // strings or browser refuses to store some cookies, we update the model in the push fn. 53 | $rootScope.$watch(push); 54 | 55 | return cookies; 56 | 57 | 58 | /** 59 | * Pushes all the cookies from the service to the browser and verifies if all cookies were stored. 60 | */ 61 | function push() { 62 | var name, 63 | value, 64 | browserCookies, 65 | updated; 66 | 67 | //delete any cookies deleted in $cookies 68 | for (name in lastCookies) { 69 | if (isUndefined(cookies[name])) { 70 | $browser.cookies(name, undefined); 71 | } 72 | } 73 | 74 | //update all cookies updated in $cookies 75 | for(name in cookies) { 76 | value = cookies[name]; 77 | if (!angular.isString(value)) { 78 | if (angular.isDefined(lastCookies[name])) { 79 | cookies[name] = lastCookies[name]; 80 | } else { 81 | delete cookies[name]; 82 | } 83 | } else if (value !== lastCookies[name]) { 84 | $browser.cookies(name, value); 85 | updated = true; 86 | } 87 | } 88 | 89 | //verify what was actually stored 90 | if (updated){ 91 | updated = false; 92 | browserCookies = $browser.cookies(); 93 | 94 | for (name in cookies) { 95 | if (cookies[name] !== browserCookies[name]) { 96 | //delete or reset all cookies that the browser dropped from $cookies 97 | if (isUndefined(browserCookies[name])) { 98 | delete cookies[name]; 99 | } else { 100 | cookies[name] = browserCookies[name]; 101 | } 102 | updated = true; 103 | } 104 | } 105 | } 106 | } 107 | }]). 108 | 109 | 110 | /** 111 | * @ngdoc object 112 | * @name ngCookies.$cookieStore 113 | * @requires $cookies 114 | * 115 | * @description 116 | * Provides a key-value (string-object) storage, that is backed by session cookies. 117 | * Objects put or retrieved from this storage are automatically serialized or 118 | * deserialized by angular's toJson/fromJson. 119 | * @example 120 | */ 121 | factory('$cookieStore', ['$cookies', function($cookies) { 122 | 123 | return { 124 | /** 125 | * @ngdoc method 126 | * @name ngCookies.$cookieStore#get 127 | * @methodOf ngCookies.$cookieStore 128 | * 129 | * @description 130 | * Returns the value of given cookie key 131 | * 132 | * @param {string} key Id to use for lookup. 133 | * @returns {Object} Deserialized cookie value. 134 | */ 135 | get: function(key) { 136 | return angular.fromJson($cookies[key]); 137 | }, 138 | 139 | /** 140 | * @ngdoc method 141 | * @name ngCookies.$cookieStore#put 142 | * @methodOf ngCookies.$cookieStore 143 | * 144 | * @description 145 | * Sets a value for given cookie key 146 | * 147 | * @param {string} key Id for the `value`. 148 | * @param {Object} value Value to be stored. 149 | */ 150 | put: function(key, value) { 151 | $cookies[key] = angular.toJson(value); 152 | }, 153 | 154 | /** 155 | * @ngdoc method 156 | * @name ngCookies.$cookieStore#remove 157 | * @methodOf ngCookies.$cookieStore 158 | * 159 | * @description 160 | * Remove given cookie 161 | * 162 | * @param {string} key Id of the key-value pair to delete. 163 | */ 164 | remove: function(key) { 165 | delete $cookies[key]; 166 | } 167 | }; 168 | 169 | }]); 170 | 171 | })(window, window.angular); 172 | -------------------------------------------------------------------------------- /server/data/restaurants.csv: -------------------------------------------------------------------------------- 1 | Restaurant name,Restaurant ID,Cuisine,Opens,Closes,Days Open,Price,Rating,Location,Description 2 | Esther's German Saloon,esthers,german,11:30:00,22:30:00,"Mo,Tu,We,Th,Fr,Sa",3,3,22 Teutonic Ave.,"German home-cooked meals and fifty-eight different beers on tap. To get more authentic, you'd need to be wearing lederhosen." 3 | Robatayaki Hachi,robatayaki,japanese,17:30:00,23:30:00,"Mo,Tu,We,Th,Fr,Sa,Su",4,5,8 Hawthorne Ln.,"Japanese food the way you like it. Fast, fresh, grilled." 4 | BBQ Tofu Paradise,tofuparadise,vegetarian,16:30:00,20:00:00,"Mo,We,Fr,Sa,Su",2,1,22A King West,"Vegetarians, we have your BBQ needs covered. Our home-made tofu skewers and secret BBQ sauce will have you licking your fingers." 5 | Le Bateau Rouge,bateaurouge,french,17:00:00,23:30:00,"Mo,Tu,We,Th,Fr,Sa,Su",5,4,2 South Park Dr.,"Fine French dining in a romantic setting. From soupe à l'oignon to coq au vin, let our chef delight you with a local take on authentic favorites." 6 | Khartoum Khartoum,khartoum,african,11:00:00,14:00:00,"Mo,Tu,We,Th,Fr",3,2,1566 Maple Rd.,"African homestyle cuisine, cooked fresh daily." 7 | Sally's Diner,sallys,american,8:30:00,20:00:00,"Mo,Tu,We,Th,Fr,Sa,Su",4,3,96 College Blvd.,"Food like mom cooked, if you grew up in Iowa and mom ran a diner. Try our blue plate special!" 8 | Saucy Piggy,saucy,barbecue,15:00:00,22:00:00,"Tu,We,Th,Fr,Sa,Su",3,2,623 Industrial Rd.,"Pork. We know how to cook it. Award-winning BBQ sauce, and meat with all the trimmings." 9 | Czech Point,czechpoint,czech/slovak,10:30:00,21:30:00,"Mo,Tu,We,Th,Fr,Sa",1,4,5567 Queen-Mary Rd,Make a point of trying our knedlíky and homemade soups. We have free wifi and the best desserts and coffee. 10 | Der Speisewagen,speisewagen,german,17:00:00,22:30:00,"Mo,Tu,We,Th,Fr,Sa,Su",3,5,402 College Blvd.,Award-winning schnitzel and other favorites. Look for our restored food truck in the NE corner of the College St lot. 11 | Beijing Express,beijing,chinese,11:00:00,22:30:00,"Mo,We,Fr,Sa,Su",2,4,38 Teutonic Ave.,"Fast, healthy, Chinese food. Family specials for takeout or delivery. Try our Peking Duck!" 12 | Satay Village,satay,thai,17:30:00,22:00:00,"Mo,Tu,We,Th,Fr,Sa,Su",4,2,12 High St.,Fine dining Thai-style. Wide selection of vegetarian entrées. We also deliver. 13 | Cancun,cancun,mexican,11:30:00,23:00:00,"Mo,Tu,We,Th,Fr",3,3,2030 Maple Rd.,"Tacos, tortas, burritos, just the way you like them. Our hot sauce and guacamole are the best in town." 14 | Curry Up,curryup,indian,17:00:00,22:00:00,"Mo,Tu,We,Th,Fr,Sa,Su",4,5,455 University,Indian food with a modern twist. We use all-natural ingredients and the finest spices to delight and tempt your palate. 15 | Carthage,carthage,african,17:00:00,22:30:00,"Tu,We,Th,Fr,Sa,Su",2,1,59 Court Terrace,Wholesome food and all the rich flavor of Africa. Try our famous lentil soup. 16 | Burgerama,burgerama,american,11:00:00,23:00:00,"Mo,Tu,We,Th,Fr,Sa",5,4,456 University,"Grade A beef, freshly ground every day, hand-cut fries, and home-made milkshakes. We make the best burgers in town. " 17 | Three Little Pigs,littlepigs,barbecue,11:30:00,22:30:00,"Mo,Tu,We,Th,Fr,Sa,Su",3,2,12 Summer Court,Genuine East Texas barbecue. Accept no substitutes! 18 | Little Prague,littleprague,czech/slovak,11:00:00,22:00:00,"Mo,We,Fr,Sa,Su",4,3,44 Park Ave,We're famous for our housemade sausage and desserts. Come taste real European cooking. 19 | Kohl Haus,kohlhaus,german,17:00:00,22:30:00,"Mo,Tu,We,Th,Fr,Sa,Su",3,2,3421 Queen-Mary Rd,"East German specialties, in a family-friendly setting. Come warm up with our delicious soups." 20 | Dragon's Tail,dragon,chinese,17:00:00,2:00:00,"Mo,Tu,We,Th,Fr",1,4,8 Jasmine Rd.,Take-out or dine-in Chinese food. Open late. Delivery available 21 | Hit Me Baby One More Thai,babythai,thai,15:00:00,22:00:00,"Mo,Tu,We,Th,Fr,Sa,Su",3,5,12 Jasmine Rd.,"Thai food with a youthful bar scene. Try our tropical inspired cocktails, or tuck into a plate of our famous pad thai." 22 | The Whole Tamale,wholetamale,mexican,10:30:00,21:30:00,"Tu,We,Th,Fr,Sa,Su",2,4,401 University,The tamale and hot sauce experts. Tamale special changes daily. 23 | Birmingham Bhangra,bhangra,indian,17:00:00,22:30:00,"Mo,Tu,We,Th,Fr,Sa",4,2,992 Riddick St.,"Curry with a metropolitan twist. Daily specials. Dine-in or takeaway, you choose." 24 | Taqueria,taqueria,mexican,11:00:00,22:30:00,"Mo,Tu,We,Th,Fr,Sa,Su",3,3,12 North Circle Dr.,Taqueria y panaderia. Birria served on weekends. 25 | Pedro's,pedros,mexican,17:30:00,22:00:00,"Mo,We,Fr,Sa,Su",4,5,5521 Alameda,Pedro's has been an Alameda staple for thirty years. Our list of fine tequilas and slow-cooked carnitas will make you a regular. 26 | Super Wonton Express,superwonton,chinese,11:30:00,23:00:00,"Mo,Tu,We,Th,Fr,Sa,Su",2,1,223 Milliways Ave,"Soups, stir-fries, and more. We cook fast." 27 | Naan Sequitur,naansequitur,indian,17:00:00,22:00:00,"Mo,Tu,We,Th,Fr",5,4,"Unit 12, Olde Towne Mall","Naan and tandoori specialties, from our clay oven. " 28 | Sakura,sakura,japanese,17:00:00,22:30:00,"Mo,Tu,We,Th,Fr,Sa,Su",3,2,"Unit 18, Olde Towne Mall","Sushi specials daily. We serve fast, friendly, fresh Japanese cuisine." 29 | Shandong Lu,shandong,chinese,17:00:00,22:30:00,"Tu,We,Th,Fr,Sa,Su",4,3,335 University,Szechuan and Mandarin specialities with a fine dining ambiance. Our hot and sour soup is the best in town. 30 | Curry Galore,currygalore,indian,11:00:00,22:30:00,"Mo,Tu,We,Th,Fr,Sa",3,2,56 Park Ave,"Famous North Indian home cooking. Spicy or mild, as you like it. Delivery available." 31 | North by Northwest,north,cafe,6:00:00,18:00:00,"Mo,Tu,We,Th,Fr,Sa,Su",1,4,201 University,Great coffee and snacks. Free wifi. 32 | Full of Beans,beans,cafe,6:30:00,20:30:00,"Mo,We,Fr,Sa,Su",3,5,498 College Ave.,We roast on premises to give you the best cup of coffee in town. 33 | Tropical Jeeve's Cafe,jeeves,cafe,7:00:00,14:30:00,"Mo,Tu,We,Th,Fr,Sa,Su",2,4,550 Milliways Ave,"Hawaiian style coffee, fresh juices, and tropical fruit smoothies." 34 | Zardoz Cafe,zardoz,cafe,10:30:00,0:00:00,"Mo,Tu,We,Th,Fr",4,2,6202 Alameda,Coffee bar and sci-fi bookshop. Come in for an espresso or a slice of our famous pie. 35 | Angular Pizza,angular,pizza,6:00:00,0:00:00,"Mo,Tu,We,Th,Fr,Sa,Su",1,5,2232 King St.,Home of the superheroic pizza! 36 | Flavia,flavia,pizza,11:30:00,22:00:00,"Tu,We,Th,Fr,Sa,Su",4,5,401 Riddick St.,"Roman-style pizza -- square, the way the gods intended." 37 | Luigi's House of Pies,luigis,pizza,16:30:00,23:00:00,"Mo,Tu,We,Th,Fr,Sa",2,1,5 Garcia Ave.,Our secret pizza sauce makes our pizza better. We specialize in large groups. 38 | Thick and Thin,thick,pizza,17:00:00,23:30:00,"Mo,Tu,We,Th,Fr,Sa,Su",5,4,832 Dominican Ave.,"Whether you're craving Chicago-style deep dish or thin as a wafer crust, we have you covered in toppings you'll love." 39 | When in Rome,wheninrome,pizza,11:00:00,21:30:00,"Mo,We,Fr,Sa,Su",3,2,234 Valencia St.,Authentic Italian pizza in a friendly neighborhood joint. 40 | Pizza 76,pizza76,pizza,10:30:00,22:00:00,"Mo,Tu,We,Th,Fr,Sa,Su",4,3,76 Market St.,Wood-fired pizza with daily ingredients fresh from our farmer's market. We make our own mozzarella in house. -------------------------------------------------------------------------------- /server/data/menus.csv: -------------------------------------------------------------------------------- 1 | Cuisine,Item Name,Price 2 | african,Dibi Lamb,8.25 3 | african,Doro Wat,5.95 4 | african,Grilled Chicken,4.95 5 | african,Grilled Fish,6.95 6 | african,Grilled Plantains in Spicy Peanut Sauce,7.95 7 | african,Lamb Mafe (Peanut Butter Stew),8.75 8 | african,Meat Pie,6 9 | african,Mechoui with Plantains,5.95 10 | african,Pepper Soup,9.95 11 | african,Piri-Piri Shrimp,11.45 12 | african,Suppa Kandja,3.95 13 | african,Thiou Boulette,6.95 14 | african,Thiou Curry with Chicken,7.95 15 | african,Thu Okra,6.95 16 | african,Yassa Chicken,8.95 17 | african,Yassa Lamb,8.25 18 | american,Buffalo wings,5.95 19 | american,California-style baked Tilapia with rice,6.95 20 | american,Cheeseburger and fries,4.55 21 | american,Cherry pie a la mode,4.95 22 | american,Chocolate milkshake,6.95 23 | american,Cobb salad,4.95 24 | american,Famous BLT on a kaiser roll with fries,4.95 25 | american,Firehouse chili,6.95 26 | american,Goat cheese and eggplant wrap (vegetarian),5.95 27 | american,Greek salad,6.95 28 | american,Grilled chicken sandwich,4.95 29 | american,Grilled sausage on a bun,6.95 30 | american,Housemade pot roast with seasonal vegetable,16.95 31 | american,Roast beef dip,11.45 32 | american,Roast chicken and mashed potatoes,7.55 33 | american,Soup of the day,8.95 34 | american,Spaghetti and meatballs,11.45 35 | barbecue,BBQ chicken,4.95 36 | barbecue,Beef ribs (full),9.95 37 | barbecue,Beef ribs (delux),10.45 38 | barbecue,Beef ribs (half),6.45 39 | barbecue,Beer,7.55 40 | barbecue,Coleslaw,8.95 41 | barbecue,Collards,9.95 42 | barbecue,Cornbread,11.45 43 | barbecue,Devilled eggs,4 44 | barbecue,German chocolate cake,5.95 45 | barbecue,Housemade chips,4 46 | barbecue,Hushpuppies,3.25 47 | barbecue,Mac and cheese,6 48 | barbecue,Pork ribs (half),6.95 49 | barbecue,Potato salad,3.95 50 | barbecue,Pulled pork sandwich on a soft roll,4.95 51 | barbecue,Riblets,10.45 52 | cafe,Apple pie,5.95 53 | cafe,B.L.T. and Avocado Sandwich,5.95 54 | cafe,Caesar salad,5.95 55 | cafe,Cappucino,3.95 56 | cafe,Cherry cheesecake,4.95 57 | cafe,Chocolate chip cookie,4.55 58 | cafe,Cobb salad,6.95 59 | cafe,Drip coffee,5.95 60 | cafe,Eggsalad Sandwich,3.95 61 | cafe,Espresso,6.95 62 | cafe,Greek salad,3.95 63 | cafe,Hot tea,2.5 64 | cafe,Iced tea,2.5 65 | cafe,Latte,4 66 | cafe,Mango and banana smoothie,3 67 | cafe,Orange juice,4.95 68 | cafe,Quiche of the day,6.95 69 | cafe,Turkey Sandwich,7.55 70 | chinese,Almond cookie,5.95 71 | chinese,Chicken and broccoli,9.95 72 | chinese,Chow mein,4.95 73 | chinese,Egg rolls (4),3.95 74 | chinese,General Tao's chicken,5.95 75 | chinese,Hot and Sour Soup,7.55 76 | chinese,Hunan dumplings,6.5 77 | chinese,Mongolian beef,6.95 78 | chinese,Pan-fried beef noodle,7.95 79 | chinese,Pea shoots with garlic,8.95 80 | chinese,Potstickers (6),6.95 81 | chinese,Seafood hotpot,4.95 82 | chinese,Steamed rice,5.95 83 | chinese,Sweet and sour pork,6.95 84 | chinese,Walnut prawns,4.95 85 | chinese,Wonton Soup,6.25 86 | chinese,Young Chow fried rice,6.45 87 | czech/slovak,Apple strudel,6.95 88 | czech/slovak,Apricot dumpling with yogurt topping ,3.95 89 | czech/slovak,Balkánský Salad,3.95 90 | czech/slovak,Beef goulash,8.95 91 | czech/slovak,Chicken breast fillet schnitzel ,10.45 92 | czech/slovak,Cucumber Salad,7.55 93 | czech/slovak,Dumplings,5.95 94 | czech/slovak,Fried goose liver with onion and bread,7.55 95 | czech/slovak,Halusky with sauerkraut and belly bacon ,9.95 96 | czech/slovak,Lentil soup,4.5 97 | czech/slovak,Pickles with cabbage and cheddar,10.45 98 | czech/slovak,Pork schnitzel,3.95 99 | czech/slovak,Potato pancake with bacon,5.95 100 | czech/slovak,Potato Salad,4.55 101 | czech/slovak,Segedínský gulash and dumplings,6.95 102 | czech/slovak,Sour cabbage soup,10.45 103 | french,bavette dans son jus,4.95 104 | french,bœuf bourguignon ,18 105 | french,bouillabaisse,17.5 106 | french,coq au vin,10.45 107 | french,frites aïoli , 108 | french,moules et frites,3.95 109 | french,poulet au riesling,4.95 110 | french,quiche lorraine,7.95 111 | french,salade de chèvre chaud,5.95 112 | french,salade du midi,6.95 113 | french,salade niçoise,3.95 114 | french,sandwich croque-madame,3.95 115 | french,sandwich croque-monsieur,4.55 116 | french,soupe à l'oignon,9.95 117 | french,steak frites,5.95 118 | french,tarte pissaladière ,6.95 119 | french,tarte tatin,5.95 120 | german,Bockwurst Würstchen,4.95 121 | german,Bratwurst mit Brötchen und Sauerkraut,5.95 122 | german,Currywurst mit Brötchen,5.95 123 | german,Das Hausmannskost,11.45 124 | german,Fleishkas mit Kartoffelsalat,6.95 125 | german,Frankfurter Würstchen,9.95 126 | german,Französische Zwiebelsuppe mit Käse,10.45 127 | german,Frikadelle mit Brötchen,6.95 128 | german,Gebackener Camenbert,7.55 129 | german,Gemischter Salat,4.55 130 | german,Haus Salatteller,11.45 131 | german,Jaegerschnitzel,9.95 132 | german,Kaesepaetzle,6.95 133 | german,Kartoffel Reibekuchen mit Apfelmus,5.95 134 | german,Maultaschen mit Käse,7.95 135 | german,Sauerbraten,10.45 136 | german,Ungarische Gulaschsuppe mit Brötchen,3.95 137 | german,Wienerschnitzel,8.95 138 | german,Wurstsalad mit Bauernbrot,6.95 139 | indian,Aloo Gobi,5.95 140 | indian,Basmati rice,6.95 141 | indian,Butter Chicken,5.95 142 | indian,Chicken Korma,7.55 143 | indian,Chicken Tikka Masala,5.95 144 | indian,Gulab Jamun,8.95 145 | indian,Kheer,4.5 146 | indian,Lamb Asparagus,9.5 147 | indian,Lamb Vindaloo,7.95 148 | indian,Mix Grill Bombay,5.95 149 | indian,Mulligatawny soup,5.95 150 | indian,Murgh Chicken,3.95 151 | indian,Naan stuffed with spinach and lamb,4.55 152 | indian,Plain naan,5.95 153 | indian,Rogan Josh,5.95 154 | indian,Saag Paneer,5.95 155 | indian,Tandoori Chicken,4.95 156 | japanese,California roll,5.95 157 | japanese,Chicken teriyaki,5.95 158 | japanese,Edamame,6.95 159 | japanese,Futomaki roll, 160 | japanese,Green tea ice cream,6.95 161 | japanese,Kitsune Udon,9.5 162 | japanese,Miso soup,5.95 163 | japanese,Pork Katsu,5.95 164 | japanese,Salmon teriyaki,5.95 165 | japanese,Sashimi combo,6.95 166 | japanese,Spicy Yellowtail roll,7.55 167 | japanese,Sushi combo,4.55 168 | japanese,Teppa Maki,5.95 169 | japanese,Unagi Don,7.55 170 | japanese,Vegetable tempura,3.95 171 | japanese,Vegetarian sushi plate,6.95 172 | japanese,Wakame salad,4.95 173 | mexican,Beans and rice,7.95 174 | mexican,Beef burrito,6.95 175 | mexican,Birria,7.95 176 | mexican,Chicken burrito,11.45 177 | mexican,Chicken mole platter,5.95 178 | mexican,Chile relleno (meat),6.95 179 | mexican,Chile relleno (vegetarian),3.95 180 | mexican,Chips and guacamole,5.95 181 | mexican,Enchiladas,4.55 182 | mexican,Flan,7.95 183 | mexican,Jamaica Aqua Fresca,2.5 184 | mexican,Pork al pastor platter,5.95 185 | mexican,Sopa de albondigas,7.95 186 | mexican,Sopa de pollo,6.95 187 | mexican,Strawberry Aqua Fresca,3.95 188 | mexican,Super nachos with carne asada,5.95 189 | mexican,Tacos de la casa (3),4.95 190 | mexican,Vegetarian platter,4.55 191 | pizza,Cesar salad,5.95 192 | pizza,Cheesecake,6.95 193 | pizza,Chicago-style deep dish chicken and spinach,6.95 194 | pizza,Chicago-style deep dish pepperoni and cheese,7.95 195 | pizza,Chicago-style deep dish vegetarian,6.95 196 | pizza,Chicago-style meat lover's,8.95 197 | pizza,Chocolate cake,3.95 198 | pizza,Coffee,7.95 199 | pizza,Garlic bread, 200 | pizza,Greek salad,5.95 201 | pizza,Pizza of the day (slice),7.55 202 | pizza,Thin crust anchovy and garlic and chili pepper,5.95 203 | pizza,"Thin crust broccoli, chicken, and mozarella",3.95 204 | pizza,Thin crust margherita,4.55 205 | pizza,Thin crust pepperoni,6.95 206 | pizza,Thin crust quattro stagione,4.95 207 | pizza,Thin crust sausage and guanciale bacon,4.95 208 | thai,Basil duck with rice,4.55 209 | thai,Curry salmon,12.45 210 | thai,Egg rolls (4),5.95 211 | thai,Fried banana and ice cream,11.45 212 | thai,Green curry with chicken,3.95 213 | thai,Green curry with pork,4.55 214 | thai,Hot tea,2.5 215 | thai,Onion pancake,6.95 216 | thai,Pad See Ew,4.95 217 | thai,Pad Thai,6.95 218 | thai,Pumpkin curry,6.95 219 | thai,Red curry with chicken,8.95 220 | thai,Red curry with pork,9.95 221 | thai,Sticky rice with mango,6.95 222 | thai,Thai iced coffee,6.95 223 | thai,Thai iced tea,3.95 224 | thai,Tofu salad rolls,10.45 225 | vegetarian,bean and cheese burrito,6.95 226 | vegetarian,cheese tortellini in tomato sauce,3.95 227 | vegetarian,coffee,6.95 228 | vegetarian,falafel wrap with tabbouleh,4.95 229 | vegetarian,flourless chocolate cake,8.95 230 | vegetarian,garden fresh salad,6.5 231 | vegetarian,happy buddha stir fry,10.45 232 | vegetarian,hummus appetizer plate,8 233 | vegetarian,lentil burger,6 234 | vegetarian,lentil soup,4.5 235 | vegetarian,pasta with olives and marinated lemon,6.95 236 | vegetarian,spinach and cheese wrap,5.95 237 | vegetarian,tea,5.95 238 | vegetarian,toasted sandwich with grilled eggplant,8.95 239 | vegetarian,tofu chicken wrap,9.95 240 | vegetarian,tomato and cheese sandwich,11.45 241 | vegetarian,vegetable stew,5.95 -------------------------------------------------------------------------------- /app/lib/angular/angular-loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.2 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | 7 | ( 8 | 9 | /** 10 | * @ngdoc interface 11 | * @name angular.Module 12 | * @description 13 | * 14 | * Interface for configuring angular {@link angular.module modules}. 15 | */ 16 | 17 | function setupModuleLoader(window) { 18 | 19 | function ensure(obj, name, factory) { 20 | return obj[name] || (obj[name] = factory()); 21 | } 22 | 23 | return ensure(ensure(window, 'angular', Object), 'module', function() { 24 | /** @type {Object.} */ 25 | var modules = {}; 26 | 27 | /** 28 | * @ngdoc function 29 | * @name angular.module 30 | * @description 31 | * 32 | * The `angular.module` is a global place for creating and registering Angular modules. All 33 | * modules (angular core or 3rd party) that should be available to an application must be 34 | * registered using this mechanism. 35 | * 36 | * 37 | * # Module 38 | * 39 | * A module is a collocation of services, directives, filters, and configure information. Module 40 | * is used to configure the {@link AUTO.$injector $injector}. 41 | * 42 | *
 43 |      * // Create a new module
 44 |      * var myModule = angular.module('myModule', []);
 45 |      *
 46 |      * // register a new service
 47 |      * myModule.value('appName', 'MyCoolApp');
 48 |      *
 49 |      * // configure existing services inside initialization blocks.
 50 |      * myModule.config(function($locationProvider) {
 51 | 'use strict';
 52 |      *   // Configure existing providers
 53 |      *   $locationProvider.hashPrefix('!');
 54 |      * });
 55 |      * 
56 | * 57 | * Then you can create an injector and load your modules like this: 58 | * 59 | *
 60 |      * var injector = angular.injector(['ng', 'MyModule'])
 61 |      * 
62 | * 63 | * However it's more likely that you'll just use 64 | * {@link ng.directive:ngApp ngApp} or 65 | * {@link angular.bootstrap} to simplify this process for you. 66 | * 67 | * @param {!string} name The name of the module to create or retrieve. 68 | * @param {Array.=} requires If specified then new module is being created. If unspecified then the 69 | * the module is being retrieved for further configuration. 70 | * @param {Function} configFn Option configuration function for the module. Same as 71 | * {@link angular.Module#config Module#config()}. 72 | * @returns {module} new module with the {@link angular.Module} api. 73 | */ 74 | return function module(name, requires, configFn) { 75 | if (requires && modules.hasOwnProperty(name)) { 76 | modules[name] = null; 77 | } 78 | return ensure(modules, name, function() { 79 | if (!requires) { 80 | throw Error('No module: ' + name); 81 | } 82 | 83 | /** @type {!Array.>} */ 84 | var invokeQueue = []; 85 | 86 | /** @type {!Array.} */ 87 | var runBlocks = []; 88 | 89 | var config = invokeLater('$injector', 'invoke'); 90 | 91 | /** @type {angular.Module} */ 92 | var moduleInstance = { 93 | // Private state 94 | _invokeQueue: invokeQueue, 95 | _runBlocks: runBlocks, 96 | 97 | /** 98 | * @ngdoc property 99 | * @name angular.Module#requires 100 | * @propertyOf angular.Module 101 | * @returns {Array.} List of module names which must be loaded before this module. 102 | * @description 103 | * Holds the list of modules which the injector will load before the current module is loaded. 104 | */ 105 | requires: requires, 106 | 107 | /** 108 | * @ngdoc property 109 | * @name angular.Module#name 110 | * @propertyOf angular.Module 111 | * @returns {string} Name of the module. 112 | * @description 113 | */ 114 | name: name, 115 | 116 | 117 | /** 118 | * @ngdoc method 119 | * @name angular.Module#provider 120 | * @methodOf angular.Module 121 | * @param {string} name service name 122 | * @param {Function} providerType Construction function for creating new instance of the service. 123 | * @description 124 | * See {@link AUTO.$provide#provider $provide.provider()}. 125 | */ 126 | provider: invokeLater('$provide', 'provider'), 127 | 128 | /** 129 | * @ngdoc method 130 | * @name angular.Module#factory 131 | * @methodOf angular.Module 132 | * @param {string} name service name 133 | * @param {Function} providerFunction Function for creating new instance of the service. 134 | * @description 135 | * See {@link AUTO.$provide#factory $provide.factory()}. 136 | */ 137 | factory: invokeLater('$provide', 'factory'), 138 | 139 | /** 140 | * @ngdoc method 141 | * @name angular.Module#service 142 | * @methodOf angular.Module 143 | * @param {string} name service name 144 | * @param {Function} constructor A constructor function that will be instantiated. 145 | * @description 146 | * See {@link AUTO.$provide#service $provide.service()}. 147 | */ 148 | service: invokeLater('$provide', 'service'), 149 | 150 | /** 151 | * @ngdoc method 152 | * @name angular.Module#value 153 | * @methodOf angular.Module 154 | * @param {string} name service name 155 | * @param {*} object Service instance object. 156 | * @description 157 | * See {@link AUTO.$provide#value $provide.value()}. 158 | */ 159 | value: invokeLater('$provide', 'value'), 160 | 161 | /** 162 | * @ngdoc method 163 | * @name angular.Module#constant 164 | * @methodOf angular.Module 165 | * @param {string} name constant name 166 | * @param {*} object Constant value. 167 | * @description 168 | * Because the constant are fixed, they get applied before other provide methods. 169 | * See {@link AUTO.$provide#constant $provide.constant()}. 170 | */ 171 | constant: invokeLater('$provide', 'constant', 'unshift'), 172 | 173 | /** 174 | * @ngdoc method 175 | * @name angular.Module#filter 176 | * @methodOf angular.Module 177 | * @param {string} name Filter name. 178 | * @param {Function} filterFactory Factory function for creating new instance of filter. 179 | * @description 180 | * See {@link ng.$filterProvider#register $filterProvider.register()}. 181 | */ 182 | filter: invokeLater('$filterProvider', 'register'), 183 | 184 | /** 185 | * @ngdoc method 186 | * @name angular.Module#controller 187 | * @methodOf angular.Module 188 | * @param {string} name Controller name. 189 | * @param {Function} constructor Controller constructor function. 190 | * @description 191 | * See {@link ng.$controllerProvider#register $controllerProvider.register()}. 192 | */ 193 | controller: invokeLater('$controllerProvider', 'register'), 194 | 195 | /** 196 | * @ngdoc method 197 | * @name angular.Module#directive 198 | * @methodOf angular.Module 199 | * @param {string} name directive name 200 | * @param {Function} directiveFactory Factory function for creating new instance of 201 | * directives. 202 | * @description 203 | * See {@link ng.$compileProvider#directive $compileProvider.directive()}. 204 | */ 205 | directive: invokeLater('$compileProvider', 'directive'), 206 | 207 | /** 208 | * @ngdoc method 209 | * @name angular.Module#config 210 | * @methodOf angular.Module 211 | * @param {Function} configFn Execute this function on module load. Useful for service 212 | * configuration. 213 | * @description 214 | * Use this method to register work which needs to be performed on module loading. 215 | */ 216 | config: config, 217 | 218 | /** 219 | * @ngdoc method 220 | * @name angular.Module#run 221 | * @methodOf angular.Module 222 | * @param {Function} initializationFn Execute this function after injector creation. 223 | * Useful for application initialization. 224 | * @description 225 | * Use this method to register work which needs to be performed when the injector with 226 | * with the current module is finished loading. 227 | */ 228 | run: function(block) { 229 | runBlocks.push(block); 230 | return this; 231 | } 232 | }; 233 | 234 | if (configFn) { 235 | config(configFn); 236 | } 237 | 238 | return moduleInstance; 239 | 240 | /** 241 | * @param {string} provider 242 | * @param {string} method 243 | * @param {String=} insertMethod 244 | * @returns {angular.Module} 245 | */ 246 | function invokeLater(provider, method, insertMethod) { 247 | return function() { 248 | invokeQueue[insertMethod || 'push']([provider, method, arguments]); 249 | return moduleInstance; 250 | } 251 | } 252 | }); 253 | }; 254 | }); 255 | 256 | } 257 | )(window); 258 | 259 | /** 260 | * Closure compiler type information 261 | * 262 | * @typedef { { 263 | * requires: !Array., 264 | * invokeQueue: !Array.>, 265 | * 266 | * service: function(string, Function):angular.Module, 267 | * factory: function(string, Function):angular.Module, 268 | * value: function(string, *):angular.Module, 269 | * 270 | * filter: function(string, Function):angular.Module, 271 | * 272 | * init: function(Function):angular.Module 273 | * } } 274 | */ 275 | angular.Module; 276 | 277 | -------------------------------------------------------------------------------- /app/lib/angular/angular-resource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.2 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) { 7 | 'use strict'; 8 | 9 | /** 10 | * @ngdoc overview 11 | * @name ngResource 12 | * @description 13 | */ 14 | 15 | /** 16 | * @ngdoc object 17 | * @name ngResource.$resource 18 | * @requires $http 19 | * 20 | * @description 21 | * A factory which creates a resource object that lets you interact with 22 | * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. 23 | * 24 | * The returned resource object has action methods which provide high-level behaviors without 25 | * the need to interact with the low level {@link ng.$http $http} service. 26 | * 27 | * @param {string} url A parameterized URL template with parameters prefixed by `:` as in 28 | * `/user/:username`. 29 | * 30 | * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in 31 | * `actions` methods. 32 | * 33 | * Each key value in the parameter object is first bound to url template if present and then any 34 | * excess keys are appended to the url search query after the `?`. 35 | * 36 | * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in 37 | * URL `/path/greet?salutation=Hello`. 38 | * 39 | * If the parameter value is prefixed with `@` then the value of that parameter is extracted from 40 | * the data object (useful for non-GET operations). 41 | * 42 | * @param {Object.=} actions Hash with declaration of custom action that should extend the 43 | * default set of resource actions. The declaration should be created in the following format: 44 | * 45 | * {action1: {method:?, params:?, isArray:?}, 46 | * action2: {method:?, params:?, isArray:?}, 47 | * ...} 48 | * 49 | * Where: 50 | * 51 | * - `action` – {string} – The name of action. This name becomes the name of the method on your 52 | * resource object. 53 | * - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, 54 | * and `JSONP` 55 | * - `params` – {object=} – Optional set of pre-bound parameters for this action. 56 | * - isArray – {boolean=} – If true then the returned object for this action is an array, see 57 | * `returns` section. 58 | * 59 | * @returns {Object} A resource "class" object with methods for the default set of resource actions 60 | * optionally extended with custom `actions`. The default set contains these actions: 61 | * 62 | * { 'get': {method:'GET'}, 63 | * 'save': {method:'POST'}, 64 | * 'query': {method:'GET', isArray:true}, 65 | * 'remove': {method:'DELETE'}, 66 | * 'delete': {method:'DELETE'} }; 67 | * 68 | * Calling these methods invoke an {@link ng.$http} with the specified http method, 69 | * destination and parameters. When the data is returned from the server then the object is an 70 | * instance of the resource class `save`, `remove` and `delete` actions are available on it as 71 | * methods with the `$` prefix. This allows you to easily perform CRUD operations (create, read, 72 | * update, delete) on server-side data like this: 73 | *
 74 |         var User = $resource('/user/:userId', {userId:'@id'});
 75 |         var user = User.get({userId:123}, function() {
 76 |           user.abc = true;
 77 |           user.$save();
 78 |         });
 79 |      
80 | * 81 | * It is important to realize that invoking a $resource object method immediately returns an 82 | * empty reference (object or array depending on `isArray`). Once the data is returned from the 83 | * server the existing reference is populated with the actual data. This is a useful trick since 84 | * usually the resource is assigned to a model which is then rendered by the view. Having an empty 85 | * object results in no rendering, once the data arrives from the server then the object is 86 | * populated with the data and the view automatically re-renders itself showing the new data. This 87 | * means that in most case one never has to write a callback function for the action methods. 88 | * 89 | * The action methods on the class object or instance object can be invoked with the following 90 | * parameters: 91 | * 92 | * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` 93 | * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` 94 | * - non-GET instance actions: `instance.$action([parameters], [success], [error])` 95 | * 96 | * 97 | * @example 98 | * 99 | * # Credit card resource 100 | * 101 | *
102 |      // Define CreditCard class
103 |      var CreditCard = $resource('/user/:userId/card/:cardId',
104 |       {userId:123, cardId:'@id'}, {
105 |        charge: {method:'POST', params:{charge:true}}
106 |       });
107 | 
108 |      // We can retrieve a collection from the server
109 |      var cards = CreditCard.query(function() {
110 |        // GET: /user/123/card
111 |        // server returns: [ {id:456, number:'1234', name:'Smith'} ];
112 | 
113 |        var card = cards[0];
114 |        // each item is an instance of CreditCard
115 |        expect(card instanceof CreditCard).toEqual(true);
116 |        card.name = "J. Smith";
117 |        // non GET methods are mapped onto the instances
118 |        card.$save();
119 |        // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
120 |        // server returns: {id:456, number:'1234', name: 'J. Smith'};
121 | 
122 |        // our custom method is mapped as well.
123 |        card.$charge({amount:9.99});
124 |        // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
125 |      });
126 | 
127 |      // we can create an instance as well
128 |      var newCard = new CreditCard({number:'0123'});
129 |      newCard.name = "Mike Smith";
130 |      newCard.$save();
131 |      // POST: /user/123/card {number:'0123', name:'Mike Smith'}
132 |      // server returns: {id:789, number:'01234', name: 'Mike Smith'};
133 |      expect(newCard.id).toEqual(789);
134 |  * 
135 | * 136 | * The object returned from this function execution is a resource "class" which has "static" method 137 | * for each action in the definition. 138 | * 139 | * Calling these methods invoke `$http` on the `url` template with the given `method` and `params`. 140 | * When the data is returned from the server then the object is an instance of the resource type and 141 | * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD 142 | * operations (create, read, update, delete) on server-side data. 143 | 144 |
145 |      var User = $resource('/user/:userId', {userId:'@id'});
146 |      var user = User.get({userId:123}, function() {
147 |        user.abc = true;
148 |        user.$save();
149 |      });
150 |    
151 | * 152 | * It's worth noting that the success callback for `get`, `query` and other method gets passed 153 | * in the response that came from the server as well as $http header getter function, so one 154 | * could rewrite the above example and get access to http headers as: 155 | * 156 |
157 |      var User = $resource('/user/:userId', {userId:'@id'});
158 |      User.get({userId:123}, function(u, getResponseHeaders){
159 |        u.abc = true;
160 |        u.$save(function(u, putResponseHeaders) {
161 |          //u => saved user object
162 |          //putResponseHeaders => $http header getter
163 |        });
164 |      });
165 |    
166 | 167 | * # Buzz client 168 | 169 | Let's look at what a buzz client created with the `$resource` service looks like: 170 | 171 | 172 | 192 | 193 |
194 | 195 | 196 |
197 |
198 |

199 | 200 | {{item.actor.name}} 201 | Expand replies: {{item.links.replies[0].count}} 202 |

203 | {{item.object.content | html}} 204 |
205 | 206 | {{reply.actor.name}}: {{reply.content | html}} 207 |
208 |
209 |
210 |
211 | 212 | 213 |
214 | */ 215 | angular.module('ngResource', ['ng']). 216 | factory('$resource', ['$http', '$parse', function($http, $parse) { 217 | var DEFAULT_ACTIONS = { 218 | 'get': {method:'GET'}, 219 | 'save': {method:'POST'}, 220 | 'query': {method:'GET', isArray:true}, 221 | 'remove': {method:'DELETE'}, 222 | 'delete': {method:'DELETE'} 223 | }; 224 | var noop = angular.noop, 225 | forEach = angular.forEach, 226 | extend = angular.extend, 227 | copy = angular.copy, 228 | isFunction = angular.isFunction, 229 | getter = function(obj, path) { 230 | return $parse(path)(obj); 231 | }; 232 | 233 | /** 234 | * We need our custom mehtod because encodeURIComponent is too agressive and doesn't follow 235 | * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path 236 | * segments: 237 | * segment = *pchar 238 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 239 | * pct-encoded = "%" HEXDIG HEXDIG 240 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 241 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 242 | * / "*" / "+" / "," / ";" / "=" 243 | */ 244 | function encodeUriSegment(val) { 245 | return encodeUriQuery(val, true). 246 | replace(/%26/gi, '&'). 247 | replace(/%3D/gi, '='). 248 | replace(/%2B/gi, '+'); 249 | } 250 | 251 | 252 | /** 253 | * This method is intended for encoding *key* or *value* parts of query component. We need a custom 254 | * method becuase encodeURIComponent is too agressive and encodes stuff that doesn't have to be 255 | * encoded per http://tools.ietf.org/html/rfc3986: 256 | * query = *( pchar / "/" / "?" ) 257 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 258 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 259 | * pct-encoded = "%" HEXDIG HEXDIG 260 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 261 | * / "*" / "+" / "," / ";" / "=" 262 | */ 263 | function encodeUriQuery(val, pctEncodeSpaces) { 264 | return encodeURIComponent(val). 265 | replace(/%40/gi, '@'). 266 | replace(/%3A/gi, ':'). 267 | replace(/%24/g, '$'). 268 | replace(/%2C/gi, ','). 269 | replace((pctEncodeSpaces ? null : /%20/g), '+'); 270 | } 271 | 272 | function Route(template, defaults) { 273 | this.template = template = template + '#'; 274 | this.defaults = defaults || {}; 275 | var urlParams = this.urlParams = {}; 276 | forEach(template.split(/\W/), function(param){ 277 | if (param && template.match(new RegExp("[^\\\\]:" + param + "\\W"))) { 278 | urlParams[param] = true; 279 | } 280 | }); 281 | this.template = template.replace(/\\:/g, ':'); 282 | } 283 | 284 | Route.prototype = { 285 | url: function(params) { 286 | var self = this, 287 | url = this.template, 288 | encodedVal; 289 | 290 | params = params || {}; 291 | forEach(this.urlParams, function(_, urlParam){ 292 | encodedVal = encodeUriSegment(params[urlParam] || self.defaults[urlParam] || ""); 293 | url = url.replace(new RegExp(":" + urlParam + "(\\W)"), encodedVal + "$1"); 294 | }); 295 | url = url.replace(/\/?#$/, ''); 296 | var query = []; 297 | forEach(params, function(value, key){ 298 | if (!self.urlParams[key]) { 299 | query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value)); 300 | } 301 | }); 302 | query.sort(); 303 | url = url.replace(/\/*$/, ''); 304 | return url + (query.length ? '?' + query.join('&') : ''); 305 | } 306 | }; 307 | 308 | 309 | function ResourceFactory(url, paramDefaults, actions) { 310 | var route = new Route(url); 311 | 312 | actions = extend({}, DEFAULT_ACTIONS, actions); 313 | 314 | function extractParams(data){ 315 | var ids = {}; 316 | forEach(paramDefaults || {}, function(value, key){ 317 | ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; 318 | }); 319 | return ids; 320 | } 321 | 322 | function Resource(value){ 323 | copy(value || {}, this); 324 | } 325 | 326 | forEach(actions, function(action, name) { 327 | var hasBody = action.method == 'POST' || action.method == 'PUT' || action.method == 'PATCH'; 328 | Resource[name] = function(a1, a2, a3, a4) { 329 | var params = {}; 330 | var data; 331 | var success = noop; 332 | var error = null; 333 | switch(arguments.length) { 334 | case 4: 335 | error = a4; 336 | success = a3; 337 | //fallthrough 338 | case 3: 339 | case 2: 340 | if (isFunction(a2)) { 341 | if (isFunction(a1)) { 342 | success = a1; 343 | error = a2; 344 | break; 345 | } 346 | 347 | success = a2; 348 | error = a3; 349 | //fallthrough 350 | } else { 351 | params = a1; 352 | data = a2; 353 | success = a3; 354 | break; 355 | } 356 | case 1: 357 | if (isFunction(a1)) success = a1; 358 | else if (hasBody) data = a1; 359 | else params = a1; 360 | break; 361 | case 0: break; 362 | default: 363 | throw "Expected between 0-4 arguments [params, data, success, error], got " + 364 | arguments.length + " arguments."; 365 | } 366 | 367 | var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data)); 368 | $http({ 369 | method: action.method, 370 | url: route.url(extend({}, extractParams(data), action.params || {}, params)), 371 | data: data 372 | }).then(function(response) { 373 | var data = response.data; 374 | 375 | if (data) { 376 | if (action.isArray) { 377 | value.length = 0; 378 | forEach(data, function(item) { 379 | value.push(new Resource(item)); 380 | }); 381 | } else { 382 | copy(data, value); 383 | } 384 | } 385 | (success||noop)(value, response.headers); 386 | }, error); 387 | 388 | return value; 389 | }; 390 | 391 | 392 | Resource.bind = function(additionalParamDefaults){ 393 | return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); 394 | }; 395 | 396 | 397 | Resource.prototype['$' + name] = function(a1, a2, a3) { 398 | var params = extractParams(this), 399 | success = noop, 400 | error; 401 | 402 | switch(arguments.length) { 403 | case 3: params = a1; success = a2; error = a3; break; 404 | case 2: 405 | case 1: 406 | if (isFunction(a1)) { 407 | success = a1; 408 | error = a2; 409 | } else { 410 | params = a1; 411 | success = a2 || noop; 412 | } 413 | case 0: break; 414 | default: 415 | throw "Expected between 1-3 arguments [params, success, error], got " + 416 | arguments.length + " arguments."; 417 | } 418 | var data = hasBody ? this : undefined; 419 | Resource[name].call(this, params, data, success, error); 420 | }; 421 | }); 422 | return Resource; 423 | } 424 | 425 | return ResourceFactory; 426 | }]); 427 | 428 | })(window, window.angular); 429 | -------------------------------------------------------------------------------- /app/lib/angular/angular-sanitize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.2 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) { 7 | 'use strict'; 8 | 9 | /** 10 | * @ngdoc overview 11 | * @name ngSanitize 12 | * @description 13 | */ 14 | 15 | /* 16 | * HTML Parser By Misko Hevery (misko@hevery.com) 17 | * based on: HTML Parser By John Resig (ejohn.org) 18 | * Original code by Erik Arvidsson, Mozilla Public License 19 | * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js 20 | * 21 | * // Use like so: 22 | * htmlParser(htmlString, { 23 | * start: function(tag, attrs, unary) {}, 24 | * end: function(tag) {}, 25 | * chars: function(text) {}, 26 | * comment: function(text) {} 27 | * }); 28 | * 29 | */ 30 | 31 | 32 | /** 33 | * @ngdoc service 34 | * @name ngSanitize.$sanitize 35 | * @function 36 | * 37 | * @description 38 | * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are 39 | * then serialized back to properly escaped html string. This means that no unsafe input can make 40 | * it into the returned string, however, since our parser is more strict than a typical browser 41 | * parser, it's possible that some obscure input, which would be recognized as valid HTML by a 42 | * browser, won't make it through the sanitizer. 43 | * 44 | * @param {string} html Html input. 45 | * @returns {string} Sanitized html. 46 | * 47 | * @example 48 | 49 | 50 | 58 |
59 | Snippet: 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 71 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
FilterSourceRendered
html filter 69 |
<div ng-bind-html="snippet">
</div>
70 |
72 |
73 |
no filter
<div ng-bind="snippet">
</div>
unsafe html filter
<div ng-bind-html-unsafe="snippet">
</div>
86 |
87 |
88 | 89 | it('should sanitize the html snippet ', function() { 90 | expect(using('#html-filter').element('div').html()). 91 | toBe('

an html\nclick here\nsnippet

'); 92 | }); 93 | 94 | it('should escape snippet without any filter', function() { 95 | expect(using('#escaped-html').element('div').html()). 96 | toBe("<p style=\"color:blue\">an html\n" + 97 | "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + 98 | "snippet</p>"); 99 | }); 100 | 101 | it('should inline raw snippet if filtered as unsafe', function() { 102 | expect(using('#html-unsafe-filter').element("div").html()). 103 | toBe("

an html\n" + 104 | "click here\n" + 105 | "snippet

"); 106 | }); 107 | 108 | it('should update', function() { 109 | input('snippet').enter('new text'); 110 | expect(using('#html-filter').binding('snippet')).toBe('new text'); 111 | expect(using('#escaped-html').element('div').html()).toBe("new <b>text</b>"); 112 | expect(using('#html-unsafe-filter').binding("snippet")).toBe('new text'); 113 | }); 114 |
115 |
116 | */ 117 | var $sanitize = function(html) { 118 | var buf = []; 119 | htmlParser(html, htmlSanitizeWriter(buf)); 120 | return buf.join(''); 121 | }; 122 | 123 | 124 | // Regular Expressions for parsing tags and attributes 125 | var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, 126 | END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, 127 | ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, 128 | BEGIN_TAG_REGEXP = /^/g, 131 | CDATA_REGEXP = //g, 132 | URI_REGEXP = /^((ftp|https?):\/\/|mailto:|#)/, 133 | NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character) 134 | 135 | 136 | // Good source of info about elements and attributes 137 | // http://dev.w3.org/html5/spec/Overview.html#semantics 138 | // http://simon.html5.org/html-elements 139 | 140 | // Safe Void Elements - HTML5 141 | // http://dev.w3.org/html5/spec/Overview.html#void-elements 142 | var voidElements = makeMap("area,br,col,hr,img,wbr"); 143 | 144 | // Elements that you can, intentionally, leave open (and which close themselves) 145 | // http://dev.w3.org/html5/spec/Overview.html#optional-tags 146 | var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), 147 | optionalEndTagInlineElements = makeMap("rp,rt"), 148 | optionalEndTagElements = angular.extend({}, optionalEndTagInlineElements, optionalEndTagBlockElements); 149 | 150 | // Safe Block Elements - HTML5 151 | var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article,aside," + 152 | "blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6," + 153 | "header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); 154 | 155 | // Inline Elements - HTML5 156 | var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b,bdi,bdo," + 157 | "big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small," + 158 | "span,strike,strong,sub,sup,time,tt,u,var")); 159 | 160 | 161 | // Special Elements (can contain anything) 162 | var specialElements = makeMap("script,style"); 163 | 164 | var validElements = angular.extend({}, voidElements, blockElements, inlineElements, optionalEndTagElements); 165 | 166 | //Attributes that have href and hence need to be sanitized 167 | var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); 168 | var validAttrs = angular.extend({}, uriAttrs, makeMap( 169 | 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ 170 | 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ 171 | 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ 172 | 'scope,scrolling,shape,span,start,summary,target,title,type,'+ 173 | 'valign,value,vspace,width')); 174 | 175 | function makeMap(str) { 176 | var obj = {}, items = str.split(','), i; 177 | for (i = 0; i < items.length; i++) obj[items[i]] = true; 178 | return obj; 179 | } 180 | 181 | 182 | /** 183 | * @example 184 | * htmlParser(htmlString, { 185 | * start: function(tag, attrs, unary) {}, 186 | * end: function(tag) {}, 187 | * chars: function(text) {}, 188 | * comment: function(text) {} 189 | * }); 190 | * 191 | * @param {string} html string 192 | * @param {object} handler 193 | */ 194 | function htmlParser( html, handler ) { 195 | var index, chars, match, stack = [], last = html; 196 | stack.last = function() { return stack[ stack.length - 1 ]; }; 197 | 198 | while ( html ) { 199 | chars = true; 200 | 201 | // Make sure we're not in a script or style element 202 | if ( !stack.last() || !specialElements[ stack.last() ] ) { 203 | 204 | // Comment 205 | if ( html.indexOf(""); 207 | 208 | if ( index >= 0 ) { 209 | if (handler.comment) handler.comment( html.substring( 4, index ) ); 210 | html = html.substring( index + 3 ); 211 | chars = false; 212 | } 213 | 214 | // end tag 215 | } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { 216 | match = html.match( END_TAG_REGEXP ); 217 | 218 | if ( match ) { 219 | html = html.substring( match[0].length ); 220 | match[0].replace( END_TAG_REGEXP, parseEndTag ); 221 | chars = false; 222 | } 223 | 224 | // start tag 225 | } else if ( BEGIN_TAG_REGEXP.test(html) ) { 226 | match = html.match( START_TAG_REGEXP ); 227 | 228 | if ( match ) { 229 | html = html.substring( match[0].length ); 230 | match[0].replace( START_TAG_REGEXP, parseStartTag ); 231 | chars = false; 232 | } 233 | } 234 | 235 | if ( chars ) { 236 | index = html.indexOf("<"); 237 | 238 | var text = index < 0 ? html : html.substring( 0, index ); 239 | html = index < 0 ? "" : html.substring( index ); 240 | 241 | if (handler.chars) handler.chars( decodeEntities(text) ); 242 | } 243 | 244 | } else { 245 | html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){ 246 | text = text. 247 | replace(COMMENT_REGEXP, "$1"). 248 | replace(CDATA_REGEXP, "$1"); 249 | 250 | if (handler.chars) handler.chars( decodeEntities(text) ); 251 | 252 | return ""; 253 | }); 254 | 255 | parseEndTag( "", stack.last() ); 256 | } 257 | 258 | if ( html == last ) { 259 | throw "Parse Error: " + html; 260 | } 261 | last = html; 262 | } 263 | 264 | // Clean up any remaining tags 265 | parseEndTag(); 266 | 267 | function parseStartTag( tag, tagName, rest, unary ) { 268 | tagName = angular.lowercase(tagName); 269 | if ( blockElements[ tagName ] ) { 270 | while ( stack.last() && inlineElements[ stack.last() ] ) { 271 | parseEndTag( "", stack.last() ); 272 | } 273 | } 274 | 275 | if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) { 276 | parseEndTag( "", tagName ); 277 | } 278 | 279 | unary = voidElements[ tagName ] || !!unary; 280 | 281 | if ( !unary ) 282 | stack.push( tagName ); 283 | 284 | var attrs = {}; 285 | 286 | rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQoutedValue, unqoutedValue) { 287 | var value = doubleQuotedValue 288 | || singleQoutedValue 289 | || unqoutedValue 290 | || ''; 291 | 292 | attrs[name] = decodeEntities(value); 293 | }); 294 | if (handler.start) handler.start( tagName, attrs, unary ); 295 | } 296 | 297 | function parseEndTag( tag, tagName ) { 298 | var pos = 0, i; 299 | tagName = angular.lowercase(tagName); 300 | if ( tagName ) 301 | // Find the closest opened tag of the same type 302 | for ( pos = stack.length - 1; pos >= 0; pos-- ) 303 | if ( stack[ pos ] == tagName ) 304 | break; 305 | 306 | if ( pos >= 0 ) { 307 | // Close all the open elements, up the stack 308 | for ( i = stack.length - 1; i >= pos; i-- ) 309 | if (handler.end) handler.end( stack[ i ] ); 310 | 311 | // Remove the open elements from the stack 312 | stack.length = pos; 313 | } 314 | } 315 | } 316 | 317 | /** 318 | * decodes all entities into regular string 319 | * @param value 320 | * @returns {string} A string with decoded entities. 321 | */ 322 | var hiddenPre=document.createElement("pre"); 323 | function decodeEntities(value) { 324 | hiddenPre.innerHTML=value.replace(//g, '>'); 343 | } 344 | 345 | /** 346 | * create an HTML/XML writer which writes to buffer 347 | * @param {Array} buf use buf.jain('') to get out sanitized html string 348 | * @returns {object} in the form of { 349 | * start: function(tag, attrs, unary) {}, 350 | * end: function(tag) {}, 351 | * chars: function(text) {}, 352 | * comment: function(text) {} 353 | * } 354 | */ 355 | function htmlSanitizeWriter(buf){ 356 | var ignore = false; 357 | var out = angular.bind(buf, buf.push); 358 | return { 359 | start: function(tag, attrs, unary){ 360 | tag = angular.lowercase(tag); 361 | if (!ignore && specialElements[tag]) { 362 | ignore = tag; 363 | } 364 | if (!ignore && validElements[tag] == true) { 365 | out('<'); 366 | out(tag); 367 | angular.forEach(attrs, function(value, key){ 368 | var lkey=angular.lowercase(key); 369 | if (validAttrs[lkey]==true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) { 370 | out(' '); 371 | out(key); 372 | out('="'); 373 | out(encodeEntities(value)); 374 | out('"'); 375 | } 376 | }); 377 | out(unary ? '/>' : '>'); 378 | } 379 | }, 380 | end: function(tag){ 381 | tag = angular.lowercase(tag); 382 | if (!ignore && validElements[tag] == true) { 383 | out(''); 386 | } 387 | if (tag == ignore) { 388 | ignore = false; 389 | } 390 | }, 391 | chars: function(chars){ 392 | if (!ignore) { 393 | out(encodeEntities(chars)); 394 | } 395 | } 396 | }; 397 | } 398 | 399 | 400 | // define ngSanitize module and register $sanitize service 401 | angular.module('ngSanitize', []).value('$sanitize', $sanitize); 402 | 403 | /** 404 | * @ngdoc directive 405 | * @name ngSanitize.directive:ngBindHtml 406 | * 407 | * @description 408 | * Creates a binding that will sanitize the result of evaluating the `expression` with the 409 | * {@link ngSanitize.$sanitize $sanitize} service and innerHTML the result into the current element. 410 | * 411 | * See {@link ngSanitize.$sanitize $sanitize} docs for examples. 412 | * 413 | * @element ANY 414 | * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. 415 | */ 416 | angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($sanitize) { 417 | return function(scope, element, attr) { 418 | element.addClass('ng-binding').data('$binding', attr.ngBindHtml); 419 | scope.$watch(attr.ngBindHtml, function(value) { 420 | value = $sanitize(value); 421 | element.html(value || ''); 422 | }); 423 | }; 424 | }]); 425 | /** 426 | * @ngdoc filter 427 | * @name ngSanitize.filter:linky 428 | * @function 429 | * 430 | * @description 431 | * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and 432 | * plain email address links. 433 | * 434 | * @param {string} text Input text. 435 | * @returns {string} Html-linkified text. 436 | * 437 | * @usage 438 | 439 | * 440 | * @example 441 | 442 | 443 | 453 |
454 | Snippet: 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 466 | 469 | 470 | 471 | 472 | 473 | 474 | 475 |
FilterSourceRendered
linky filter 464 |
<div ng-bind-html="snippet | linky">
</div>
465 |
467 |
468 |
no filter
<div ng-bind="snippet">
</div>
476 | 477 | 478 | it('should linkify the snippet with urls', function() { 479 | expect(using('#linky-filter').binding('snippet | linky')). 480 | toBe('Pretty text with some links: ' + 481 | 'http://angularjs.org/, ' + 482 | 'us@somewhere.org, ' + 483 | 'another@somewhere.org, ' + 484 | 'and one more: ftp://127.0.0.1/.'); 485 | }); 486 | 487 | it ('should not linkify snippet without the linky filter', function() { 488 | expect(using('#escaped-html').binding('snippet')). 489 | toBe("Pretty text with some links:\n" + 490 | "http://angularjs.org/,\n" + 491 | "mailto:us@somewhere.org,\n" + 492 | "another@somewhere.org,\n" + 493 | "and one more: ftp://127.0.0.1/."); 494 | }); 495 | 496 | it('should update', function() { 497 | input('snippet').enter('new http://link.'); 498 | expect(using('#linky-filter').binding('snippet | linky')). 499 | toBe('new http://link.'); 500 | expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); 501 | }); 502 | 503 | 504 | */ 505 | angular.module('ngSanitize').filter('linky', function() { 506 | var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, 507 | MAILTO_REGEXP = /^mailto:/; 508 | 509 | return function(text) { 510 | if (!text) return text; 511 | var match; 512 | var raw = text; 513 | var html = []; 514 | // TODO(vojta): use $sanitize instead 515 | var writer = htmlSanitizeWriter(html); 516 | var url; 517 | var i; 518 | while ((match = raw.match(LINKY_URL_REGEXP))) { 519 | // We can not end in these as they are sometimes found at the end of the sentence 520 | url = match[0]; 521 | // if we did not match ftp/http/mailto then assume mailto 522 | if (match[2] == match[3]) url = 'mailto:' + url; 523 | i = match.index; 524 | writer.chars(raw.substr(0, i)); 525 | writer.start('a', {href:url}); 526 | writer.chars(match[0].replace(MAILTO_REGEXP, '')); 527 | writer.end('a'); 528 | raw = raw.substring(i + match[0].length); 529 | } 530 | writer.chars(raw); 531 | return html.join(''); 532 | }; 533 | }); 534 | 535 | })(window, window.angular); 536 | -------------------------------------------------------------------------------- /server/data/restaurants.json: -------------------------------------------------------------------------------- 1 | [{"days":[1,2,3,4,5,6],"menuItems":[{"name":"Bockwurst Würstchen","price":4.95},{"name":"Bratwurst mit Brötchen und Sauerkraut","price":5.95},{"name":"Currywurst mit Brötchen","price":5.95},{"name":"Das Hausmannskost","price":11.45},{"name":"Fleishkas mit Kartoffelsalat","price":6.95},{"name":"Frankfurter Würstchen","price":9.95},{"name":"Französische Zwiebelsuppe mit Käse","price":10.45},{"name":"Frikadelle mit Brötchen","price":6.95},{"name":"Gebackener Camenbert","price":7.55},{"name":"Gemischter Salat","price":4.55},{"name":"Haus Salatteller","price":11.45},{"name":"Jaegerschnitzel","price":9.95},{"name":"Kaesepaetzle","price":6.95},{"name":"Kartoffel Reibekuchen mit Apfelmus","price":5.95},{"name":"Maultaschen mit Käse","price":7.95},{"name":"Sauerbraten","price":10.45},{"name":"Ungarische Gulaschsuppe mit Brötchen","price":3.95},{"name":"Wienerschnitzel","price":8.95},{"name":"Wurstsalad mit Bauernbrot","price":6.95}],"price":3,"rating":3,"id":"esthers","name":"Esther's German Saloon","cuisine":"german","opens":"11:30:00","closes":"22:30:00","location":"22 Teutonic Ave.","description":"German home-cooked meals and fifty-eight different beers on tap. To get more authentic, you'd need to be wearing lederhosen."},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"California roll","price":5.95},{"name":"Chicken teriyaki","price":5.95},{"name":"Edamame","price":6.95},{"name":"Futomaki roll","price":null},{"name":"Green tea ice cream","price":6.95},{"name":"Kitsune Udon","price":9.5},{"name":"Miso soup","price":5.95},{"name":"Pork Katsu","price":5.95},{"name":"Salmon teriyaki","price":5.95},{"name":"Sashimi combo","price":6.95},{"name":"Spicy Yellowtail roll","price":7.55},{"name":"Sushi combo","price":4.55},{"name":"Teppa Maki","price":5.95},{"name":"Unagi Don","price":7.55},{"name":"Vegetable tempura","price":3.95},{"name":"Vegetarian sushi plate","price":6.95},{"name":"Wakame salad","price":4.95}],"price":4,"rating":5,"id":"robatayaki","name":"Robatayaki Hachi","cuisine":"japanese","opens":"17:30:00","closes":"23:30:00","location":"8 Hawthorne Ln.","description":"Japanese food the way you like it. Fast, fresh, grilled."},{"days":[1,3,5,6,0],"menuItems":[{"name":"bean and cheese burrito","price":6.95},{"name":"cheese tortellini in tomato sauce","price":3.95},{"name":"coffee","price":6.95},{"name":"falafel wrap with tabbouleh","price":4.95},{"name":"flourless chocolate cake","price":8.95},{"name":"garden fresh salad","price":6.5},{"name":"happy buddha stir fry","price":10.45},{"name":"hummus appetizer plate","price":8},{"name":"lentil burger","price":6},{"name":"lentil soup","price":4.5},{"name":"pasta with olives and marinated lemon","price":6.95},{"name":"spinach and cheese wrap","price":5.95},{"name":"tea","price":5.95},{"name":"toasted sandwich with grilled eggplant","price":8.95},{"name":"tofu chicken wrap","price":9.95},{"name":"tomato and cheese sandwich","price":11.45},{"name":"vegetable stew","price":5.95}],"price":2,"rating":1,"id":"tofuparadise","name":"BBQ Tofu Paradise","cuisine":"vegetarian","opens":"16:30:00","closes":"20:00:00","location":"22A King West","description":"Vegetarians, we have your BBQ needs covered. Our home-made tofu skewers and secret BBQ sauce will have you licking your fingers."},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"bavette dans son jus","price":4.95},{"name":"bœuf bourguignon ","price":18},{"name":"bouillabaisse","price":17.5},{"name":"coq au vin","price":10.45},{"name":"frites aïoli ","price":null},{"name":"moules et frites","price":3.95},{"name":"poulet au riesling","price":4.95},{"name":"quiche lorraine","price":7.95},{"name":"salade de chèvre chaud","price":5.95},{"name":"salade du midi","price":6.95},{"name":"salade niçoise","price":3.95},{"name":"sandwich croque-madame","price":3.95},{"name":"sandwich croque-monsieur","price":4.55},{"name":"soupe à l'oignon","price":9.95},{"name":"steak frites","price":5.95},{"name":"tarte pissaladière ","price":6.95},{"name":"tarte tatin","price":5.95}],"price":5,"rating":4,"id":"bateaurouge","name":"Le Bateau Rouge","cuisine":"french","opens":"17:00:00","closes":"23:30:00","location":"2 South Park Dr.","description":"Fine French dining in a romantic setting. From soupe à l'oignon to coq au vin, let our chef delight you with a local take on authentic favorites."},{"days":[1,2,3,4,5],"menuItems":[{"name":"Dibi Lamb","price":8.25},{"name":"Doro Wat","price":5.95},{"name":"Grilled Chicken","price":4.95},{"name":"Grilled Fish","price":6.95},{"name":"Grilled Plantains in Spicy Peanut Sauce","price":7.95},{"name":"Lamb Mafe (Peanut Butter Stew)","price":8.75},{"name":"Meat Pie","price":6},{"name":"Mechoui with Plantains","price":5.95},{"name":"Pepper Soup","price":9.95},{"name":"Piri-Piri Shrimp","price":11.45},{"name":"Suppa Kandja","price":3.95},{"name":"Thiou Boulette","price":6.95},{"name":"Thiou Curry with Chicken","price":7.95},{"name":"Thu Okra","price":6.95},{"name":"Yassa Chicken","price":8.95},{"name":"Yassa Lamb","price":8.25}],"price":3,"rating":2,"id":"khartoum","name":"Khartoum Khartoum","cuisine":"african","opens":"11:00:00","closes":"14:00:00","location":"1566 Maple Rd.","description":"African homestyle cuisine, cooked fresh daily."},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"Buffalo wings","price":5.95},{"name":"California-style baked Tilapia with rice","price":6.95},{"name":"Cheeseburger and fries","price":4.55},{"name":"Cherry pie a la mode","price":4.95},{"name":"Chocolate milkshake","price":6.95},{"name":"Cobb salad","price":4.95},{"name":"Famous BLT on a kaiser roll with fries","price":4.95},{"name":"Firehouse chili","price":6.95},{"name":"Goat cheese and eggplant wrap (vegetarian)","price":5.95},{"name":"Greek salad","price":6.95},{"name":"Grilled chicken sandwich","price":4.95},{"name":"Grilled sausage on a bun","price":6.95},{"name":"Housemade pot roast with seasonal vegetable","price":16.95},{"name":"Roast beef dip","price":11.45},{"name":"Roast chicken and mashed potatoes","price":7.55},{"name":"Soup of the day","price":8.95},{"name":"Spaghetti and meatballs","price":11.45}],"price":4,"rating":3,"id":"sallys","name":"Sally's Diner","cuisine":"american","opens":"8:30:00","closes":"20:00:00","location":"96 College Blvd.","description":"Food like mom cooked, if you grew up in Iowa and mom ran a diner. Try our blue plate special!"},{"days":[2,3,4,5,6,0],"menuItems":[{"name":"BBQ chicken","price":4.95},{"name":"Beef ribs (full)","price":9.95},{"name":"Beef ribs (delux)","price":10.45},{"name":"Beef ribs (half)","price":6.45},{"name":"Beer","price":7.55},{"name":"Coleslaw","price":8.95},{"name":"Collards","price":9.95},{"name":"Cornbread","price":11.45},{"name":"Devilled eggs","price":4},{"name":"German chocolate cake","price":5.95},{"name":"Housemade chips","price":4},{"name":"Hushpuppies","price":3.25},{"name":"Mac and cheese","price":6},{"name":"Pork ribs (half)","price":6.95},{"name":"Potato salad","price":3.95},{"name":"Pulled pork sandwich on a soft roll","price":4.95},{"name":"Riblets","price":10.45}],"price":3,"rating":2,"id":"saucy","name":"Saucy Piggy","cuisine":"barbecue","opens":"15:00:00","closes":"22:00:00","location":"623 Industrial Rd.","description":"Pork. We know how to cook it. Award-winning BBQ sauce, and meat with all the trimmings."},{"days":[1,2,3,4,5,6],"menuItems":[{"name":"Apple strudel","price":6.95},{"name":"Apricot dumpling with yogurt topping ","price":3.95},{"name":"Balkánský Salad","price":3.95},{"name":"Beef goulash","price":8.95},{"name":"Chicken breast fillet schnitzel ","price":10.45},{"name":"Cucumber Salad","price":7.55},{"name":"Dumplings","price":5.95},{"name":"Fried goose liver with onion and bread","price":7.55},{"name":"Halusky with sauerkraut and belly bacon ","price":9.95},{"name":"Lentil soup","price":4.5},{"name":"Pickles with cabbage and cheddar","price":10.45},{"name":"Pork schnitzel","price":3.95},{"name":"Potato pancake with bacon","price":5.95},{"name":"Potato Salad","price":4.55},{"name":"Segedínský gulash and dumplings","price":6.95},{"name":"Sour cabbage soup","price":10.45}],"price":1,"rating":4,"id":"czechpoint","name":"Czech Point","cuisine":"czech/slovak","opens":"10:30:00","closes":"21:30:00","location":"5567 Queen-Mary Rd","description":"Make a point of trying our knedlíky and homemade soups. We have free wifi and the best desserts and coffee. "},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"Bockwurst Würstchen","price":4.95},{"name":"Bratwurst mit Brötchen und Sauerkraut","price":5.95},{"name":"Currywurst mit Brötchen","price":5.95},{"name":"Das Hausmannskost","price":11.45},{"name":"Fleishkas mit Kartoffelsalat","price":6.95},{"name":"Frankfurter Würstchen","price":9.95},{"name":"Französische Zwiebelsuppe mit Käse","price":10.45},{"name":"Frikadelle mit Brötchen","price":6.95},{"name":"Gebackener Camenbert","price":7.55},{"name":"Gemischter Salat","price":4.55},{"name":"Haus Salatteller","price":11.45},{"name":"Jaegerschnitzel","price":9.95},{"name":"Kaesepaetzle","price":6.95},{"name":"Kartoffel Reibekuchen mit Apfelmus","price":5.95},{"name":"Maultaschen mit Käse","price":7.95},{"name":"Sauerbraten","price":10.45},{"name":"Ungarische Gulaschsuppe mit Brötchen","price":3.95},{"name":"Wienerschnitzel","price":8.95},{"name":"Wurstsalad mit Bauernbrot","price":6.95}],"price":3,"rating":5,"id":"speisewagen","name":"Der Speisewagen","cuisine":"german","opens":"17:00:00","closes":"22:30:00","location":"402 College Blvd.","description":"Award-winning schnitzel and other favorites. Look for our restored food truck in the NE corner of the College St lot."},{"days":[1,3,5,6,0],"menuItems":[{"name":"Almond cookie","price":5.95},{"name":"Chicken and broccoli","price":9.95},{"name":"Chow mein","price":4.95},{"name":"Egg rolls (4)","price":3.95},{"name":"General Tao's chicken","price":5.95},{"name":"Hot and Sour Soup","price":7.55},{"name":"Hunan dumplings","price":6.5},{"name":"Mongolian beef","price":6.95},{"name":"Pan-fried beef noodle","price":7.95},{"name":"Pea shoots with garlic","price":8.95},{"name":"Potstickers (6)","price":6.95},{"name":"Seafood hotpot","price":4.95},{"name":"Steamed rice","price":5.95},{"name":"Sweet and sour pork","price":6.95},{"name":"Walnut prawns","price":4.95},{"name":"Wonton Soup","price":6.25},{"name":"Young Chow fried rice","price":6.45}],"price":2,"rating":4,"id":"beijing","name":"Beijing Express","cuisine":"chinese","opens":"11:00:00","closes":"22:30:00","location":"38 Teutonic Ave.","description":"Fast, healthy, Chinese food. Family specials for takeout or delivery. Try our Peking Duck!"},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"Basil duck with rice","price":4.55},{"name":"Curry salmon","price":12.45},{"name":"Egg rolls (4)","price":5.95},{"name":"Fried banana and ice cream","price":11.45},{"name":"Green curry with chicken","price":3.95},{"name":"Green curry with pork","price":4.55},{"name":"Hot tea","price":2.5},{"name":"Onion pancake","price":6.95},{"name":"Pad See Ew","price":4.95},{"name":"Pad Thai","price":6.95},{"name":"Pumpkin curry","price":6.95},{"name":"Red curry with chicken","price":8.95},{"name":"Red curry with pork","price":9.95},{"name":"Sticky rice with mango","price":6.95},{"name":"Thai iced coffee","price":6.95},{"name":"Thai iced tea","price":3.95},{"name":"Tofu salad rolls","price":10.45}],"price":4,"rating":2,"id":"satay","name":"Satay Village","cuisine":"thai","opens":"17:30:00","closes":"22:00:00","location":"12 High St.","description":"Fine dining Thai-style. Wide selection of vegetarian entrées. We also deliver."},{"days":[1,2,3,4,5],"menuItems":[{"name":"Beans and rice","price":7.95},{"name":"Beef burrito","price":6.95},{"name":"Birria","price":7.95},{"name":"Chicken burrito","price":11.45},{"name":"Chicken mole platter","price":5.95},{"name":"Chile relleno (meat)","price":6.95},{"name":"Chile relleno (vegetarian)","price":3.95},{"name":"Chips and guacamole","price":5.95},{"name":"Enchiladas","price":4.55},{"name":"Flan","price":7.95},{"name":"Jamaica Aqua Fresca","price":2.5},{"name":"Pork al pastor platter","price":5.95},{"name":"Sopa de albondigas","price":7.95},{"name":"Sopa de pollo","price":6.95},{"name":"Strawberry Aqua Fresca","price":3.95},{"name":"Super nachos with carne asada","price":5.95},{"name":"Tacos de la casa (3)","price":4.95},{"name":"Vegetarian platter","price":4.55}],"price":3,"rating":3,"id":"cancun","name":"Cancun","cuisine":"mexican","opens":"11:30:00","closes":"23:00:00","location":"2030 Maple Rd.","description":"Tacos, tortas, burritos, just the way you like them. Our hot sauce and guacamole are the best in town."},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"Aloo Gobi","price":5.95},{"name":"Basmati rice","price":6.95},{"name":"Butter Chicken","price":5.95},{"name":"Chicken Korma","price":7.55},{"name":"Chicken Tikka Masala","price":5.95},{"name":"Gulab Jamun","price":8.95},{"name":"Kheer","price":4.5},{"name":"Lamb Asparagus","price":9.5},{"name":"Lamb Vindaloo","price":7.95},{"name":"Mix Grill Bombay","price":5.95},{"name":"Mulligatawny soup","price":5.95},{"name":"Murgh Chicken","price":3.95},{"name":"Naan stuffed with spinach and lamb","price":4.55},{"name":"Plain naan","price":5.95},{"name":"Rogan Josh","price":5.95},{"name":"Saag Paneer","price":5.95},{"name":"Tandoori Chicken","price":4.95}],"price":4,"rating":5,"id":"curryup","name":"Curry Up","cuisine":"indian","opens":"17:00:00","closes":"22:00:00","location":"455 University","description":"Indian food with a modern twist. We use all-natural ingredients and the finest spices to delight and tempt your palate."},{"days":[2,3,4,5,6,0],"menuItems":[{"name":"Dibi Lamb","price":8.25},{"name":"Doro Wat","price":5.95},{"name":"Grilled Chicken","price":4.95},{"name":"Grilled Fish","price":6.95},{"name":"Grilled Plantains in Spicy Peanut Sauce","price":7.95},{"name":"Lamb Mafe (Peanut Butter Stew)","price":8.75},{"name":"Meat Pie","price":6},{"name":"Mechoui with Plantains","price":5.95},{"name":"Pepper Soup","price":9.95},{"name":"Piri-Piri Shrimp","price":11.45},{"name":"Suppa Kandja","price":3.95},{"name":"Thiou Boulette","price":6.95},{"name":"Thiou Curry with Chicken","price":7.95},{"name":"Thu Okra","price":6.95},{"name":"Yassa Chicken","price":8.95},{"name":"Yassa Lamb","price":8.25}],"price":2,"rating":1,"id":"carthage","name":"Carthage","cuisine":"african","opens":"17:00:00","closes":"22:30:00","location":"59 Court Terrace","description":"Wholesome food and all the rich flavor of Africa. Try our famous lentil soup."},{"days":[1,2,3,4,5,6],"menuItems":[{"name":"Buffalo wings","price":5.95},{"name":"California-style baked Tilapia with rice","price":6.95},{"name":"Cheeseburger and fries","price":4.55},{"name":"Cherry pie a la mode","price":4.95},{"name":"Chocolate milkshake","price":6.95},{"name":"Cobb salad","price":4.95},{"name":"Famous BLT on a kaiser roll with fries","price":4.95},{"name":"Firehouse chili","price":6.95},{"name":"Goat cheese and eggplant wrap (vegetarian)","price":5.95},{"name":"Greek salad","price":6.95},{"name":"Grilled chicken sandwich","price":4.95},{"name":"Grilled sausage on a bun","price":6.95},{"name":"Housemade pot roast with seasonal vegetable","price":16.95},{"name":"Roast beef dip","price":11.45},{"name":"Roast chicken and mashed potatoes","price":7.55},{"name":"Soup of the day","price":8.95},{"name":"Spaghetti and meatballs","price":11.45}],"price":5,"rating":4,"id":"burgerama","name":"Burgerama","cuisine":"american","opens":"11:00:00","closes":"23:00:00","location":"456 University","description":"Grade A beef, freshly ground every day, hand-cut fries, and home-made milkshakes. We make the best burgers in town. "},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"BBQ chicken","price":4.95},{"name":"Beef ribs (full)","price":9.95},{"name":"Beef ribs (delux)","price":10.45},{"name":"Beef ribs (half)","price":6.45},{"name":"Beer","price":7.55},{"name":"Coleslaw","price":8.95},{"name":"Collards","price":9.95},{"name":"Cornbread","price":11.45},{"name":"Devilled eggs","price":4},{"name":"German chocolate cake","price":5.95},{"name":"Housemade chips","price":4},{"name":"Hushpuppies","price":3.25},{"name":"Mac and cheese","price":6},{"name":"Pork ribs (half)","price":6.95},{"name":"Potato salad","price":3.95},{"name":"Pulled pork sandwich on a soft roll","price":4.95},{"name":"Riblets","price":10.45}],"price":3,"rating":2,"id":"littlepigs","name":"Three Little Pigs","cuisine":"barbecue","opens":"11:30:00","closes":"22:30:00","location":"12 Summer Court","description":"Genuine East Texas barbecue. Accept no substitutes! "},{"days":[1,3,5,6,0],"menuItems":[{"name":"Apple strudel","price":6.95},{"name":"Apricot dumpling with yogurt topping ","price":3.95},{"name":"Balkánský Salad","price":3.95},{"name":"Beef goulash","price":8.95},{"name":"Chicken breast fillet schnitzel ","price":10.45},{"name":"Cucumber Salad","price":7.55},{"name":"Dumplings","price":5.95},{"name":"Fried goose liver with onion and bread","price":7.55},{"name":"Halusky with sauerkraut and belly bacon ","price":9.95},{"name":"Lentil soup","price":4.5},{"name":"Pickles with cabbage and cheddar","price":10.45},{"name":"Pork schnitzel","price":3.95},{"name":"Potato pancake with bacon","price":5.95},{"name":"Potato Salad","price":4.55},{"name":"Segedínský gulash and dumplings","price":6.95},{"name":"Sour cabbage soup","price":10.45}],"price":4,"rating":3,"id":"littleprague","name":"Little Prague","cuisine":"czech/slovak","opens":"11:00:00","closes":"22:00:00","location":"44 Park Ave","description":"We're famous for our housemade sausage and desserts. Come taste real European cooking."},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"Bockwurst Würstchen","price":4.95},{"name":"Bratwurst mit Brötchen und Sauerkraut","price":5.95},{"name":"Currywurst mit Brötchen","price":5.95},{"name":"Das Hausmannskost","price":11.45},{"name":"Fleishkas mit Kartoffelsalat","price":6.95},{"name":"Frankfurter Würstchen","price":9.95},{"name":"Französische Zwiebelsuppe mit Käse","price":10.45},{"name":"Frikadelle mit Brötchen","price":6.95},{"name":"Gebackener Camenbert","price":7.55},{"name":"Gemischter Salat","price":4.55},{"name":"Haus Salatteller","price":11.45},{"name":"Jaegerschnitzel","price":9.95},{"name":"Kaesepaetzle","price":6.95},{"name":"Kartoffel Reibekuchen mit Apfelmus","price":5.95},{"name":"Maultaschen mit Käse","price":7.95},{"name":"Sauerbraten","price":10.45},{"name":"Ungarische Gulaschsuppe mit Brötchen","price":3.95},{"name":"Wienerschnitzel","price":8.95},{"name":"Wurstsalad mit Bauernbrot","price":6.95}],"price":3,"rating":2,"id":"kohlhaus","name":"Kohl Haus","cuisine":"german","opens":"17:00:00","closes":"22:30:00","location":"3421 Queen-Mary Rd","description":"East German specialties, in a family-friendly setting. Come warm up with our delicious soups."},{"days":[1,2,3,4,5],"menuItems":[{"name":"Almond cookie","price":5.95},{"name":"Chicken and broccoli","price":9.95},{"name":"Chow mein","price":4.95},{"name":"Egg rolls (4)","price":3.95},{"name":"General Tao's chicken","price":5.95},{"name":"Hot and Sour Soup","price":7.55},{"name":"Hunan dumplings","price":6.5},{"name":"Mongolian beef","price":6.95},{"name":"Pan-fried beef noodle","price":7.95},{"name":"Pea shoots with garlic","price":8.95},{"name":"Potstickers (6)","price":6.95},{"name":"Seafood hotpot","price":4.95},{"name":"Steamed rice","price":5.95},{"name":"Sweet and sour pork","price":6.95},{"name":"Walnut prawns","price":4.95},{"name":"Wonton Soup","price":6.25},{"name":"Young Chow fried rice","price":6.45}],"price":1,"rating":4,"id":"dragon","name":"Dragon's Tail","cuisine":"chinese","opens":"17:00:00","closes":"2:00:00","location":"8 Jasmine Rd.","description":"Take-out or dine-in Chinese food. Open late. Delivery available"},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"Basil duck with rice","price":4.55},{"name":"Curry salmon","price":12.45},{"name":"Egg rolls (4)","price":5.95},{"name":"Fried banana and ice cream","price":11.45},{"name":"Green curry with chicken","price":3.95},{"name":"Green curry with pork","price":4.55},{"name":"Hot tea","price":2.5},{"name":"Onion pancake","price":6.95},{"name":"Pad See Ew","price":4.95},{"name":"Pad Thai","price":6.95},{"name":"Pumpkin curry","price":6.95},{"name":"Red curry with chicken","price":8.95},{"name":"Red curry with pork","price":9.95},{"name":"Sticky rice with mango","price":6.95},{"name":"Thai iced coffee","price":6.95},{"name":"Thai iced tea","price":3.95},{"name":"Tofu salad rolls","price":10.45}],"price":3,"rating":5,"id":"babythai","name":"Hit Me Baby One More Thai","cuisine":"thai","opens":"15:00:00","closes":"22:00:00","location":"12 Jasmine Rd.","description":"Thai food with a youthful bar scene. Try our tropical inspired cocktails, or tuck into a plate of our famous pad thai."},{"days":[2,3,4,5,6,0],"menuItems":[{"name":"Beans and rice","price":7.95},{"name":"Beef burrito","price":6.95},{"name":"Birria","price":7.95},{"name":"Chicken burrito","price":11.45},{"name":"Chicken mole platter","price":5.95},{"name":"Chile relleno (meat)","price":6.95},{"name":"Chile relleno (vegetarian)","price":3.95},{"name":"Chips and guacamole","price":5.95},{"name":"Enchiladas","price":4.55},{"name":"Flan","price":7.95},{"name":"Jamaica Aqua Fresca","price":2.5},{"name":"Pork al pastor platter","price":5.95},{"name":"Sopa de albondigas","price":7.95},{"name":"Sopa de pollo","price":6.95},{"name":"Strawberry Aqua Fresca","price":3.95},{"name":"Super nachos with carne asada","price":5.95},{"name":"Tacos de la casa (3)","price":4.95},{"name":"Vegetarian platter","price":4.55}],"price":2,"rating":4,"id":"wholetamale","name":"The Whole Tamale","cuisine":"mexican","opens":"10:30:00","closes":"21:30:00","location":"401 University","description":"The tamale and hot sauce experts. Tamale special changes daily."},{"days":[1,2,3,4,5,6],"menuItems":[{"name":"Aloo Gobi","price":5.95},{"name":"Basmati rice","price":6.95},{"name":"Butter Chicken","price":5.95},{"name":"Chicken Korma","price":7.55},{"name":"Chicken Tikka Masala","price":5.95},{"name":"Gulab Jamun","price":8.95},{"name":"Kheer","price":4.5},{"name":"Lamb Asparagus","price":9.5},{"name":"Lamb Vindaloo","price":7.95},{"name":"Mix Grill Bombay","price":5.95},{"name":"Mulligatawny soup","price":5.95},{"name":"Murgh Chicken","price":3.95},{"name":"Naan stuffed with spinach and lamb","price":4.55},{"name":"Plain naan","price":5.95},{"name":"Rogan Josh","price":5.95},{"name":"Saag Paneer","price":5.95},{"name":"Tandoori Chicken","price":4.95}],"price":4,"rating":2,"id":"bhangra","name":"Birmingham Bhangra","cuisine":"indian","opens":"17:00:00","closes":"22:30:00","location":"992 Riddick St.","description":"Curry with a metropolitan twist. Daily specials. Dine-in or takeaway, you choose."},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"Beans and rice","price":7.95},{"name":"Beef burrito","price":6.95},{"name":"Birria","price":7.95},{"name":"Chicken burrito","price":11.45},{"name":"Chicken mole platter","price":5.95},{"name":"Chile relleno (meat)","price":6.95},{"name":"Chile relleno (vegetarian)","price":3.95},{"name":"Chips and guacamole","price":5.95},{"name":"Enchiladas","price":4.55},{"name":"Flan","price":7.95},{"name":"Jamaica Aqua Fresca","price":2.5},{"name":"Pork al pastor platter","price":5.95},{"name":"Sopa de albondigas","price":7.95},{"name":"Sopa de pollo","price":6.95},{"name":"Strawberry Aqua Fresca","price":3.95},{"name":"Super nachos with carne asada","price":5.95},{"name":"Tacos de la casa (3)","price":4.95},{"name":"Vegetarian platter","price":4.55}],"price":3,"rating":3,"id":"taqueria","name":"Taqueria","cuisine":"mexican","opens":"11:00:00","closes":"22:30:00","location":"12 North Circle Dr.","description":"Taqueria y panaderia. Birria served on weekends."},{"days":[1,3,5,6,0],"menuItems":[{"name":"Beans and rice","price":7.95},{"name":"Beef burrito","price":6.95},{"name":"Birria","price":7.95},{"name":"Chicken burrito","price":11.45},{"name":"Chicken mole platter","price":5.95},{"name":"Chile relleno (meat)","price":6.95},{"name":"Chile relleno (vegetarian)","price":3.95},{"name":"Chips and guacamole","price":5.95},{"name":"Enchiladas","price":4.55},{"name":"Flan","price":7.95},{"name":"Jamaica Aqua Fresca","price":2.5},{"name":"Pork al pastor platter","price":5.95},{"name":"Sopa de albondigas","price":7.95},{"name":"Sopa de pollo","price":6.95},{"name":"Strawberry Aqua Fresca","price":3.95},{"name":"Super nachos with carne asada","price":5.95},{"name":"Tacos de la casa (3)","price":4.95},{"name":"Vegetarian platter","price":4.55}],"price":4,"rating":5,"id":"pedros","name":"Pedro's","cuisine":"mexican","opens":"17:30:00","closes":"22:00:00","location":"5521 Alameda","description":"Pedro's has been an Alameda staple for thirty years. Our list of fine tequilas and slow-cooked carnitas will make you a regular."},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"Almond cookie","price":5.95},{"name":"Chicken and broccoli","price":9.95},{"name":"Chow mein","price":4.95},{"name":"Egg rolls (4)","price":3.95},{"name":"General Tao's chicken","price":5.95},{"name":"Hot and Sour Soup","price":7.55},{"name":"Hunan dumplings","price":6.5},{"name":"Mongolian beef","price":6.95},{"name":"Pan-fried beef noodle","price":7.95},{"name":"Pea shoots with garlic","price":8.95},{"name":"Potstickers (6)","price":6.95},{"name":"Seafood hotpot","price":4.95},{"name":"Steamed rice","price":5.95},{"name":"Sweet and sour pork","price":6.95},{"name":"Walnut prawns","price":4.95},{"name":"Wonton Soup","price":6.25},{"name":"Young Chow fried rice","price":6.45}],"price":2,"rating":1,"id":"superwonton","name":"Super Wonton Express","cuisine":"chinese","opens":"11:30:00","closes":"23:00:00","location":"223 Milliways Ave","description":"Soups, stir-fries, and more. We cook fast."},{"days":[1,2,3,4,5],"menuItems":[{"name":"Aloo Gobi","price":5.95},{"name":"Basmati rice","price":6.95},{"name":"Butter Chicken","price":5.95},{"name":"Chicken Korma","price":7.55},{"name":"Chicken Tikka Masala","price":5.95},{"name":"Gulab Jamun","price":8.95},{"name":"Kheer","price":4.5},{"name":"Lamb Asparagus","price":9.5},{"name":"Lamb Vindaloo","price":7.95},{"name":"Mix Grill Bombay","price":5.95},{"name":"Mulligatawny soup","price":5.95},{"name":"Murgh Chicken","price":3.95},{"name":"Naan stuffed with spinach and lamb","price":4.55},{"name":"Plain naan","price":5.95},{"name":"Rogan Josh","price":5.95},{"name":"Saag Paneer","price":5.95},{"name":"Tandoori Chicken","price":4.95}],"price":5,"rating":4,"id":"naansequitur","name":"Naan Sequitur","cuisine":"indian","opens":"17:00:00","closes":"22:00:00","location":"Unit 12, Olde Towne Mall","description":"Naan and tandoori specialties, from our clay oven. "},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"California roll","price":5.95},{"name":"Chicken teriyaki","price":5.95},{"name":"Edamame","price":6.95},{"name":"Futomaki roll","price":null},{"name":"Green tea ice cream","price":6.95},{"name":"Kitsune Udon","price":9.5},{"name":"Miso soup","price":5.95},{"name":"Pork Katsu","price":5.95},{"name":"Salmon teriyaki","price":5.95},{"name":"Sashimi combo","price":6.95},{"name":"Spicy Yellowtail roll","price":7.55},{"name":"Sushi combo","price":4.55},{"name":"Teppa Maki","price":5.95},{"name":"Unagi Don","price":7.55},{"name":"Vegetable tempura","price":3.95},{"name":"Vegetarian sushi plate","price":6.95},{"name":"Wakame salad","price":4.95}],"price":3,"rating":2,"id":"sakura","name":"Sakura","cuisine":"japanese","opens":"17:00:00","closes":"22:30:00","location":"Unit 18, Olde Towne Mall","description":"Sushi specials daily. We serve fast, friendly, fresh Japanese cuisine."},{"days":[2,3,4,5,6,0],"menuItems":[{"name":"Almond cookie","price":5.95},{"name":"Chicken and broccoli","price":9.95},{"name":"Chow mein","price":4.95},{"name":"Egg rolls (4)","price":3.95},{"name":"General Tao's chicken","price":5.95},{"name":"Hot and Sour Soup","price":7.55},{"name":"Hunan dumplings","price":6.5},{"name":"Mongolian beef","price":6.95},{"name":"Pan-fried beef noodle","price":7.95},{"name":"Pea shoots with garlic","price":8.95},{"name":"Potstickers (6)","price":6.95},{"name":"Seafood hotpot","price":4.95},{"name":"Steamed rice","price":5.95},{"name":"Sweet and sour pork","price":6.95},{"name":"Walnut prawns","price":4.95},{"name":"Wonton Soup","price":6.25},{"name":"Young Chow fried rice","price":6.45}],"price":4,"rating":3,"id":"shandong","name":"Shandong Lu","cuisine":"chinese","opens":"17:00:00","closes":"22:30:00","location":"335 University","description":"Szechuan and Mandarin specialities with a fine dining ambiance. Our hot and sour soup is the best in town."},{"days":[1,2,3,4,5,6],"menuItems":[{"name":"Aloo Gobi","price":5.95},{"name":"Basmati rice","price":6.95},{"name":"Butter Chicken","price":5.95},{"name":"Chicken Korma","price":7.55},{"name":"Chicken Tikka Masala","price":5.95},{"name":"Gulab Jamun","price":8.95},{"name":"Kheer","price":4.5},{"name":"Lamb Asparagus","price":9.5},{"name":"Lamb Vindaloo","price":7.95},{"name":"Mix Grill Bombay","price":5.95},{"name":"Mulligatawny soup","price":5.95},{"name":"Murgh Chicken","price":3.95},{"name":"Naan stuffed with spinach and lamb","price":4.55},{"name":"Plain naan","price":5.95},{"name":"Rogan Josh","price":5.95},{"name":"Saag Paneer","price":5.95},{"name":"Tandoori Chicken","price":4.95}],"price":3,"rating":2,"id":"currygalore","name":"Curry Galore","cuisine":"indian","opens":"11:00:00","closes":"22:30:00","location":"56 Park Ave","description":"Famous North Indian home cooking. Spicy or mild, as you like it. Delivery available."},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"Apple pie","price":5.95},{"name":"B.L.T. and Avocado Sandwich","price":5.95},{"name":"Caesar salad","price":5.95},{"name":"Cappucino","price":3.95},{"name":"Cherry cheesecake","price":4.95},{"name":"Chocolate chip cookie","price":4.55},{"name":"Cobb salad","price":6.95},{"name":"Drip coffee","price":5.95},{"name":"Eggsalad Sandwich","price":3.95},{"name":"Espresso","price":6.95},{"name":"Greek salad","price":3.95},{"name":"Hot tea","price":2.5},{"name":"Iced tea","price":2.5},{"name":"Latte","price":4},{"name":"Mango and banana smoothie","price":3},{"name":"Orange juice","price":4.95},{"name":"Quiche of the day","price":6.95},{"name":"Turkey Sandwich","price":7.55}],"price":1,"rating":4,"id":"north","name":"North by Northwest","cuisine":"cafe","opens":"6:00:00","closes":"18:00:00","location":"201 University","description":"Great coffee and snacks. Free wifi. "},{"days":[1,3,5,6,0],"menuItems":[{"name":"Apple pie","price":5.95},{"name":"B.L.T. and Avocado Sandwich","price":5.95},{"name":"Caesar salad","price":5.95},{"name":"Cappucino","price":3.95},{"name":"Cherry cheesecake","price":4.95},{"name":"Chocolate chip cookie","price":4.55},{"name":"Cobb salad","price":6.95},{"name":"Drip coffee","price":5.95},{"name":"Eggsalad Sandwich","price":3.95},{"name":"Espresso","price":6.95},{"name":"Greek salad","price":3.95},{"name":"Hot tea","price":2.5},{"name":"Iced tea","price":2.5},{"name":"Latte","price":4},{"name":"Mango and banana smoothie","price":3},{"name":"Orange juice","price":4.95},{"name":"Quiche of the day","price":6.95},{"name":"Turkey Sandwich","price":7.55}],"price":3,"rating":5,"id":"beans","name":"Full of Beans","cuisine":"cafe","opens":"6:30:00","closes":"20:30:00","location":"498 College Ave.","description":"We roast on premises to give you the best cup of coffee in town. "},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"Apple pie","price":5.95},{"name":"B.L.T. and Avocado Sandwich","price":5.95},{"name":"Caesar salad","price":5.95},{"name":"Cappucino","price":3.95},{"name":"Cherry cheesecake","price":4.95},{"name":"Chocolate chip cookie","price":4.55},{"name":"Cobb salad","price":6.95},{"name":"Drip coffee","price":5.95},{"name":"Eggsalad Sandwich","price":3.95},{"name":"Espresso","price":6.95},{"name":"Greek salad","price":3.95},{"name":"Hot tea","price":2.5},{"name":"Iced tea","price":2.5},{"name":"Latte","price":4},{"name":"Mango and banana smoothie","price":3},{"name":"Orange juice","price":4.95},{"name":"Quiche of the day","price":6.95},{"name":"Turkey Sandwich","price":7.55}],"price":2,"rating":4,"id":"jeeves","name":"Tropical Jeeve's Cafe","cuisine":"cafe","opens":"7:00:00","closes":"14:30:00","location":"550 Milliways Ave","description":"Hawaiian style coffee, fresh juices, and tropical fruit smoothies."},{"days":[1,2,3,4,5],"menuItems":[{"name":"Apple pie","price":5.95},{"name":"B.L.T. and Avocado Sandwich","price":5.95},{"name":"Caesar salad","price":5.95},{"name":"Cappucino","price":3.95},{"name":"Cherry cheesecake","price":4.95},{"name":"Chocolate chip cookie","price":4.55},{"name":"Cobb salad","price":6.95},{"name":"Drip coffee","price":5.95},{"name":"Eggsalad Sandwich","price":3.95},{"name":"Espresso","price":6.95},{"name":"Greek salad","price":3.95},{"name":"Hot tea","price":2.5},{"name":"Iced tea","price":2.5},{"name":"Latte","price":4},{"name":"Mango and banana smoothie","price":3},{"name":"Orange juice","price":4.95},{"name":"Quiche of the day","price":6.95},{"name":"Turkey Sandwich","price":7.55}],"price":4,"rating":2,"id":"zardoz","name":"Zardoz Cafe","cuisine":"cafe","opens":"10:30:00","closes":"0:00:00","location":"6202 Alameda","description":"Coffee bar and sci-fi bookshop. Come in for an espresso or a slice of our famous pie."},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"Cesar salad","price":5.95},{"name":"Cheesecake","price":6.95},{"name":"Chicago-style deep dish chicken and spinach","price":6.95},{"name":"Chicago-style deep dish pepperoni and cheese","price":7.95},{"name":"Chicago-style deep dish vegetarian","price":6.95},{"name":"Chicago-style meat lover's","price":8.95},{"name":"Chocolate cake","price":3.95},{"name":"Coffee","price":7.95},{"name":"Garlic bread","price":null},{"name":"Greek salad","price":5.95},{"name":"Pizza of the day (slice)","price":7.55},{"name":"Thin crust anchovy and garlic and chili pepper","price":5.95},{"name":"Thin crust broccoli, chicken, and mozarella","price":3.95},{"name":"Thin crust margherita","price":4.55},{"name":"Thin crust pepperoni","price":6.95},{"name":"Thin crust quattro stagione","price":4.95},{"name":"Thin crust sausage and guanciale bacon","price":4.95}],"price":1,"rating":5,"id":"angular","name":"Angular Pizza","cuisine":"pizza","opens":"6:00:00","closes":"0:00:00","location":"2232 King St.","description":"Home of the superheroic pizza! "},{"days":[2,3,4,5,6,0],"menuItems":[{"name":"Cesar salad","price":5.95},{"name":"Cheesecake","price":6.95},{"name":"Chicago-style deep dish chicken and spinach","price":6.95},{"name":"Chicago-style deep dish pepperoni and cheese","price":7.95},{"name":"Chicago-style deep dish vegetarian","price":6.95},{"name":"Chicago-style meat lover's","price":8.95},{"name":"Chocolate cake","price":3.95},{"name":"Coffee","price":7.95},{"name":"Garlic bread","price":null},{"name":"Greek salad","price":5.95},{"name":"Pizza of the day (slice)","price":7.55},{"name":"Thin crust anchovy and garlic and chili pepper","price":5.95},{"name":"Thin crust broccoli, chicken, and mozarella","price":3.95},{"name":"Thin crust margherita","price":4.55},{"name":"Thin crust pepperoni","price":6.95},{"name":"Thin crust quattro stagione","price":4.95},{"name":"Thin crust sausage and guanciale bacon","price":4.95}],"price":4,"rating":5,"id":"flavia","name":"Flavia","cuisine":"pizza","opens":"11:30:00","closes":"22:00:00","location":"401 Riddick St.","description":"Roman-style pizza -- square, the way the gods intended."},{"days":[1,2,3,4,5,6],"menuItems":[{"name":"Cesar salad","price":5.95},{"name":"Cheesecake","price":6.95},{"name":"Chicago-style deep dish chicken and spinach","price":6.95},{"name":"Chicago-style deep dish pepperoni and cheese","price":7.95},{"name":"Chicago-style deep dish vegetarian","price":6.95},{"name":"Chicago-style meat lover's","price":8.95},{"name":"Chocolate cake","price":3.95},{"name":"Coffee","price":7.95},{"name":"Garlic bread","price":null},{"name":"Greek salad","price":5.95},{"name":"Pizza of the day (slice)","price":7.55},{"name":"Thin crust anchovy and garlic and chili pepper","price":5.95},{"name":"Thin crust broccoli, chicken, and mozarella","price":3.95},{"name":"Thin crust margherita","price":4.55},{"name":"Thin crust pepperoni","price":6.95},{"name":"Thin crust quattro stagione","price":4.95},{"name":"Thin crust sausage and guanciale bacon","price":4.95}],"price":2,"rating":1,"id":"luigis","name":"Luigi's House of Pies","cuisine":"pizza","opens":"16:30:00","closes":"23:00:00","location":"5 Garcia Ave.","description":"Our secret pizza sauce makes our pizza better. We specialize in large groups."},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"Cesar salad","price":5.95},{"name":"Cheesecake","price":6.95},{"name":"Chicago-style deep dish chicken and spinach","price":6.95},{"name":"Chicago-style deep dish pepperoni and cheese","price":7.95},{"name":"Chicago-style deep dish vegetarian","price":6.95},{"name":"Chicago-style meat lover's","price":8.95},{"name":"Chocolate cake","price":3.95},{"name":"Coffee","price":7.95},{"name":"Garlic bread","price":null},{"name":"Greek salad","price":5.95},{"name":"Pizza of the day (slice)","price":7.55},{"name":"Thin crust anchovy and garlic and chili pepper","price":5.95},{"name":"Thin crust broccoli, chicken, and mozarella","price":3.95},{"name":"Thin crust margherita","price":4.55},{"name":"Thin crust pepperoni","price":6.95},{"name":"Thin crust quattro stagione","price":4.95},{"name":"Thin crust sausage and guanciale bacon","price":4.95}],"price":5,"rating":4,"id":"thick","name":"Thick and Thin","cuisine":"pizza","opens":"17:00:00","closes":"23:30:00","location":"832 Dominican Ave.","description":"Whether you're craving Chicago-style deep dish or thin as a wafer crust, we have you covered in toppings you'll love."},{"days":[1,3,5,6,0],"menuItems":[{"name":"Cesar salad","price":5.95},{"name":"Cheesecake","price":6.95},{"name":"Chicago-style deep dish chicken and spinach","price":6.95},{"name":"Chicago-style deep dish pepperoni and cheese","price":7.95},{"name":"Chicago-style deep dish vegetarian","price":6.95},{"name":"Chicago-style meat lover's","price":8.95},{"name":"Chocolate cake","price":3.95},{"name":"Coffee","price":7.95},{"name":"Garlic bread","price":null},{"name":"Greek salad","price":5.95},{"name":"Pizza of the day (slice)","price":7.55},{"name":"Thin crust anchovy and garlic and chili pepper","price":5.95},{"name":"Thin crust broccoli, chicken, and mozarella","price":3.95},{"name":"Thin crust margherita","price":4.55},{"name":"Thin crust pepperoni","price":6.95},{"name":"Thin crust quattro stagione","price":4.95},{"name":"Thin crust sausage and guanciale bacon","price":4.95}],"price":3,"rating":2,"id":"wheninrome","name":"When in Rome","cuisine":"pizza","opens":"11:00:00","closes":"21:30:00","location":"234 Valencia St.","description":"Authentic Italian pizza in a friendly neighborhood joint."},{"days":[1,2,3,4,5,6,0],"menuItems":[{"name":"Cesar salad","price":5.95},{"name":"Cheesecake","price":6.95},{"name":"Chicago-style deep dish chicken and spinach","price":6.95},{"name":"Chicago-style deep dish pepperoni and cheese","price":7.95},{"name":"Chicago-style deep dish vegetarian","price":6.95},{"name":"Chicago-style meat lover's","price":8.95},{"name":"Chocolate cake","price":3.95},{"name":"Coffee","price":7.95},{"name":"Garlic bread","price":null},{"name":"Greek salad","price":5.95},{"name":"Pizza of the day (slice)","price":7.55},{"name":"Thin crust anchovy and garlic and chili pepper","price":5.95},{"name":"Thin crust broccoli, chicken, and mozarella","price":3.95},{"name":"Thin crust margherita","price":4.55},{"name":"Thin crust pepperoni","price":6.95},{"name":"Thin crust quattro stagione","price":4.95},{"name":"Thin crust sausage and guanciale bacon","price":4.95}],"price":4,"rating":3,"id":"pizza76","name":"Pizza 76","cuisine":"pizza","opens":"10:30:00","closes":"22:00:00","location":"76 Market St.","description":"Wood-fired pizza with daily ingredients fresh from our farmer's market. We make our own mozzarella in house."}] --------------------------------------------------------------------------------