├── .bowerrc ├── src ├── img │ ├── ionic.png │ ├── logo.png │ ├── slide1.jpg │ ├── slide2.jpg │ └── slide3.jpg ├── js │ ├── templates.js │ ├── app │ │ ├── intro │ │ │ ├── intro-header.html │ │ │ ├── intro-header.directive.js │ │ │ ├── intro.router.js │ │ │ ├── nav-buttons.html │ │ │ ├── intro.ctrl.js │ │ │ └── intro.html │ │ ├── mainPage │ │ │ ├── chatDetail.html │ │ │ ├── chatDetail.ctrl.js │ │ │ ├── account.html │ │ │ ├── dash.ctrl.js │ │ │ ├── chats.ctrl.js │ │ │ ├── tabs.html │ │ │ ├── chats.html │ │ │ ├── dash.html │ │ │ ├── account.ctrl.js │ │ │ ├── mainPage.router.js │ │ │ └── services │ │ │ │ └── chats.service.js │ │ ├── auth │ │ │ ├── login │ │ │ │ ├── logout.ctrl.js │ │ │ │ ├── login.router.js │ │ │ │ ├── loggedout.html │ │ │ │ ├── login.ctrl.js │ │ │ │ └── login.html │ │ │ ├── signup │ │ │ │ ├── signup.router.js │ │ │ │ ├── signup.html │ │ │ │ └── signup.ctrl.js │ │ │ ├── changePassword │ │ │ │ ├── changePassword.router.js │ │ │ │ ├── changePassword.ctrl.js │ │ │ │ └── changePassword.html │ │ │ └── forgotPassword │ │ │ │ ├── forgotPassword.html │ │ │ │ └── forgotPassword.js │ │ ├── util │ │ │ ├── services │ │ │ │ ├── dummy │ │ │ │ │ └── tracking.service.dummyImpl.js │ │ │ │ ├── storage.service.js │ │ │ │ ├── tracking.service.js │ │ │ │ ├── local │ │ │ │ │ └── storage.service.localImpl.js │ │ │ │ ├── logentries │ │ │ │ │ └── tracking.service.logentriesImpl.js │ │ │ │ ├── stopWatch.service.js │ │ │ │ └── logging.service.js │ │ │ ├── templates │ │ │ │ └── form-errors.html │ │ │ └── directives │ │ │ │ ├── formErrors.directive.js │ │ │ │ ├── itemEnteredIndicator.directive.js │ │ │ │ ├── formField.directive.js │ │ │ │ └── formDirtyCheck.directive.js │ │ ├── user │ │ │ ├── services │ │ │ │ ├── user.service.js │ │ │ │ ├── firebase │ │ │ │ │ └── oauthHelper.service.js │ │ │ │ └── mock │ │ │ │ │ └── user.service.mockImpl.js │ │ │ └── models │ │ │ │ └── user.model.js │ │ ├── appHooks.service.mockImpl.js │ │ ├── appHooks.service.firebasekImpl.js │ │ ├── appHooks.service.js │ │ ├── application.ctrl.js │ │ ├── manage │ │ │ ├── image-crop-modal.html │ │ │ └── userProfile.html │ │ ├── menu │ │ │ └── menu.html │ │ ├── firebase │ │ │ └── fbutil.service.js │ │ ├── image │ │ │ └── services │ │ │ │ ├── image.service.js │ │ │ │ └── fileManager.service.js │ │ ├── app.js │ │ └── application.service.js │ ├── config │ │ ├── config-dev.json │ │ ├── config-prod.json │ │ └── config-base.json │ ├── lib │ │ ├── ng-img-crop-customized │ │ │ ├── README.md │ │ │ └── ng-img-crop.scss │ │ └── logentries │ │ │ └── le.min.js │ ├── modules.js │ └── locales │ │ └── en.json └── index-template.html ├── scss ├── app │ ├── .gitignore │ ├── _text.scss │ ├── app.scss │ ├── _util.scss │ ├── _tabs.scss │ ├── _button.scss │ ├── _bar.scss │ ├── _layout.scss │ ├── _styles.scss │ ├── _form.scss │ ├── _content-banner.scss │ ├── _profile.scss │ ├── _intro.scss │ ├── _action-sheet-customized.scss │ └── variables.scss ├── ionic.app.scss └── ionic-customized.scss ├── typings └── tsd.d.ts ├── www └── .gitignore ├── bin ├── ionic_serve.sh ├── run_protractor.sh └── protractor-chromedriver.sh ├── ionic.config.json ├── .gitignore ├── tsd.json ├── test ├── e2e │ ├── pages │ │ ├── logout.page.js │ │ ├── sideMenu.page.js │ │ └── login.page.js │ ├── specs │ │ └── auth │ │ │ └── login.spec.js │ └── common │ │ ├── utils.js │ │ └── login.helper.js └── unit │ └── mainPage │ ├── services │ └── chats.service-spec.js │ └── account.ctrl-spec.js ├── bower.json ├── LICENSE ├── karma.conf.js ├── package.json ├── hooks ├── after_prepare │ └── 010_add_platform_class.js └── README.md ├── protractor.conf.js ├── config.xml └── gulpfile.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "src/lib" 3 | } 4 | -------------------------------------------------------------------------------- /src/img/ionic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leob/ionic-quickstarter/HEAD/src/img/ionic.png -------------------------------------------------------------------------------- /src/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leob/ionic-quickstarter/HEAD/src/img/logo.png -------------------------------------------------------------------------------- /src/img/slide1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leob/ionic-quickstarter/HEAD/src/img/slide1.jpg -------------------------------------------------------------------------------- /src/img/slide2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leob/ionic-quickstarter/HEAD/src/img/slide2.jpg -------------------------------------------------------------------------------- /src/img/slide3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leob/ionic-quickstarter/HEAD/src/img/slide3.jpg -------------------------------------------------------------------------------- /scss/app/.gitignore: -------------------------------------------------------------------------------- 1 | # Git-ignore override for this directory. See: http://stackoverflow.com/a/5581995/2474068 2 | !_* 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /typings/tsd.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore contents of this directory (www) but not the directory itself. See: http://stackoverflow.com/a/5581995/2474068 2 | * 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /src/js/templates.js: -------------------------------------------------------------------------------- 1 | // THIS IS A PLACEHOLDER, TO BE REPLACED DURING PRODUCTION BUILDS BY GULP-ANGULAR-TEMPLATECACHE - DO NOT REMOVE ! 2 | angular.module("templates", []); -------------------------------------------------------------------------------- /src/js/app/intro/intro-header.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 7 |
8 | -------------------------------------------------------------------------------- /bin/ionic_serve.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # NOTE: Currently, "ionic_serve.sh" does exactly the same thing as 'ionic serve', but if needed we could add/change 3 | # stuff here. 4 | ionic serve --address 127.0.0.1 $* 5 | -------------------------------------------------------------------------------- /src/js/app/mainPage/chatDetail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | {{vm.chat.lastText}} 6 |

7 |
8 |
9 | -------------------------------------------------------------------------------- /src/js/config/config-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "constants": { 3 | "APP": { 4 | "devMode": true, 5 | "testMode": true, 6 | "tracking": false 7 | }, 8 | "TrackLogLevels": { 9 | "debug": false, 10 | "log": true, 11 | "info": true, 12 | "warn": true, 13 | "error": true 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/js/app/auth/login/logout.ctrl.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | var LogoutCtrl = /*@ngInject*/function ($state, Application) { 5 | var vm = this; 6 | 7 | vm.intro = function () { 8 | Application.gotoIntroPage($state); 9 | }; 10 | }; 11 | 12 | appModule('app.auth.login').controller('LogoutCtrl', LogoutCtrl); 13 | }()); 14 | -------------------------------------------------------------------------------- /src/js/config/config-prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "constants": { 3 | "APP": { 4 | "devMode": false, 5 | "testMode": false, 6 | "tracking": true 7 | }, 8 | "TrackLogLevels": { 9 | "debug": false, 10 | "log": true, 11 | "info": true, 12 | "warn": true, 13 | "error": true 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/js/app/auth/signup/signup.router.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.auth.signup') 5 | .config(function ($stateProvider) { 6 | $stateProvider 7 | .state('signup', { 8 | url: '/signup', 9 | templateUrl: 'js/app/auth/signup/signup.html', 10 | controller: 'SignupCtrl as vm' 11 | }); 12 | }); 13 | }()); 14 | -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-quickstarter", 3 | "app_id": "", 4 | "proxies": [ 5 | ], 6 | "watchPatterns": [ 7 | "src/**/*", 8 | "!src/lib/**/*" 9 | ], 10 | "documentRoot": "src", 11 | "browsers": [ 12 | { 13 | "platform": "android", 14 | "browser": "crosswalk", 15 | "version": "12.41.296.5" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/js/app/util/services/dummy/tracking.service.dummyImpl.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.util') 5 | 6 | .factory('TrackingServiceDummyImpl', function () { 7 | 8 | var trackEvent = function (logLevel, message) { 9 | // do nothing :-) 10 | }; 11 | 12 | return { 13 | trackEvent: trackEvent 14 | }; 15 | }) 16 | ; 17 | }()); 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | node_modules/ 5 | resources/ 6 | platforms/ 7 | plugins/ 8 | src/lib/ 9 | 10 | _* 11 | .DS_STORE 12 | .idea 13 | *.swp 14 | 15 | # EXCLUDE BUILD ARTIFACTS: 16 | src/index.html 17 | src/css/ionic.app.css 18 | src/css/ionic.app.min.css 19 | src/js/config/config.js 20 | npm-debug.log -------------------------------------------------------------------------------- /src/js/app/util/templates/form-errors.html: -------------------------------------------------------------------------------- 1 |
message.required
2 |
message.min-5
3 |
message.max-50
4 |
message.valid-email
-------------------------------------------------------------------------------- /src/js/app/intro/intro-header.directive.js: -------------------------------------------------------------------------------- 1 | ;(function () { 2 | "use strict"; 3 | 4 | var IntroHeader = function () { 5 | return ({ 6 | restrict: 'E', 7 | replace: true, 8 | scope: { 9 | img: '@' 10 | }, 11 | templateUrl: 'js/app/intro/intro-header.html' 12 | }); 13 | }; 14 | 15 | appModule('app.intro').directive('introHeader', IntroHeader); 16 | }()); 17 | -------------------------------------------------------------------------------- /bin/run_protractor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PORT=$1 3 | USER=$2 4 | PASSWORD=$3 5 | #npm run protractor -- --baseUrl=http://127.0.0.1:${PORT} --params.login.user=${USER} --params.login.password=${PASSWORD} 6 | # run without npm and with directConnect = true, this is now the default - WAY FASTER ! 7 | node_modules/protractor/bin/protractor --troubleshoot --baseUrl=http://127.0.0.1:${PORT} --params.login.user=${USER} --params.login.password=${PASSWORD} 8 | -------------------------------------------------------------------------------- /src/js/app/auth/changePassword/changePassword.router.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.auth.changePassword') 5 | .config(function ($stateProvider) { 6 | $stateProvider 7 | .state('changePassword', { 8 | url: '/changePassword?mode', 9 | templateUrl: 'js/app/auth/changePassword/changePassword.html', 10 | controller: 'ChangePasswordCtrl as vm' 11 | }); 12 | }); 13 | }()); 14 | -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "typings", 6 | "bundle": "typings/tsd.d.ts", 7 | "installed": { 8 | "jasmine/jasmine.d.ts": { 9 | "commit": "beba46f84f208cd9d03a329737fc04432d13f77f" 10 | }, 11 | "angular-protractor/angular-protractor.d.ts": { 12 | "commit": "beba46f84f208cd9d03a329737fc04432d13f77f" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/js/app/intro/intro.router.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.intro') 5 | .config(function ($stateProvider) { 6 | $stateProvider 7 | .state('app.intro', { 8 | url: '/intro', 9 | views: { 10 | 'menuContent@app': { 11 | templateUrl: 'js/app/intro/intro.html', 12 | controller: 'IntroCtrl as vm' 13 | } 14 | } 15 | }); 16 | }); 17 | }()); 18 | -------------------------------------------------------------------------------- /src/js/app/mainPage/chatDetail.ctrl.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | var ChatDetailCtrl = /*@ngInject*/function ($stateParams, Chats) { 5 | 6 | // vm: the "Controller as vm" convention from: http://www.johnpapa.net/angularjss-controller-as-and-the-vm-variable/ 7 | var vm = this; 8 | 9 | vm.chat = Chats.get($stateParams.chatId); 10 | }; 11 | 12 | appModule('app.mainPage').controller('ChatDetailCtrl', ChatDetailCtrl); 13 | }()); 14 | -------------------------------------------------------------------------------- /scss/app/_text.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Text styles 3 | */ 4 | 5 | .note { 6 | color: $note-color; 7 | font-size: 14px; 8 | } 9 | 10 | .link { 11 | color: $link-color; 12 | font-size: 14px; 13 | } 14 | 15 | .subtitle { 16 | font-size: large; 17 | } 18 | 19 | .italic { 20 | font-style: italic; 21 | } 22 | 23 | .bold { 24 | font-weight: bold; 25 | } 26 | 27 | .larger { 28 | font-size: larger; 29 | } 30 | 31 | .smaller { 32 | font-size: smaller; 33 | } 34 | -------------------------------------------------------------------------------- /test/e2e/pages/logout.page.js: -------------------------------------------------------------------------------- 1 | module.exports = (function () { 2 | 'use strict'; 3 | 4 | var page = 'loggedout'; 5 | 6 | var LogoutPage = function () { 7 | }; 8 | 9 | LogoutPage.prototype = Object.create({}, { 10 | url: { 11 | get: function () { 12 | return '/' + page; 13 | } 14 | }, 15 | clickLogin: { 16 | value: function () { 17 | element(by.name('loginPanel')).click(); 18 | } 19 | } 20 | }); 21 | 22 | return LogoutPage; 23 | }()); -------------------------------------------------------------------------------- /src/js/config/config-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app.config", 3 | "constants": { 4 | "APP": { 5 | "routerDefaultState": "app.auth.main.dash" 6 | }, 7 | "FirebaseConfiguration": { 8 | "url": "https://your-firebase-app.firebaseio.com", 9 | "debug": false 10 | }, 11 | "TwitterReauthentication": { 12 | "useReauthenticationHack": "false", 13 | "consumerKey": "TWITTER_CONSUMER_KEY", 14 | "consumerSecretKey": "TWITTER_CONSUMER_SECRET" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/js/app/util/directives/formErrors.directive.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.util').directive('formErrors', function() { 5 | return { 6 | restrict: 'E', 7 | replace: true, 8 | template: '
' + 9 | '
' + 10 | '{{vm.error.message}}' + 11 | '
' + 12 | '
' 13 | }; 14 | }); 15 | }()); -------------------------------------------------------------------------------- /src/js/app/util/services/storage.service.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.util') 5 | 6 | // Conditional DI, technique taken from: 7 | // http://phonegap-tips.com/articles/conditional-dependency-injection-with-angularjs.html 8 | 9 | .factory('StorageService', function ($injector, APP) { 10 | //if (APP.devMode) { 11 | return $injector.get('StorageServiceLocalImpl'); 12 | //} else { 13 | // return $injector.get('StorageServiceLocalImpl'); 14 | //} 15 | }); 16 | }()); 17 | -------------------------------------------------------------------------------- /src/js/app/util/services/tracking.service.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.util') 5 | 6 | // Conditional DI, technique taken from: 7 | // http://phonegap-tips.com/articles/conditional-dependency-injection-with-angularjs.html 8 | 9 | .factory('TrackingService', function ($injector, APP) { 10 | //if (APP.devMode) { 11 | return $injector.get('TrackingServiceDummyImpl'); 12 | //} else { 13 | // return $injector.get('TrackingServiceLogentriesImpl'); 14 | //} 15 | }); 16 | }()); 17 | -------------------------------------------------------------------------------- /src/js/app/auth/login/login.router.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.auth.login') 5 | .config(function ($stateProvider) { 6 | $stateProvider 7 | .state('login', { 8 | url: '/login', 9 | templateUrl: 'js/app/auth/login/login.html', 10 | controller: 'LoginCtrl as vm' 11 | }) 12 | .state('loggedout', { 13 | url: '/loggedout', 14 | templateUrl: 'js/app/auth/login/loggedout.html', 15 | controller: 'LogoutCtrl as vm' 16 | }); 17 | }); 18 | }()); 19 | -------------------------------------------------------------------------------- /src/js/app/user/services/user.service.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.user') 5 | 6 | // Conditional DI, technique taken from: 7 | // http://phonegap-tips.com/articles/conditional-dependency-injection-with-angularjs.html 8 | 9 | .factory('UserService', function ($injector, APP) { 10 | if (APP.devMode) { 11 | return $injector.get('UserServiceMockImpl'); 12 | } else { 13 | // Firebase implementation 14 | return $injector.get('UserServiceFirebaseImpl'); 15 | } 16 | }); 17 | }()); 18 | -------------------------------------------------------------------------------- /scss/app/app.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Custom styles 3 | */ 4 | 5 | @import "action-sheet-customized"; 6 | @import "bar"; 7 | @import "button"; 8 | @import "content-banner"; 9 | @import "form"; 10 | @import "intro"; 11 | @import "layout"; 12 | @import "profile"; 13 | @import "styles"; 14 | @import "tabs"; 15 | @import "text"; 16 | @import "util"; 17 | 18 | // ionic-content-banner: https://github.com/djett41/ionic-content-banner 19 | //@import "../../src/lib/ionic-content-banner/scss/ionic.content.banner"; 20 | 21 | @import "src/js/lib/ng-img-crop-customized/ng-img-crop"; 22 | 23 | -------------------------------------------------------------------------------- /test/e2e/pages/sideMenu.page.js: -------------------------------------------------------------------------------- 1 | module.exports = (function () { 2 | 'use strict'; 3 | 4 | var SideMenu = function () { 5 | }; 6 | 7 | SideMenu.prototype = Object.create({}, { 8 | menu: { 9 | get: function () { 10 | return element(by.tagName('ion-side-menu')); 11 | } 12 | }, 13 | logout: { 14 | get: function () { 15 | return this.menu.element(by.name('logout')) 16 | } 17 | }, 18 | clickLogout: { 19 | value: function () { 20 | return this.logout.click(); 21 | } 22 | } 23 | }); 24 | 25 | return SideMenu; 26 | }()); -------------------------------------------------------------------------------- /src/js/app/mainPage/account.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | Logged in as: {{vm.user}} 11 | 12 | 13 | Logged in for: {{vm.loginDuration()}} 14 | 15 | 16 | Enable Friends 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/js/app/auth/login/loggedout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 | link.login-again 14 |
15 | 16 |
17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /src/js/lib/ng-img-crop-customized/README.md: -------------------------------------------------------------------------------- 1 | # ngImgCrop - manually built version of https://github.com/iblank/ngImgCrop 2 | 3 | This library is a manually built version of https://github.com/iblank/ngImgCrop which itself is a fork of the original 4 | ngImgCrop directive (https://github.com/alexk111/ngImgCrop). 5 | 6 | The difference is that the fork (github.com/iblank/ngImgCrop) allows rectangular (non-square) crop areas, while the 7 | original version (github.com/alexk111/ngImgCrop) supports only square cropping areas. 8 | 9 | Build process: cloned the repo: 10 | 11 | git clone https://github.com/iblank/ngImgCrop 12 | 13 | and then did a "gulp" build. 14 | -------------------------------------------------------------------------------- /src/js/app/appHooks.service.mockImpl.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.hooks') 5 | 6 | // 7 | // 'Mock' implementation of the AppHooks service'. 8 | // 9 | 10 | .factory('AppHooksMockImpl', function () { 11 | 12 | // Generic hook function, called when the user is "loaded" (after login) or "unloaded". If more stuff needs to be 13 | // loaded/unloaded or other things need to be done after logging in or out then you can place them here. 14 | var loadUnloadUser = function (userService, user, load) { 15 | // nothing here, at the moment 16 | }; 17 | 18 | return { 19 | loadUnloadUser: loadUnloadUser 20 | }; 21 | }) 22 | ; 23 | }()); 24 | -------------------------------------------------------------------------------- /src/js/app/mainPage/dash.ctrl.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | var DashCtrl = /*@ngInject*/function (Application, $scope) { 5 | // vm: the "Controller as vm" convention from: http://www.johnpapa.net/angularjss-controller-as-and-the-vm-variable/ 6 | var vm = this; 7 | var log = Application.getLogger('MainPageDashCtrl'); 8 | 9 | $scope.$on('$ionicView.beforeEnter', function (event, viewData) { 10 | log.debug("beforeEnter"); 11 | 12 | // do stuff here which you want to do anytime you switch to the tab managed by this controller 13 | 14 | log.debug("beforeEnter end"); 15 | }); 16 | 17 | }; 18 | 19 | appModule('app.mainPage').controller('DashCtrl', DashCtrl); 20 | }()); 21 | -------------------------------------------------------------------------------- /src/js/app/appHooks.service.firebasekImpl.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.hooks') 5 | 6 | // 7 | // Firebase implementation of the AppHooks service'. 8 | // 9 | 10 | .factory('AppHooksFirebaseImpl', function (fbutil) { 11 | 12 | // Generic hook function, called when the user is "loaded" (after login) or "unloaded". If more stuff needs to be 13 | // loaded/unloaded or other things need to be done after logging in or out then you can place them here. 14 | var loadUnloadUser = function (userService, user, load) { 15 | // nothing here, at the moment 16 | }; 17 | 18 | return { 19 | loadUnloadUser: loadUnloadUser 20 | }; 21 | }) 22 | ; 23 | }()); 24 | -------------------------------------------------------------------------------- /src/js/app/intro/nav-buttons.html: -------------------------------------------------------------------------------- 1 |
2 | 6 | 7 | 10 | 11 | 15 |
16 | -------------------------------------------------------------------------------- /scss/app/_util.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Title bars, nav bars 3 | */ 4 | 5 | .brand-primary, a.brand-primary { 6 | color: $brand-primary; 7 | } 8 | .primary-bg { 9 | background-color: $brand-primary; 10 | } 11 | .primary-border { 12 | border-color: $button-primary-border; 13 | } 14 | 15 | .brand-secondary, a.brand-secondary { 16 | color: $brand-secondary; 17 | } 18 | .secondary-bg { 19 | background-color: $brand-secondary; 20 | } 21 | .secondary-border { 22 | border-color: $button-secondary-border; 23 | } 24 | 25 | .margin-left-5 { 26 | margin-left: 5px; 27 | } 28 | .margin-right-5 { 29 | margin-right: 5px; 30 | } 31 | 32 | .margin-top-15 { 33 | margin-top: 15px; 34 | } 35 | .margin-bottom-15 { 36 | margin-bottom: 15px; 37 | } -------------------------------------------------------------------------------- /src/js/app/appHooks.service.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | // 5 | // A service exposing methods which function as app-specific "hooks" (callbacks) to customize the behavior of generic 6 | // services for that app. The "hook" methods are called from generic services. 7 | // 8 | 9 | appModule('app.hooks') 10 | 11 | // Conditional DI, technique taken from: 12 | // http://phonegap-tips.com/articles/conditional-dependency-injection-with-angularjs.html 13 | 14 | .factory('AppHooks', function ($injector, APP) { 15 | if (APP.devMode) { 16 | return $injector.get('AppHooksMockImpl'); 17 | } else { 18 | // Firebase implementation 19 | return $injector.get('AppHooksFirebaseImpl'); 20 | } 21 | }); 22 | ; 23 | }()); 24 | -------------------------------------------------------------------------------- /src/js/app/mainPage/chats.ctrl.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | var ChatsCtrl = /*@ngInject*/function (Application, $scope, Chats) { 5 | 6 | // vm: the "Controller as vm" convention from: http://www.johnpapa.net/angularjss-controller-as-and-the-vm-variable/ 7 | var vm = this; 8 | var log = Application.getLogger('MainPageChatsCtrl'); 9 | 10 | $scope.$on('$ionicView.beforeEnter', function (event, viewData) { 11 | log.debug("beforeEnter"); 12 | 13 | // do stuff here which you want to do anytime you switch to the tab managed by this controller 14 | vm.chats = Chats.all(); 15 | 16 | log.debug("beforeEnter end"); 17 | }); 18 | 19 | vm.remove = function (chat) { 20 | Chats.remove(chat); 21 | }; 22 | }; 23 | 24 | appModule('app.mainPage').controller('ChatsCtrl', ChatsCtrl); 25 | }()); 26 | -------------------------------------------------------------------------------- /test/unit/mainPage/services/chats.service-spec.js: -------------------------------------------------------------------------------- 1 | describe('Chats', function() { 2 | var Chats; 3 | 4 | beforeEach(module('ui.router')); 5 | beforeEach(module('app.mainPage')); 6 | 7 | beforeEach(inject(function (_Chats_) { 8 | Chats = _Chats_; 9 | })); 10 | 11 | it('can get an instance of my factory', function() { 12 | expect(Chats).toBeDefined(); 13 | }); 14 | 15 | it('has 5 chats', function() { 16 | expect(Chats.all().length).toEqual(5); 17 | }); 18 | 19 | it('has Max as friend with id 1', function() { 20 | var oneFriend = { 21 | id: 1, 22 | name: 'Max Lynx', 23 | notes: 'Odd obsession with everything', 24 | face: 'https://avatars3.githubusercontent.com/u/11214?v=3&s=460' 25 | }; 26 | 27 | expect(Chats.get(1).name).toEqual(oneFriend.name); 28 | }); 29 | }); -------------------------------------------------------------------------------- /src/js/app/util/services/local/storage.service.localImpl.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | // 5 | // Wrapper service for local storage. 6 | // 7 | // This could be overridden/reimplemented to use another storage mechanism e.g. SQLite or PouchDB. 8 | // 9 | 10 | appModule('app.util') 11 | 12 | .factory('StorageServiceLocalImpl', function ($window) { 13 | return { 14 | set: function (key, value) { 15 | $window.localStorage[key] = value; 16 | }, 17 | get: function (key, defaultValue) { 18 | return $window.localStorage[key] || defaultValue; 19 | }, 20 | setObject: function (key, value) { 21 | $window.localStorage[key] = JSON.stringify(value); 22 | }, 23 | getObject: function (key) { 24 | return JSON.parse($window.localStorage[key] || '{}'); 25 | } 26 | }; 27 | }) 28 | ; 29 | }()); 30 | -------------------------------------------------------------------------------- /src/js/app/mainPage/tabs.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/unit/mainPage/account.ctrl-spec.js: -------------------------------------------------------------------------------- 1 | describe('AccountCtrl', function() { 2 | var $controller; 3 | 4 | beforeEach(module('ui.router')); 5 | beforeEach(module('app.mainPage')); 6 | 7 | beforeEach(inject(function($rootScope, $log, _$controller_) { 8 | var $scope = $rootScope.$new(); 9 | 10 | var ApplicationServiceMock = { 11 | getLogger: function() { 12 | return $log; 13 | } 14 | }; 15 | 16 | var UserServiceMock = { 17 | currentUser: function() { 18 | return null; 19 | } 20 | }; 21 | 22 | $controller = _$controller_('AccountCtrl', { 23 | '$scope': $scope, 24 | 'Application': ApplicationServiceMock, 25 | 'UserService': UserServiceMock 26 | }); 27 | })); 28 | 29 | it('should have enabled friends to be true', function(){ 30 | expect($controller.settings.enableFriends).toEqual(true); 31 | }); 32 | }); -------------------------------------------------------------------------------- /src/js/lib/ng-img-crop-customized/ng-img-crop.scss: -------------------------------------------------------------------------------- 1 | @mixin inline-block { 2 | display: -moz-inline-stack; // ff 2 3 | display: inline-block; 4 | zoom:1; *display: inline; _height: 15px; // ie 6-7 5 | } 6 | img-crop { 7 | width:100%; 8 | height:100%; 9 | display:block; 10 | position:relative; 11 | overflow:hidden; 12 | 13 | canvas { 14 | display:block; 15 | position:absolute; 16 | top:50%; 17 | left:50%; 18 | 19 | // Disable Outline 20 | outline: none; 21 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); /* mobile webkit */ 22 | } 23 | } 24 | div[img-crop-result] { 25 | position: relative; 26 | width: 200px; /* this is overwritten by element.style changes */ 27 | @include inline-block; 28 | 29 | .imgCropResultContainer { 30 | position: relative; 31 | width: 100%; 32 | padding-top: 0; 33 | overflow: hidden; 34 | 35 | img { 36 | position: absolute; 37 | } 38 | 39 | } 40 | } -------------------------------------------------------------------------------- /scss/app/_tabs.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Tab styles 3 | */ 4 | 5 | .tabs-brand > .tabs, 6 | .tabs.tabs-brand { 7 | @include tab-style($tabs-brand-bg, $tabs-brand-border, $tabs-brand-text); 8 | @include tab-badge-style($tabs-brand-text, $tabs-brand-bg); 9 | } 10 | 11 | .tabs-striped { 12 | @include tabs-striped('tabs-brand', $light, $tabs-brand); 13 | @include tabs-striped-background('tabs-background-brand', $tabs-brand); 14 | @include tabs-striped-color('tabs-color-brand', $tabs-brand); 15 | } 16 | 17 | @include tabs-background('tabs-background-brand', $tabs-brand, $bar-brand-border); 18 | 19 | @include tabs-color('tabs-color-brand', $tabs-brand); 20 | 21 | ion-tabs { 22 | @include tabs-standard-color('tabs-color-active-brand', $tabs-brand, $dark); 23 | } 24 | 25 | .tab-item.tab-item-active, 26 | .tab-item.active, 27 | .tab-item.activated { 28 | opacity: 1; 29 | 30 | &.tab-item-brand { 31 | color: $tabs-brand; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/e2e/specs/auth/login.spec.js: -------------------------------------------------------------------------------- 1 | // 2 | // Test the login/logout experience 3 | // 4 | 'use strict'; 5 | 6 | describe('Log in and log out', function () { 7 | 8 | var loginHelper = require('../../common/login.helper.js'); 9 | 10 | it('when logged in it should be able to log out and then to login again', function () { 11 | loginHelper.ensureLoggedIn(); 12 | 13 | loginHelper.logoutAndGotoLoginPage(); 14 | 15 | // perform a successful login 16 | loginHelper.login(); 17 | loginHelper.checkSuccessfulLogin(); 18 | }); 19 | 20 | it('try login with incorrect credentials', function () { 21 | loginHelper.ensureLoggedIn(); 22 | 23 | loginHelper.logoutAndGotoLoginPage(); 24 | 25 | // check that login with incorrect credentials fails 26 | 27 | loginHelper.login({ 28 | username: 'wronguser@gmail.com', 29 | password: 'wrongpassword' 30 | }); 31 | 32 | loginHelper.checkFailedLogin(); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /scss/app/_button.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Buttons 3 | */ 4 | 5 | .button { 6 | &.button-primary { 7 | @include button-style($button-primary-bg, $button-primary-border, $button-primary-active-bg, $button-primary-active-border, $button-primary-text); 8 | @include button-clear($button-primary-bg); 9 | @include button-outline($button-primary-bg); 10 | } 11 | &.button-secondary { 12 | @include button-style($button-secondary-bg, $button-secondary-border, $button-secondary-active-bg, $button-secondary-active-border, $button-secondary-text); 13 | @include button-clear($button-secondary-bg); 14 | @include button-outline($button-secondary-bg); 15 | } 16 | &.button-neutral { 17 | @include button-style($button-neutral-bg, $button-neutral-border, $button-neutral-active-bg, $button-neutral-active-border, $button-neutral-text); 18 | @include button-clear($button-neutral-bg); 19 | @include button-outline($button-neutral-bg); 20 | } 21 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "authors": [ 6 | "leob " 7 | ], 8 | "dependencies": { 9 | "angular-animate": "1.4.3", 10 | "angular-elastic": "~2.4.2", 11 | "angular-messages": "1.4.3", 12 | "angular-resource": "1.4.3", 13 | "angular-sanitize": "1.4.3", 14 | "angular-translate": "~2.7.2", 15 | "angular-translate-loader-static-files": "~2.7.2", 16 | "angular-ui-router": "0.2.13", 17 | "angularfire": "~1.1.2", 18 | "firebase": "~2.2.9", 19 | "ionic-content-banner": "~1.0.1", 20 | "ngCordova": "0.1.27-alpha", 21 | "fus-messages": "~0.0.2", 22 | "jsSHA": "1.6.0", 23 | "ng-cordova-oauth": "^0.2.10" 24 | }, 25 | "devDependencies": { 26 | "ionic": "driftyco/ionic-bower#1.3.2", 27 | "angular-mocks": "1.5.0" 28 | }, 29 | "resolutions": { 30 | "angular": "1.5.3", 31 | "angular-animate": "1.5.3", 32 | "angular-sanitize": "1.5.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/js/app/mainPage/chats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 |

{{chat.name}}

16 |

{{chat.lastText}}

17 | 18 | 19 | 20 | Delete 21 | 22 |
23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /src/js/app/mainPage/dash.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 |
9 |
Recent Updates
10 |
11 |
12 | There is a fire in sector 3 13 |
14 |
15 |
16 |
17 |
Health
18 |
19 |
20 | You ate an apple today! 21 |
22 |
23 |
24 |
25 |
Upcoming
26 |
27 |
28 | You have 29 meetings on your calendar tomorrow. 29 |
30 |
31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /bin/protractor-chromedriver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # http://stackoverflow.com/questions/31662828/how-to-access-chromedriver-logs-for-protractor-test/31840996#31840996 3 | NODE_MODULES="$(dirname $0)/../node_modules" 4 | # PATH TO CHROME DRIVER CHANGED - see: http://stackoverflow.com/a/39897937/2474068 5 | #CHROMEDRIVER="${NODE_MODULES}/protractor/selenium/chromedriver" 6 | CHROMEDRIVER="${NODE_MODULES}/protractor/node_modules/webdriver-manager/selenium/chromedriver_2.25" 7 | 8 | #LOGFILE="_chromedriver.$$.log" 9 | #LOG="_logs/$LOGFILE" 10 | #LAST="_logs/_chromedriver.log" 11 | #ln -s $LOGFILE $LAST 12 | 13 | LOG="_logs/_chromedriver.log" 14 | 15 | fatal() { 16 | # Dump to stderr because that seems reasonable 17 | echo >&2 "$0: ERROR: $*" 18 | # Dump to a logfile because webdriver redirects stderr to /dev/null (?!) 19 | echo >"${LOG}" "$0: ERROR: $*" 20 | exit 11 21 | } 22 | 23 | [ ! -x "$CHROMEDRIVER" ] && fatal "Cannot find chromedriver: $CHROMEDRIVER" 24 | 25 | #exec "${CHROMEDRIVER}" --verbose --log-path="${LOG}" "$@" 26 | exec "${CHROMEDRIVER}" --log-path="${LOG}" "$@" 27 | -------------------------------------------------------------------------------- /src/js/app/util/directives/itemEnteredIndicator.directive.js: -------------------------------------------------------------------------------- 1 | /*jshint sub:true*/ 2 | ;(function() { 3 | "use strict"; 4 | 5 | // 6 | // A directive that displays an indicator to alert the user to the fact that a certain item has not ben entered yet. 7 | // 8 | 9 | appModule('app.util').directive('itemEnteredIndicator', function () { 10 | return { 11 | restrict: 'E', 12 | replace: false, 13 | template: 'prompt.not-entered-yet', 14 | scope: true, 15 | link: function (scope, element, attrs, ctrls, transclude) { 16 | scope.isVisible = false; 17 | 18 | var unwatch = scope.$watchCollection(showMessage, toggleVisible); 19 | 20 | scope.$on('$destroy', function () { 21 | unwatch(); 22 | }); 23 | 24 | function showMessage () { 25 | return scope.$eval(attrs['field']) === false; 26 | } 27 | 28 | function toggleVisible(isVisible) { 29 | scope.isVisible = isVisible; 30 | } 31 | } 32 | }; 33 | }); 34 | 35 | }()); -------------------------------------------------------------------------------- /src/js/app/application.ctrl.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | angular.module('app') 5 | 6 | // 7 | // This is the global top-level parent controller specified on the "body" tag in index.html, it wraps all other 8 | // controllers and can be used e.g. for shared callbacks/event handlers on any page; for explanation see: 9 | // 10 | // http://www.clearlyinnovative.com/ionic-framework-tabs-go-home-view/ 11 | // 12 | .controller('ApplicationCtrl', function ($state, Application, UserService) { 13 | 14 | // UTILITY FUNCTIONS 15 | 16 | this.logout = function() { 17 | UserService.logoutApp().then(function() { 18 | $state.go('loggedout'); 19 | }); 20 | }; 21 | 22 | this.login = function() { 23 | $state.go('login'); 24 | }; 25 | 26 | this.isLoggedIn = function() { 27 | return Application.isUserLoggedIn(); 28 | }; 29 | 30 | this.goUserProfile = function() { 31 | Application.gotoUserProfilePage($state, true); 32 | }; 33 | 34 | this.goHome = function() { 35 | Application.gotoStartPage($state, false); 36 | }; 37 | 38 | }) 39 | ; 40 | }()); 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 leob https://github.com/leob 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/js/app/util/services/logentries/tracking.service.logentriesImpl.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | // 5 | // Track an event using the Logentries service. 6 | // 7 | // NOTE: for this to work, edit the "index-template.html" file, uncomment the following 4 lines, and substitute 8 | // your Logentries token into the "LE.init" call: 9 | // 10 | // 11 | // 12 | // 13 | // 14 | // 15 | 16 | appModule('app.util') 17 | .factory('TrackingServiceLogentriesImpl', function () { 18 | 19 | var trackEvent = function (logLevel, message) { 20 | // Note: object "LE" is defined by logentries/le.min.js 21 | if (logLevel === 'ERROR') { 22 | LE.error(message); 23 | } else if (logLevel === 'WARN') { 24 | LE.warn(message); 25 | } else if (logLevel === 'INFO') { 26 | LE.info(message); 27 | } else { 28 | LE.log(message); 29 | } 30 | }; 31 | 32 | return { 33 | trackEvent: trackEvent 34 | }; 35 | }) 36 | ; 37 | }()); 38 | -------------------------------------------------------------------------------- /test/e2e/pages/login.page.js: -------------------------------------------------------------------------------- 1 | module.exports = (function () { 2 | 'use strict'; 3 | 4 | var loginPath = 'login'; 5 | 6 | var LoginPage = function () { 7 | 8 | this.load = function () { 9 | 10 | // load the login page if it's not already loaded 11 | browser.getLocationAbsUrl().then(function (url) { 12 | if (!url.match('\/' + loginPath + '\/*$')) { 13 | browser.get('/#/' + loginPath); 14 | } 15 | }); 16 | }; 17 | }; 18 | 19 | LoginPage.prototype = Object.create({}, { 20 | url: { 21 | get: function () { 22 | return '/' + loginPath; 23 | } 24 | }, 25 | username: { 26 | get: function () { 27 | return element(by.name('email')); 28 | } 29 | }, 30 | setUsername: { 31 | value: function (keys) { 32 | return this.username.sendKeys(keys); 33 | } 34 | }, 35 | setPassword: { 36 | value: function (keys) { 37 | return element(by.name('password')).sendKeys(keys); 38 | } 39 | }, 40 | login: { 41 | value: function () { 42 | element(by.name('login')).click(); 43 | } 44 | } 45 | }); 46 | 47 | return LoginPage; 48 | }()); -------------------------------------------------------------------------------- /scss/app/_bar.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Title bars, nav bars 3 | */ 4 | 5 | .bar { 6 | &.bar-primary { 7 | @include bar-style($bar-primary-bg, $bar-primary-border, $bar-primary-text); 8 | &.bar-footer{ 9 | background-image: linear-gradient(180deg, $bar-primary-border, $bar-primary-border 50%, transparent 50%); 10 | } 11 | } 12 | } 13 | 14 | .bar-primary { 15 | .button { 16 | @include button-style($bar-primary-bg, $bar-primary-border, $bar-primary-active-bg, $bar-primary-active-border, $bar-primary-text); 17 | @include button-clear(#fff, $bar-title-font-size); 18 | } 19 | } 20 | 21 | .bar { 22 | &.bar-menu { 23 | @include bar-style($bar-secondary-bg, $bar-secondary-border, $bar-secondary-text); 24 | &.bar-footer{ 25 | background-image: linear-gradient(180deg, $bar-secondary-border, $bar-secondary-border 50%, transparent 50%); 26 | } 27 | } 28 | } 29 | 30 | .bar-menu { 31 | .button { 32 | @include button-style($bar-secondary-bg, $bar-secondary-border, $bar-secondary-active-bg, $bar-secondary-active-border, $bar-secondary-text); 33 | @include button-clear(#fff, $bar-title-font-size); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /scss/app/_layout.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Layout 3 | */ 4 | 5 | $logo-header-height: 120px; 6 | 7 | .logo-header { 8 | padding-top: 20px; 9 | height: $logo-header-height; 10 | } 11 | 12 | // http://forum.ionicframework.com/t/vertically-center-a-button/2040/2 13 | .vertical-center { 14 | display: -webkit-box; 15 | display: -moz-box; 16 | display: -ms-flexbox; 17 | display: -webkit-flex; 18 | display: flex; 19 | -webkit-box-direction: normal; 20 | -moz-box-direction: normal; 21 | -webkit-box-orient: horizontal; 22 | -moz-box-orient: horizontal; 23 | -webkit-flex-direction: row; 24 | -ms-flex-direction: row; 25 | flex-direction: row; 26 | -webkit-flex-wrap: nowrap; 27 | -ms-flex-wrap: nowrap; 28 | flex-wrap: nowrap; 29 | -webkit-box-pack: center; 30 | -moz-box-pack: center; 31 | -webkit-justify-content: center; 32 | -ms-flex-pack: center; 33 | justify-content: center; 34 | -webkit-align-content: stretch; 35 | -ms-flex-line-pack: stretch; 36 | align-content: stretch; 37 | -webkit-box-align: center; 38 | -moz-box-align: center; 39 | -webkit-align-items: center; 40 | -ms-flex-align: center; 41 | align-items: center; 42 | } 43 | -------------------------------------------------------------------------------- /scss/ionic.app.scss: -------------------------------------------------------------------------------- 1 | /* 2 | To customize the look and feel of Ionic, you can override the variables 3 | in ionic's _variables.scss file. 4 | 5 | For example, you might change some of the default colors: 6 | 7 | $light: #fff !default; 8 | $stable: #f8f8f8 !default; 9 | $positive: #387ef5 !default; 10 | $calm: #11c1f3 !default; 11 | $balanced: #33cd5f !default; 12 | $energized: #ffc900 !default; 13 | $assertive: #ef473a !default; 14 | $royal: #886aea !default; 15 | $dark: #444 !default; 16 | */ 17 | 18 | // Import app variables (NEEDS TO GO BEFORE THE IONIC IMPORT) 19 | @import "scss/app/variables"; 20 | 21 | // The path for our ionicons font files, relative to the built CSS in www/css 22 | $ionicons-font-path: "../lib/ionic/fonts" !default; 23 | 24 | // Include all of Ionic - NOTE: we include a CUSTOMIZED version of the Ionic master SCSS file here. See comments inside 25 | // the "ionic-customized.scss" file for more info. 26 | @import "ionic-customized"; 27 | 28 | // Import app styles 29 | @import "scss/app/app"; -------------------------------------------------------------------------------- /src/js/modules.js: -------------------------------------------------------------------------------- 1 | // Use this variable to maintain a list of registered modules (see 'module' function below) 2 | var modules = {}; 3 | 4 | // 5 | // This function is a simple wrapper around "angular.module(...)". 6 | // 7 | // It can replace both angular,module(..., []) AND angular.module(...) (the variants with and without a list of 8 | // dependencies), so it can be used both to register/create a module AND to use a module; 9 | // 10 | // it will figure out automatically if the module was already registered and call the right 'angular.module()' 11 | // variant (with or without a dependency array). 12 | // 13 | // This circumvents the problem where a module is defined in 2 separate Javascript source files, and the module is 14 | // created in 1 file and used in the others. You then have to load the Javascript files in the right order, or it 15 | // won't work. Using the 'module' function circumvents this. 16 | // 17 | // NOTE: practically speaking this is only useful if you don't have to declare dependencies for the module. 18 | // 19 | var appModule = function(moduleName, deps) { 20 | var mod = modules[moduleName]; 21 | 22 | if (!mod) { 23 | mod = angular.module(moduleName, deps || []); 24 | modules[moduleName] = mod; 25 | } 26 | 27 | return mod; 28 | }; 29 | -------------------------------------------------------------------------------- /src/js/app/mainPage/account.ctrl.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | var AccountCtrl = /*@ngInject*/function ($scope, Application, UserService) { 5 | 6 | // vm: the "Controller as vm" convention from: http://www.johnpapa.net/angularjss-controller-as-and-the-vm-variable/ 7 | var vm = this; 8 | var log = Application.getLogger('AccountCtrl'); 9 | var user; 10 | 11 | $scope.$on('$ionicView.beforeEnter', function (event, viewData) { 12 | log.debug("beforeEnter start ..."); 13 | 14 | user = UserService.currentUser(); 15 | 16 | log.debug("beforeEnter end"); 17 | }); 18 | 19 | vm.settings = { 20 | enableFriends: true 21 | }; 22 | 23 | function formatNumber(num) { 24 | return ("00" + num).substr(-2, 2); 25 | } 26 | 27 | vm.loginDuration = function () { 28 | 29 | // call the model object's "getLoggedInDuration" method 30 | var duration = user.getLoggedInDuration() / 1000; 31 | 32 | var seconds = parseInt(duration % 60), 33 | minutes = parseInt(duration / 60 % 60), 34 | hours = parseInt(duration / (60*60)); 35 | 36 | return formatNumber(hours) + ':' + formatNumber(minutes) + ':' + formatNumber(seconds); 37 | }; 38 | }; 39 | 40 | appModule('app.mainPage').controller('AccountCtrl', AccountCtrl); 41 | }()); 42 | -------------------------------------------------------------------------------- /scss/app/_styles.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Various custom styles 3 | */ 4 | 5 | body { 6 | } 7 | 8 | /* Image cropping https://github.com/iblank/ngImgCrop */ 9 | .image-crop { 10 | display: block; 11 | width: 100%; 12 | height: 200px; 13 | text-align: center; 14 | -webkit-box-flex: 1; 15 | -webkit-flex: 1; 16 | -moz-box-flex: 1; 17 | -moz-flex: 1; 18 | -ms-flex: 1; 19 | flex: 1; 20 | padding: 5px; 21 | } 22 | 23 | .image-crop-result { 24 | height: 120px; 25 | text-align: center; 26 | -webkit-box-flex: 1; 27 | -webkit-flex: 1; 28 | -moz-box-flex: 1; 29 | -moz-flex: 1; 30 | -ms-flex: 1; 31 | flex: 1; 32 | display: block; 33 | padding: 5px; 34 | width: 100%; 35 | } 36 | 37 | .image-crop-result img { 38 | border-radius: 3px; 39 | margin: 0; 40 | position: absolute; 41 | top: 50%; 42 | margin-right: -50%; 43 | transform: translate(-50%, -50%) 44 | } 45 | 46 | .image-crop-row { 47 | display: flex; 48 | padding: 5px; 49 | width: 100%; 50 | } 51 | /* ==== IONIC FIXES ==== */ 52 | 53 | /* 54 | CSS styles to eliminate the annoying flickering header when changing tabs in an Ionic app on Android: 55 | http://forum.ionicframework.com/t/flickering-when-navigating-via-tabs-on-android/27281/2 56 | */ 57 | .platform-android .header-item.title { 58 | transition-duration: 0ms; 59 | } 60 | .platform-android .header-item.buttons { 61 | transition-duration: 0ms; 62 | } -------------------------------------------------------------------------------- /src/js/app/intro/intro.ctrl.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | var IntroCtrl = /*@ngInject*/function ($scope, $state, $ionicSlideBoxDelegate, $ionicScrollDelegate, Application) { 5 | // vm: the "Controller as vm" convention from: http://www.johnpapa.net/angularjss-controller-as-and-the-vm-variable/ 6 | var vm = this; 7 | 8 | // when entering the view, always go to the first slide (instead of just showing whichever slide was shown last when 9 | // you left the view) 10 | $scope.$on('$ionicView.beforeEnter', function () { 11 | vm.slideIndex = 0; 12 | $ionicSlideBoxDelegate.slide(0); 13 | $ionicScrollDelegate.scrollTop(); 14 | }); 15 | 16 | vm.startApp = function () { 17 | // user has viewed (all or part of) the intro, set 'initialRun' to false so that next time when opening the app the 18 | // intro doesn't show up again automatically (it can always be opened manually from the menu) 19 | Application.setInitialRun(false); 20 | 21 | // go to the start page (after 'initialRun' has been set to false) 22 | Application.gotoStartPage($state); 23 | }; 24 | vm.next = function () { 25 | $ionicSlideBoxDelegate.next(); 26 | $ionicScrollDelegate.scrollTop(); 27 | }; 28 | vm.previous = function () { 29 | $ionicSlideBoxDelegate.previous(); 30 | $ionicScrollDelegate.scrollTop(); 31 | }; 32 | 33 | vm.slideChanged = function (index) { 34 | vm.slideIndex = index; 35 | }; 36 | }; 37 | 38 | appModule('app.intro').controller('IntroCtrl', IntroCtrl); 39 | }()); 40 | -------------------------------------------------------------------------------- /src/js/app/manage/image-crop-modal.html: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /src/js/app/auth/forgotPassword/forgotPassword.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 |
13 | auth.forgot-password-title 14 |
15 | 16 | 17 | 18 | 25 | 26 |
27 |
28 |
29 | 30 | 31 |
32 | 33 |
34 | 35 | 38 | 39 | link.back 40 |
41 | 42 |
43 | 44 |
45 |
-------------------------------------------------------------------------------- /src/js/app/mainPage/mainPage.router.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.mainPage') 5 | .config(function ($stateProvider) { 6 | $stateProvider 7 | .state('app.auth.main', { 8 | url: "/main", 9 | abstract: true, 10 | views: { 11 | 'menuContent@app': { 12 | templateUrl: "js/app/mainPage/tabs.html" 13 | } 14 | } 15 | }) 16 | // Note: each tab has its own nav history stack 17 | .state('app.auth.main.dash', { 18 | url: '/dash', 19 | views: { 20 | 'main-dash': { 21 | templateUrl: 'js/app/mainPage/dash.html', 22 | controller: 'DashCtrl as vm' 23 | } 24 | } 25 | }) 26 | .state('app.auth.main.chats', { 27 | url: '/chats', 28 | views: { 29 | 'main-chats': { 30 | templateUrl: 'js/app/mainPage/chats.html', 31 | controller: 'ChatsCtrl as vm' 32 | } 33 | } 34 | }) 35 | .state('app.auth.main.chat-detail', { 36 | url: '/chats/:chatId', 37 | views: { 38 | 'main-chats': { 39 | templateUrl: 'js/app/mainPage/chatDetail.html', 40 | controller: 'ChatDetailCtrl as vm' 41 | } 42 | } 43 | }) 44 | .state('app.auth.main.account', { 45 | url: '/account', 46 | views: { 47 | 'main-account': { 48 | templateUrl: 'js/app/mainPage/account.html', 49 | controller: 'AccountCtrl as vm' 50 | } 51 | } 52 | }); 53 | }) 54 | ; 55 | }()); 56 | -------------------------------------------------------------------------------- /scss/app/_form.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Form styles 3 | */ 4 | 5 | /* ==== FORM VALIDATION ==== */ 6 | 7 | .label-no-border { 8 | border: none; 9 | } 10 | 11 | /* BEGIN inspired by: https://calendee.com/2014/12/26/validation-in-ionic-framework-apps-with-ngmessages/ */ 12 | //.valid-lr { 13 | // border-left: 2px solid green; 14 | // border-right: 2px solid green; 15 | //} 16 | 17 | label.item.has-error { 18 | border-bottom: 1px solid $brand-primary; 19 | } 20 | 21 | // Override the "label.item.has-focus" style: give the field the error style (not the focus style) even if it has focus 22 | label.item.has-focus.has-error { 23 | border-bottom: 1px solid $brand-primary; 24 | } 25 | 26 | // fix/hack/workaround for the first field on a form which uses inline labels and has more than 1 field 27 | label.item.has-focus.first-field { 28 | border-bottom: 2px solid darken(lightgrey,15%); 29 | padding-bottom: 1px; 30 | } 31 | 32 | // Override the "label.item.has-focus" style: give the field the error style (not the focus style) even if it has focus 33 | label.item.has-focus.has-error.first-field { 34 | border-bottom: 1px solid $brand-primary; 35 | } 36 | 37 | // on focus give the field a lightgrey 2 px bottom border; also need a 1px padding to make it visible 38 | label.item.has-focus { 39 | border-bottom: 2px solid darken(lightgrey,15%); 40 | } 41 | 42 | .form-errors { 43 | margin: 5px; 44 | clear: both; 45 | } 46 | 47 | .form-error { 48 | padding: 2px 0 2px 16px; 49 | color: $brand-primary; 50 | } 51 | /* END inspired by: https://calendee.com/2014/12/26/validation-in-ionic-framework-apps-with-ngmessages/ */ 52 | -------------------------------------------------------------------------------- /src/js/app/mainPage/services/chats.service.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.mainPage') 5 | 6 | // COPIED FROM IONIC TABS STARTER APP 7 | .factory('Chats', function() { 8 | // Might use a resource here that returns a JSON array 9 | 10 | // Some fake testing data 11 | var chats = [{ 12 | id: 0, 13 | name: 'Ben Sparrow', 14 | lastText: 'You on your way?', 15 | face: 'https://pbs.twimg.com/profile_images/514549811765211136/9SgAuHeY.png' 16 | }, { 17 | id: 1, 18 | name: 'Max Lynx', 19 | lastText: 'Hey, it\'s me', 20 | face: 'https://avatars3.githubusercontent.com/u/11214?v=3&s=460' 21 | },{ 22 | id: 2, 23 | name: 'Adam Bradleyson', 24 | lastText: 'I should buy a boat', 25 | face: 'https://pbs.twimg.com/profile_images/479090794058379264/84TKj_qa.jpeg' 26 | }, { 27 | id: 3, 28 | name: 'Perry Governor', 29 | lastText: 'Look at my mukluks!', 30 | face: 'https://pbs.twimg.com/profile_images/491995398135767040/ie2Z_V6e.jpeg' 31 | }, { 32 | id: 4, 33 | name: 'Mike Harrington', 34 | lastText: 'This is wicked good ice cream.', 35 | face: 'https://pbs.twimg.com/profile_images/578237281384841216/R3ae1n61.png' 36 | }]; 37 | 38 | return { 39 | all: function() { 40 | return chats; 41 | }, 42 | remove: function(chat) { 43 | chats.splice(chats.indexOf(chat), 1); 44 | }, 45 | get: function(chatId) { 46 | for (var i = 0; i < chats.length; i++) { 47 | if (chats[i].id === parseInt(chatId)) { 48 | return chats[i]; 49 | } 50 | } 51 | return null; 52 | } 53 | }; 54 | }); 55 | }()); 56 | -------------------------------------------------------------------------------- /scss/ionic-customized.scss: -------------------------------------------------------------------------------- 1 | // 2 | // NOTE: 3 | // 4 | // This is a copied and customized version of the master "ionic.scss" file. Purpose is to EXCLUDE the standard/default 5 | // version of _action-sheet.scss so that we can include a customized version (see _action-sheet-customized.scss). 6 | // 7 | 8 | @charset "src/lib/ionic/scss/UTF-8"; 9 | 10 | @import 11 | // Ionicons 12 | "src/lib/ionic/scss/ionicons/ionicons.scss", 13 | 14 | // Variables 15 | "src/lib/ionic/scss/mixins", 16 | "src/lib/ionic/scss/variables", 17 | 18 | // Base 19 | "src/lib/ionic/scss/reset", 20 | "src/lib/ionic/scss/scaffolding", 21 | "src/lib/ionic/scss/type", 22 | 23 | // Components 24 | //"src/lib/ionic/scss/action-sheet", // COMMENTED OUT 25 | "src/lib/ionic/scss/backdrop", 26 | "src/lib/ionic/scss/bar", 27 | "src/lib/ionic/scss/tabs", 28 | "src/lib/ionic/scss/menu", 29 | "src/lib/ionic/scss/modal", 30 | "src/lib/ionic/scss/popover", 31 | "src/lib/ionic/scss/popup", 32 | "src/lib/ionic/scss/loading", 33 | "src/lib/ionic/scss/items", 34 | "src/lib/ionic/scss/list", 35 | "src/lib/ionic/scss/badge", 36 | "src/lib/ionic/scss/slide-box", 37 | "src/lib/ionic/scss/refresher", 38 | "src/lib/ionic/scss/spinner", 39 | 40 | // Forms 41 | "src/lib/ionic/scss/form", 42 | "src/lib/ionic/scss/checkbox", 43 | "src/lib/ionic/scss/toggle", 44 | "src/lib/ionic/scss/radio", 45 | "src/lib/ionic/scss/range", 46 | "src/lib/ionic/scss/select", 47 | "src/lib/ionic/scss/progress", 48 | 49 | // Buttons 50 | "src/lib/ionic/scss/button", 51 | "src/lib/ionic/scss/button-bar", 52 | 53 | // Util 54 | "src/lib/ionic/scss/grid", 55 | "src/lib/ionic/scss/util", 56 | "src/lib/ionic/scss/platform", 57 | 58 | // Animations 59 | "src/lib/ionic/scss/animations", 60 | "src/lib/ionic/scss/transitions"; 61 | -------------------------------------------------------------------------------- /src/js/app/user/models/user.model.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.user') 5 | 6 | // 7 | // A simple model object. 8 | // 9 | // Inspired by: https://medium.com/opinionated-angularjs/angular-model-objects-with-javascript-classes-2e6a067c73bc 10 | // 11 | 12 | .factory('User', function () { 13 | 14 | // Constructor, with class name 15 | function User(provider, userName, createdAt, id) { 16 | // Public properties, assigned to the instance ('this') 17 | this.provider = provider; 18 | this.userName = userName; 19 | this.createdAt = createdAt; 20 | this.id = id; 21 | // user type is initially null (not determined) 22 | this.userRole = null; 23 | } 24 | 25 | /** 26 | * Public method, assigned to prototype 27 | */ 28 | User.prototype.getLoggedInDuration = function () { 29 | return (new Date()).getTime() - this.createdAt.getTime(); 30 | }; 31 | 32 | User.prototype.getUserRole = function () { 33 | return this.userRole; 34 | }; 35 | 36 | User.prototype.setUserRole = function (value) { 37 | this.userRole = value; 38 | }; 39 | 40 | User.prototype.isAdminUser = function () { 41 | return this.getUserRole() === 'admin'; 42 | }; 43 | 44 | // Static method, assigned to class; instance ('this') is not available in static context 45 | User.build = function (data) { 46 | if (!data) { 47 | return null; 48 | } 49 | 50 | return new User( 51 | data.provider, 52 | data.userName, 53 | new Date(), 54 | data.id 55 | ); 56 | }; 57 | 58 | /** 59 | * Return the constructor function ('class') 60 | */ 61 | return User; 62 | }) 63 | 64 | ; 65 | }()); 66 | -------------------------------------------------------------------------------- /src/js/app/auth/forgotPassword/forgotPassword.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | var ForgotPasswordCtrl = /*@ngInject*/function ($scope, $state, $translate, Application, UserService) { 5 | // vm: the "Controller as vm" convention from: http://www.johnpapa.net/angularjss-controller-as-and-the-vm-variable/ 6 | var vm = this; 7 | var log = Application.getLogger('ForgotPasswordCtrl'); 8 | 9 | $scope.$on('$ionicView.beforeEnter', function () { 10 | Application.resetForm(vm); 11 | 12 | vm.user = { 13 | email: Application.getEmail() 14 | }; 15 | }); 16 | 17 | vm.reset = function (form) { 18 | if (!form.$valid) { 19 | return; 20 | } 21 | 22 | Application.showLoading(true); 23 | 24 | UserService.resetPassword(('' + vm.user.email).toLowerCase()).then(function () { 25 | Application.hideLoading(); 26 | 27 | log.info("Password reset successfully"); 28 | 29 | // go to the change-password page, displaying a message asking the user to verify their email 30 | Application.setState('mode', 'reset-password'); 31 | $state.go('changePassword', {mode: 'reset-password'}); 32 | }) 33 | .catch(function (error) { 34 | Application.hideLoading(); 35 | 36 | if (error === "invalid_email") { 37 | Application.errorMessage(vm, 'message.not-registered'); 38 | } else { 39 | Application.errorMessage(vm, 'message.unknown-error'); 40 | } 41 | }); 42 | }; 43 | 44 | vm.login = function () { 45 | $state.go('login'); 46 | }; 47 | 48 | }; 49 | 50 | // controller and router 51 | appModule('app.auth.forgotPassword') 52 | .controller('ForgotPasswordCtrl', ForgotPasswordCtrl) 53 | .config(function ($stateProvider) { 54 | $stateProvider 55 | .state('forgotPassword', { 56 | url: '/forgotPassword', 57 | templateUrl: 'js/app/auth/forgotPassword/forgotPassword.html', 58 | controller: 'ForgotPasswordCtrl as vm' 59 | }); 60 | }) 61 | ; 62 | }()); 63 | -------------------------------------------------------------------------------- /test/e2e/common/utils.js: -------------------------------------------------------------------------------- 1 | module.exports = (function () { 2 | 'use strict'; 3 | 4 | var EC = protractor.ExpectedConditions; 5 | 6 | var WAIT_FOR_LOADER_TO_APPEAR = 1000; 7 | var WAIT_FOR_LOADER_TO_DISAPPEAR = 25000; 8 | var WAIT_FOR_PAGE = 8000; 9 | 10 | function waitForLoaderToDisappear(opts) { 11 | 12 | // wait for the loader to disappear 13 | browser.wait(EC.not(EC.presenceOf(element(by.css('.loading-container.visible.active')))), 14 | opts.waitForLoaderToDisappear || WAIT_FOR_LOADER_TO_DISAPPEAR).then(function () { 15 | 16 | expect(element(by.css('.loading-container.visible.active')).isPresent()).toBeFalsy('Loader hidden'); 17 | }); 18 | } 19 | 20 | var waitForLoader = function (options) { 21 | var opts = options || {}; 22 | 23 | browser.wait(EC.presenceOf(element(by.css('.loading-container.visible.active'))), 24 | opts.waitForLoaderToAppear || WAIT_FOR_LOADER_TO_APPEAR).then( 25 | 26 | // Handle both the success and error conditions with the same call to "loginDone()", see explanation here: 27 | // http://stackoverflow.com/questions/34740129/protractor-wait-on-condition-should-not-fail-after-timeout 28 | function () { // SUCCESS CALLBACK 29 | expect(element(by.css('.loading-container.visible.active')).isPresent()).toBeTruthy('Loader shown'); 30 | 31 | waitForLoaderToDisappear(opts); 32 | }, 33 | function () { // ERROR CALLBACK 34 | waitForLoaderToDisappear(opts); 35 | }); 36 | }; 37 | 38 | var waitForPage = function (pageUrl, message, timeout) { 39 | browser.driver.wait(function () { 40 | return browser.driver.getCurrentUrl().then(function (url) { 41 | return new RegExp(pageUrl).test(url); 42 | }); 43 | }, timeout || WAIT_FOR_PAGE).then(function () { 44 | expect(browser.getLocationAbsUrl()).toContain(pageUrl, message); 45 | }); 46 | }; 47 | 48 | return { 49 | waitForLoader: waitForLoader, 50 | waitForPage: waitForPage 51 | }; 52 | }()); 53 | -------------------------------------------------------------------------------- /src/js/app/menu/menu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | menu.home 25 | 26 | 27 | menu.intro 28 | 29 | 33 | menu.user-profile 34 | 35 | 36 | menu.login 37 | 38 | 39 | menu.logout 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/js/app/auth/signup/signup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 |
13 | auth.signup-title 14 |
15 | 16 | 17 | 18 | 29 | 30 |
31 |
32 |
33 | 34 | 35 |
36 | 37 |
38 | 41 | 42 | prompt.have-an-accountlink.login 44 |
45 | 46 |
47 | prompt.login-with-twitter 48 | 50 |
51 |
52 | 53 | 54 |
55 | 56 |
57 |
-------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | module.exports = function(config) { 3 | config.set({ 4 | 5 | // base path that will be used to resolve all patterns (eg. files, exclude) 6 | basePath: '', 7 | 8 | 9 | // frameworks to use 10 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 11 | frameworks: ['jasmine'], 12 | 13 | 14 | // list of files / patterns to load in the browser 15 | files: [ 16 | 'src/lib/ionic/js/ionic.bundle.js', 17 | 'src/lib/angular-mocks/angular-mocks.js', 18 | 'src/lib/ngCordova/dist/ng-cordova.js', 19 | 'src/lib/ngCordova/dist/ng-cordova-mocks.js', 20 | 'src/js/modules.js', 21 | 'src/js/app/app.js', 22 | 'src/js/config/config.js', 23 | 'src/js/**/*.js', 24 | 'test/**/*-spec.js' 25 | ], 26 | 27 | // list of files to exclude 28 | exclude: [ 29 | ], 30 | 31 | 32 | // preprocess matching files before serving them to the browser 33 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 34 | preprocessors: { 35 | }, 36 | 37 | 38 | // test results reporter to use 39 | // possible values: 'dots', 'progress' 40 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 41 | reporters: [/*'progress', */'story'], // comment out 'story' and uncomment 'progress' for concise output 42 | 43 | 44 | // web server port 45 | port: 9876, 46 | 47 | 48 | // enable / disable colors in the output (reporters and logs) 49 | colors: true, 50 | 51 | 52 | // level of logging 53 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 54 | logLevel: config.LOG_INFO, 55 | 56 | 57 | // enable / disable watching file and executing tests whenever any file changes 58 | autoWatch: false, 59 | 60 | 61 | // start these browsers 62 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 63 | browsers: ['Chrome'], 64 | 65 | 66 | // Continuous Integration mode 67 | // if true, Karma captures browsers, runs the tests and exits 68 | singleRun: false 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "app: An Ionic project", 6 | "dependencies": { 7 | "gulp": "^3.5.6", 8 | "gulp-sass": "^2.0.4", 9 | "gulp-concat": "^2.2.0", 10 | "gulp-minify-css": "^0.3.0" 11 | }, 12 | "devDependencies": { 13 | "bower": "^1.3.3", 14 | "del": "^1.2.0", 15 | "gulp-angular-templatecache": "^1.7.0", 16 | "gulp-extend": "^0.2.0", 17 | "gulp-html-replace": "^1.5.1", 18 | "gulp-if": "^1.2.5", 19 | "gulp-imagemin": "^2.3.0", 20 | "gulp-inject": "^1.5.0", 21 | "gulp-jshint": "^1.11.2", 22 | "gulp-minify-html": "^1.0.4", 23 | "gulp-ng-annotate": "^1.0.0", 24 | "gulp-ng-constant": "^0.3.0", 25 | "gulp-rename": "^1.2.2", 26 | "gulp-replace": "^0.5.3", 27 | "gulp-sourcemaps": "^1.5.2", 28 | "gulp-uglify": "^1.2.0", 29 | "gulp-util": "^2.2.14", 30 | "jasmine-core": "^2.3.4", 31 | "jasmine-spec-reporter": "^2.4.0", 32 | "karma": "^0.13.3", 33 | "karma-chrome-launcher": "^0.2.0", 34 | "karma-jasmine": "^0.3.6", 35 | "karma-story-reporter": "^0.3.1", 36 | "protractor": "^4.0.0", 37 | "protractor-fail-fast": "^2.0.0", 38 | "shelljs": "^0.3.0", 39 | "vinyl-paths": "^1.0.0" 40 | }, 41 | "cordovaPlugins": [ 42 | "cordova-plugin-whitelist", 43 | "cordova-plugin-camera", 44 | "cordova-plugin-file", 45 | "cordova-plugin-file-transfer", 46 | { 47 | "locator": "https://github.com/EddyVerbruggen/Toast-PhoneGap-Plugin.git", 48 | "id": "cordova-plugin-x-toast" 49 | }, 50 | "ionic-plugin-keyboard", 51 | "cordova-plugin-device", 52 | "cordova-plugin-console", 53 | "cordova-plugin-splashscreen", 54 | "cordova-plugin-inappbrowser", 55 | "cordova-plugin-statusbar", 56 | "cordova-plugin-splashscreen", 57 | "cordova-plugin-crosswalk-webview@1.6.1" 58 | ], 59 | "scripts": { 60 | "preupdate-webdriver": "npm install", 61 | "update-webdriver": "webdriver-manager update", 62 | "preprotractor": "npm run update-webdriver", 63 | "protractor": "protractor protractor.conf.js" 64 | }, 65 | "cordovaPlatforms": [ 66 | "android", 67 | "ios" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /scss/app/_content-banner.scss: -------------------------------------------------------------------------------- 1 | // Content Banner 2 | 3 | // Variables 4 | //----------------------------------- 5 | 6 | 7 | $content-banner-opacity: 0.95 !default; 8 | $content-banner-error-opacity: 1.0 !default; 9 | 10 | $content-banner-info-bg: $brand-secondary !default; 11 | $content-banner-error-bg: $brand-secondary !default; 12 | 13 | $content-banner-height: 50px !default; 14 | $content-banner-error-height: 30px !default; 15 | 16 | // Styles 17 | //----------------------------------- 18 | 19 | .content-banner { 20 | width: 100%; 21 | color: white; 22 | position: absolute; 23 | top: 0; 24 | 25 | &.error { 26 | background-color: $content-banner-error-bg; 27 | opacity: $content-banner-error-opacity; 28 | height: $content-banner-error-height; 29 | line-height: $content-banner-error-height; 30 | 31 | .content-banner-close { 32 | line-height: $content-banner-error-height; 33 | &:before { 34 | line-height: $content-banner-error-height; 35 | } 36 | } 37 | } 38 | &.info { 39 | background-color: $content-banner-info-bg; 40 | opacity: $content-banner-opacity; 41 | height: $content-banner-height; 42 | line-height: $content-banner-height; 43 | 44 | .content-banner-close { 45 | line-height: $content-banner-height; 46 | &:before { 47 | line-height: $content-banner-height; 48 | } 49 | } 50 | } 51 | .content-banner-text { 52 | @include transition(opacity 500ms linear) ; 53 | position: absolute; 54 | top: 0; 55 | right: ($button-padding * 2) + 5px + 12px; 56 | left: ($button-padding * 2) + 5px + 12px; 57 | text-align: center; 58 | &.active { 59 | opacity: 1; 60 | } 61 | &:not(.active){ 62 | opacity: 0; 63 | } 64 | } 65 | .content-banner-close { 66 | position: absolute; 67 | right: 5px; 68 | top: 0; 69 | padding: 0 $button-padding; 70 | height: 100%; 71 | min-height: 0; 72 | color: white; 73 | } 74 | } 75 | 76 | .content-banner-transition-vertical { 77 | @include transition-transform(linear 250ms); 78 | @include translate3d(0, -100%, 0); 79 | } 80 | 81 | .content-banner-transition-fade { 82 | @include transition(opacity 400ms linear) ; 83 | opacity: 0; 84 | } 85 | 86 | .content-banner-in { 87 | @include translate3d(0, 0, 0); 88 | opacity: $content-banner-opacity; 89 | } -------------------------------------------------------------------------------- /src/js/app/util/services/stopWatch.service.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.util') 5 | 6 | // 7 | // A simple Stopwatch service. 8 | // 9 | // Inspired by: https://medium.com/opinionated-angularjs/angular-model-objects-with-javascript-classes-2e6a067c73bc 10 | // 11 | 12 | .factory('Stopwatch', function () { 13 | 14 | /** 15 | * Constructor, with class name 16 | */ 17 | function Stopwatch(context) { 18 | this.context = context; 19 | this.startAt = 0; // Time of last start / resume. (0 if not running) 20 | this.lapTime = 0; // Time on the clock when last stopped in milliseconds 21 | } 22 | 23 | // Helper functions 24 | 25 | function now() { 26 | return (new Date()).getTime(); 27 | } 28 | 29 | function pad(num, size) { 30 | var s = "0000" + num; 31 | return s.substr(s.length - size); 32 | } 33 | 34 | function formatTime(time) { 35 | var h, m, s; //, ms = 0; 36 | h = m = s = 0; 37 | 38 | h = Math.floor( time / (60 * 60 * 1000) ); 39 | time = time % (60 * 60 * 1000); 40 | m = Math.floor( time / (60 * 1000) ); 41 | time = time % (60 * 1000); 42 | s = Math.floor( time / 1000 ); 43 | //ms = time % 1000; 44 | 45 | return pad(h, 2) + ':' + pad(m, 2) + ':' + pad(s, 2); // + ':' + pad(ms, 3); 46 | } 47 | 48 | // Public methods 49 | 50 | // Start or resume 51 | Stopwatch.prototype.start = function() { 52 | this.startAt = this.startAt || now(); 53 | }; 54 | 55 | // Stop or pause 56 | Stopwatch.prototype.stop = function() { 57 | // If running, update elapsed time otherwise keep it 58 | this.lapTime = this.startAt ? this.lapTime + now() - this.startAt : this.lapTime; 59 | this.startAt = 0; // Paused 60 | }; 61 | 62 | // Reset 63 | Stopwatch.prototype.reset = function() { 64 | this.lapTime = this.startAt = 0; 65 | }; 66 | 67 | // Duration 68 | Stopwatch.prototype.time = function() { 69 | return this.lapTime + (this.startAt ? now() - this.startAt : 0); 70 | }; 71 | 72 | // Duration, formattted 73 | Stopwatch.prototype.fmtTime = function() { 74 | return formatTime(this.time()); 75 | }; 76 | 77 | /** 78 | * Return the constructor function ('class') 79 | */ 80 | return Stopwatch; 81 | }) 82 | 83 | ; 84 | }()); 85 | -------------------------------------------------------------------------------- /src/js/app/auth/login/login.ctrl.js: -------------------------------------------------------------------------------- 1 | ;(function () { 2 | "use strict"; 3 | 4 | var LoginCtrl = /*@ngInject*/function ($scope, $state, $stateParams, Application, UserService) { 5 | var vm = this; 6 | var log = Application.getLogger('LoginCtrl'); 7 | 8 | $scope.$on('$ionicView.beforeEnter', function () { 9 | // enforce/ensure no logged in user at this point 10 | UserService.logoutApp(); 11 | 12 | Application.resetForm(vm); 13 | 14 | vm.user = { 15 | username: null, 16 | password: null 17 | }; 18 | }); 19 | 20 | vm.login = function (form) { 21 | if (!form.$valid) { 22 | return; 23 | } 24 | 25 | Application.showLoading(true); 26 | 27 | UserService.login(('' + vm.user.username).toLowerCase(), vm.user.password).then(function (loggedinUser) { 28 | Application.hideLoading(); 29 | Application.gotoStartPage($state); 30 | }) 31 | .catch(function (error) { 32 | Application.hideLoading(); 33 | 34 | // login failed, check error to see why 35 | if (error == "invalid_credentials") { 36 | Application.errorMessage(vm, 'message.invalid-credentials'); 37 | } else { 38 | Application.errorMessage(vm, 'message.unknown-error'); 39 | } 40 | }); 41 | }; 42 | 43 | vm.loginWithTwitter = function () { 44 | Application.showLoading(true); 45 | 46 | UserService.loginWithTwitter().then(function (loggedinUser) { 47 | Application.hideLoading(); 48 | Application.gotoStartPage($state); 49 | }) 50 | .catch(function (error) { 51 | Application.hideLoading(); 52 | 53 | // login failed, check error to see why 54 | if (error == "invalid_credentials") { 55 | Application.errorMessage(vm, 'message.invalid-credentials'); 56 | } else { 57 | Application.errorMessage(vm, 'message.unknown-error'); 58 | } 59 | }); 60 | }; 61 | 62 | vm.canLoginWithTwitter = function () { 63 | return UserService.canLoginWithTwitter(); 64 | }; 65 | 66 | vm.forgot = function () { 67 | $state.go('forgotPassword'); 68 | }; 69 | 70 | vm.signup = function () { 71 | $state.go('signup'); 72 | }; 73 | 74 | vm.intro = function () { 75 | Application.gotoIntroPage($state); 76 | }; 77 | 78 | }; 79 | 80 | appModule('app.auth.login').controller('LoginCtrl', LoginCtrl); 81 | }()); 82 | -------------------------------------------------------------------------------- /src/js/app/util/directives/formField.directive.js: -------------------------------------------------------------------------------- 1 | /*jshint sub:true*/ 2 | ;(function() { 3 | "use strict"; 4 | 5 | // 6 | // A directive that can be used on an input or textarea element to indicate the form field associated with the input. 7 | // This is then used to highlight the label (using CSS classes) when the field becomes 'valid' or 'invalid'. 8 | // 9 | // The design of this directive was inspired by the fus-messages directive: github.com/fusionalliance/fus-messages 10 | // 11 | 12 | appModule('app.util').directive('formField', function () { 13 | return { 14 | restrict: 'A', 15 | require: ['^form'], 16 | scope: true, 17 | link: function (scope, element, attrs, ctrls, transclude) { 18 | 19 | scope.inputModel = scope.$eval(attrs['formField']); 20 | var unwatch = scope.$watchCollection(getFieldStatus, changeCssClasses); 21 | 22 | var parent = element.parent(); 23 | 24 | element.bind('focus',function () { 25 | parent.addClass('has-focus'); 26 | }).bind('blur', function () { 27 | parent.removeClass('has-focus'); 28 | // clear form errors when a form field gets the focus 29 | }).bind('focus', function () { 30 | var form = getForm(); 31 | 32 | if (form.parent && form.parent.error && Object.keys(form.parent.error).length > 0) { 33 | form.parent.error = {}; 34 | scope.$apply(); 35 | } 36 | }); 37 | 38 | scope.$on('$destroy', function () { 39 | unwatch(); 40 | }); 41 | 42 | function getForm() { 43 | return ctrls[0]; 44 | } 45 | 46 | // get the field status depending on whether the input is dirty or when the form has been submitted, etc. 47 | function getFieldStatus () { 48 | var formController = getForm(); 49 | if (formController.$submitted || scope.inputModel.$dirty) { 50 | if (scope.inputModel.$invalid) { 51 | return 'I'; 52 | } 53 | if (scope.inputModel.$valid) { 54 | return 'V'; 55 | } 56 | } 57 | 58 | return undefined; 59 | } 60 | 61 | function changeCssClasses(state) { 62 | if (state == 'I') { 63 | parent.addClass('has-error'); 64 | parent.removeClass('valid-lr'); 65 | } else if (state == 'V') { 66 | parent.removeClass('has-error'); 67 | parent.addClass('valid-lr'); 68 | } else { 69 | parent.removeClass('has-error'); 70 | parent.removeClass('valid-lr'); 71 | } 72 | } 73 | 74 | } 75 | }; 76 | }); 77 | 78 | }()); -------------------------------------------------------------------------------- /src/js/app/firebase/fbutil.service.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | // 5 | // Utility methods for Firebase. Taken from: 6 | // 7 | // https://github.com/firebase/angularfire-seed/blob/master/app/components/firebase.utils/firebase.utils.js 8 | // 9 | 10 | appModule('app.firebase') 11 | 12 | .factory('fbutil', function ($q, $window, FirebaseConfiguration) { 13 | 14 | function pathRef(args) { 15 | for (var i = 0; i < args.length; i++) { 16 | if (angular.isArray(args[i])) { 17 | args[i] = pathRef(args[i]); 18 | } else if( typeof args[i] !== 'string' ) { 19 | throw new Error('Argument '+i+' to firebaseRef is not a string: '+args[i]); 20 | } 21 | } 22 | return args.join('/'); 23 | } 24 | 25 | /** 26 | * Example: 27 | * 28 | * function(firebaseRef) { 29 | * var ref = firebaseRef('path/to/data'); 30 | * } 31 | * 32 | * 33 | * @function 34 | * @name firebaseRef 35 | * @param {String|Array...} path relative path to the root folder in Firebase instance 36 | * @return a Firebase instance 37 | */ 38 | function ref(path) { 39 | var fbRef = new $window.Firebase(FirebaseConfiguration.url); 40 | var args = Array.prototype.slice.call(arguments); 41 | if( args.length ) { 42 | fbRef = fbRef.child(pathRef(args)); 43 | } 44 | return fbRef; 45 | } 46 | 47 | // Convert a Node.js or Firebase style callback to an Angular style future/promise 48 | var handler = function(fn, context) { 49 | return defer(function(def) { 50 | fn.call(context, function(err, result) { 51 | if( err !== null ) { 52 | def.reject(err); 53 | } else { 54 | def.resolve(result); 55 | } 56 | }); 57 | 58 | }); 59 | }; 60 | 61 | // Convert an alternatve style future/promise, used for some Firebase APIs 62 | var handlerForResult = function(fn, context) { 63 | return defer(function(def) { 64 | fn.call(context, function(result) { 65 | def.resolve(result); 66 | }, function(err) { 67 | def.reject(err); 68 | }); 69 | 70 | }); 71 | }; 72 | 73 | // Abstract the process of creating a future/promise 74 | var defer = function(fn, context) { 75 | var def = $q.defer(); 76 | fn.call(context, def); 77 | return def.promise; 78 | }; 79 | 80 | return { 81 | ref: ref, 82 | defer: defer, 83 | handler: handler, 84 | handlerForResult: handlerForResult 85 | }; 86 | }) 87 | ; 88 | }()); 89 | -------------------------------------------------------------------------------- /src/js/app/image/services/image.service.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.image') 5 | 6 | // 7 | // http://devdactic.com/complete-image-guide-ionic 8 | // https://github.com/iblank/ngImgCrop 9 | // http://stackoverflow.com/questions/6752366/resizing-photo-on-a-canvas-without-losing-the-aspect-ratio 10 | // http://forum.ionicframework.com/t/impossible-to-resize-image-taken-by-camera/22588/2 11 | // http://www.ngroutes.com/questions/1b80967/can-you-save-base64-data-as-a-file-with-ngcordova.html 12 | // http://stackoverflow.com/questions/9902797/phone-gap-camera-orientation 13 | // http://simonmacdonald.blogspot.ca/2012/07/change-to-camera-code-in-phonegap-190.html ["correctOrientation"] 14 | // 15 | 16 | .factory('ImageService', function($cordovaCamera, $q, $log) { 17 | 18 | function optionsForType(type, quality, targetSize) { 19 | var source; 20 | switch (type) { 21 | case 0: 22 | source = Camera.PictureSourceType.CAMERA; 23 | break; 24 | case 1: 25 | source = Camera.PictureSourceType.PHOTOLIBRARY; 26 | break; 27 | } 28 | return { 29 | quality: quality, // e.g. 75, 30 | destinationType: Camera.DestinationType.FILE_URI, 31 | sourceType: source, 32 | allowEdit: false, //true, 33 | encodingType: Camera.EncodingType.JPEG, 34 | popoverOptions: CameraPopoverOptions, 35 | saveToPhotoAlbum: true, //false, 36 | targetWidth: targetSize, // e.g. 500, 37 | targetHeight: targetSize, // e.g. 500, 38 | correctOrientation: true // SEE: simonmacdonald.blogspot.ca/2012/07/change-to-camera-code-in-phonegap-190.html 39 | }; 40 | } 41 | 42 | var getPicture = function (type, quality, targetSize) { 43 | return $q(function (resolve, reject) { 44 | var options = optionsForType(type, quality, targetSize); 45 | 46 | $cordovaCamera.getPicture(options).then(function (imageUrl) { 47 | 48 | $log.debug('ImageService#getPicture, $cordovaCamera.getPicture imageUrl = ' + imageUrl); 49 | 50 | resolve(imageUrl); 51 | 52 | }, function (error) { 53 | $log.debug('ImageService#getPicture, $cordovaCamera.getPicture error = ' + JSON.stringify(error)); 54 | 55 | reject(error); 56 | }); 57 | 58 | }); 59 | }; 60 | 61 | var cleanup = function () { 62 | // Cleanup temp files from the camera's picture taking process. Only needed for Camera.DestinationType.FILE_URI. 63 | // Returns a promise the result of which is probably ignored. 64 | return $cordovaCamera.cleanup(); 65 | }; 66 | 67 | return { 68 | getPicture: getPicture, 69 | cleanup: cleanup 70 | }; 71 | }); 72 | }()); 73 | -------------------------------------------------------------------------------- /src/js/app/intro/intro.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |

intro.slide-1.title

17 | 18 |

19 | 20 |
21 |
22 | 23 |

24 | 25 | 26 |
27 | 28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 |
36 |

intro.slide-2.title

37 | 38 |

39 | 40 |
41 |
42 | - 43 |
44 | - 45 |
46 | - 47 |

48 | 49 | 50 |
51 | 52 |
53 |
54 | 55 | 56 | 57 | 58 | 59 |
60 |

intro.slide-3.title

61 | 62 |

63 | 64 |

65 | 66 | 67 |
68 | 69 |
70 |
71 | 72 |
73 |
74 |
75 | -------------------------------------------------------------------------------- /src/js/app/auth/changePassword/changePassword.ctrl.js: -------------------------------------------------------------------------------- 1 | ;(function () { 2 | "use strict"; 3 | 4 | var ChangePasswordCtrl = /*@ngInject*/function ($scope, $state, $stateParams, $translate, Application, UserService) { 5 | 6 | var vm = this; 7 | var onboarding = false; 8 | 9 | $scope.$on('$ionicView.beforeEnter', function () { 10 | 11 | var mode = Application.getState('mode'); 12 | onboarding = (mode === 'onboarding'); 13 | 14 | Application.contentBannerInit(vm, $scope); 15 | 16 | // enforce/ensure no logged in user at this point 17 | UserService.logoutApp(); 18 | 19 | Application.resetForm(vm); 20 | 21 | vm.user = { 22 | userName: Application.getEmail(), 23 | passwordOld: null, 24 | passwordNew: null 25 | }; 26 | 27 | var key = onboarding ? 'auth.changePassword-onboarding-title' : 'auth.changePassword-title'; 28 | 29 | $translate(key).then(function (translation) { 30 | vm.title = translation; 31 | }); 32 | 33 | }); 34 | 35 | // the ionic-content-banner needs to be displayed in the 'enter' event because it will only work if the view 36 | // is displayed completely 37 | $scope.$on('$ionicView.enter', function () { 38 | var keys = ['message.check-your-email1', 'message.check-your-email2']; 39 | 40 | Application.contentBannerShow(vm, keys, null, null, 'error'); 41 | }); 42 | 43 | vm.changePassword = function (form) { 44 | if (!form.$valid) { 45 | return; 46 | } 47 | 48 | var email = ('' + vm.user.userName).toLowerCase(); 49 | 50 | Application.showLoading(true); 51 | 52 | UserService.changePassword(email, vm.user.passwordOld, vm.user.passwordNew).then(function () { 53 | 54 | return UserService.login(email, vm.user.passwordNew); 55 | 56 | }).then(function () { 57 | Application.hideLoading(); 58 | 59 | var key = onboarding ? 'auth.changePassword-onboarding-success' : 'auth.changePassword-success'; 60 | 61 | $translate(key).then(function (translation) { 62 | Application.showToast(translation); 63 | 64 | if (onboarding) { 65 | Application.gotoUserProfilePage($state, true); 66 | } else { 67 | Application.gotoStartPage($state); 68 | } 69 | }); 70 | }).catch(function (error) { 71 | Application.hideLoading(); 72 | 73 | // login failed, check error to see why 74 | if (error === "invalid_credentials") { 75 | Application.errorMessage(vm, 'message.invalid-credentials'); 76 | } else { 77 | Application.errorMessage(vm, 'message.unknown-error'); 78 | } 79 | }); 80 | 81 | }; 82 | 83 | vm.login = function () { 84 | $state.go('login'); 85 | }; 86 | }; 87 | 88 | appModule('app.auth.changePassword').controller('ChangePasswordCtrl', ChangePasswordCtrl); 89 | }()); 90 | -------------------------------------------------------------------------------- /scss/app/_profile.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Profile header/images 3 | */ 4 | 5 | $profile-header-height: 120px !default; 6 | 7 | $profile-logo-size: 90px !default; 8 | 9 | $profile-logo-img-margin: 5px; 10 | $profile-logo-img-size: $profile-logo-size - (2 * $profile-logo-img-margin); 11 | 12 | .profile-header { 13 | position: relative; 14 | width: 100%; 15 | height: $profile-header-height; 16 | padding: 0; 17 | background-color: lighten($brand-secondary, 20%); 18 | } 19 | 20 | .profile-header img { 21 | width: 100%; 22 | max-height: $profile-header-height; 23 | opacity: 0.6; 24 | background-color: lighten($brand-secondary, 20%); 25 | } 26 | 27 | .profile-header-logo-wrapper { 28 | width: $profile-logo-size; 29 | left: 6%; 30 | background-color: lighten($brand-secondary, 25%); 31 | 32 | // 33 | // Vertically center the div through: "position: absolute; margin: auto; top: 0; bottom: 0; height: ;" 34 | // 35 | // http://www.smashingmagazine.com/2013/08/absolute-horizontal-vertical-centering-css/ 36 | // 37 | position: absolute; 38 | margin: auto; 39 | top: 0; 40 | bottom: 0; 41 | height: $profile-logo-size; 42 | } 43 | 44 | .profile-header-logo { 45 | height: $profile-logo-img-size; 46 | width: $profile-logo-img-size; 47 | margin: $profile-logo-img-margin; 48 | opacity: 0.5; 49 | background-color: lighten($brand-secondary, 20%); 50 | overflow: hidden; 51 | } 52 | 53 | .profile-header-logo-text { 54 | text-align: center; 55 | width: 100%; 56 | // Vertically align, see: http://www.smashingmagazine.com/2013/08/absolute-horizontal-vertical-centering-css/ 57 | position: absolute; 58 | margin: auto; 59 | left: 0; 60 | right: 0; 61 | top: 0; 62 | bottom: 0; 63 | height: 40px; 64 | } 65 | 66 | .profile-header-info-wrapper { 67 | width: 50%; 68 | left: 40%; 69 | background-color: lighten($brand-secondary, 14%); 70 | opacity: 0.8; 71 | text-align: center; 72 | // Vertically align, see: http://www.smashingmagazine.com/2013/08/absolute-horizontal-vertical-centering-css/ 73 | position: absolute; 74 | margin: auto; 75 | top: 0; 76 | bottom: 0; 77 | height: $profile-logo-size; 78 | } 79 | 80 | .profile-header-info { 81 | text-align: center; 82 | width: 100%; 83 | // Vertically align, see: http://www.smashingmagazine.com/2013/08/absolute-horizontal-vertical-centering-css/ 84 | position: absolute; 85 | margin: auto; 86 | top: 0; 87 | bottom: 0; 88 | height: 40px; 89 | } 90 | 91 | /* centering vertically foroward icon */ 92 | .centering-vertically { 93 | position: absolute; 94 | top: 50%; 95 | left: 50%; 96 | } 97 | 98 | .dialog-footer { 99 | margin: 0 !important; 100 | } 101 | -------------------------------------------------------------------------------- /src/js/app/auth/login/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 |
13 | auth.login-title 14 |
15 | 16 | 17 | 18 | 29 | 30 |
31 |
32 |
33 | 34 | 44 | 45 |
46 |
47 |
48 | 49 |
link.forgot-password
50 | 51 | 52 |
53 | 54 |
55 | 57 | 58 | prompt.no-accountlink.signup 60 |
61 | 62 |
63 | prompt.login-with-twitter 64 | 66 |
67 |
68 | 69 |
70 | 71 |
72 |
-------------------------------------------------------------------------------- /src/js/app/image/services/fileManager.service.js: -------------------------------------------------------------------------------- 1 | ;(function () { 2 | "use strict"; 3 | 4 | appModule('app.image') 5 | 6 | // 7 | // https://github.com/apache/cordova-plugin-file 8 | // http://www.raymondcamden.com/2014/08/18/PhoneGapCordova-Example-Getting-File-Metadata-and-an-update-to-the-FAQ 9 | // http://www.html5rocks.com/en/tutorials/file/filesystem/ 10 | // http://community.phonegap.com/nitobi/topics/dataurl_to_png 11 | // 12 | 13 | .factory('FileManager', function ($q, $log, $cordovaFile, $cordovaFileTransfer, Application) { 14 | 15 | var downloadFile = function(sourceURI, targetDir, targetFile) { 16 | 17 | var deferred = $q.defer(); 18 | 19 | $log.debug("FileManager#downloadFile source (original): '" + sourceURI + "'"); 20 | sourceURI = decodeURI(sourceURI); 21 | 22 | var w = Application.logStarted("FileManager#downloadFile source (decoded): '" + sourceURI + "'"); 23 | 24 | var targetPath = targetDir + targetFile; 25 | var trustHosts = true; 26 | var options = {}; 27 | 28 | $cordovaFileTransfer.download(sourceURI, targetPath, options, trustHosts).then( 29 | function(result) { 30 | Application.logFinished(w, 'finished, result: ' + JSON.stringify(result)); 31 | deferred.resolve(result); 32 | 33 | }, function(error) { 34 | Application.logError(w, JSON.stringify(error)); 35 | deferred.reject(error); 36 | 37 | }, function (progress) { 38 | //$timeout(function () { 39 | // $scope.downloadProgress = (progress.loaded / progress.total) * 100; 40 | //}) 41 | }); 42 | 43 | return deferred.promise; 44 | }; 45 | 46 | var getFileInfo = function (baseDir, filePath) { 47 | var deferred = $q.defer(); 48 | 49 | $log.debug("FileManager#checkFile baseDir = '" + baseDir + "', filePath = '" + filePath + "'"); 50 | 51 | $cordovaFile.checkFile(baseDir, filePath).then( 52 | function (fileEntry) { 53 | fileEntry.getMetadata( 54 | function (result) { 55 | deferred.resolve(result); 56 | }, 57 | function (error) { 58 | deferred.reject(error); 59 | } 60 | ); 61 | }, 62 | function (error) { 63 | deferred.reject(error); 64 | } 65 | ); 66 | 67 | return deferred.promise; 68 | }; 69 | 70 | var removeFile = function (baseDir, filePath) { 71 | $log.debug("FileManager#removeFile baseDir = '" + baseDir + "', filePath = '" + filePath + "'"); 72 | 73 | return $cordovaFile.removeFile(baseDir, filePath); 74 | }; 75 | 76 | return { 77 | downloadFile: downloadFile, 78 | getFileInfo: getFileInfo, 79 | removeFile: removeFile 80 | }; 81 | }); 82 | }()); 83 | -------------------------------------------------------------------------------- /src/js/app/auth/changePassword/changePassword.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 |
13 | {{vm.title}} 14 |
15 | 16 | 18 | 19 | 30 | 31 |
32 |
33 |
34 | 35 | 45 | 46 |
47 |
48 |
49 | 50 | 60 | 61 |
62 |
63 |
64 | 65 | 66 |
67 | 68 |
69 | 71 | 72 | 73 |
74 | 75 |
76 | 77 |
78 |
-------------------------------------------------------------------------------- /hooks/after_prepare/010_add_platform_class.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Add Platform Class 4 | // v1.0 5 | // Automatically adds the platform class to the body tag 6 | // after the `prepare` command. By placing the platform CSS classes 7 | // directly in the HTML built for the platform, it speeds up 8 | // rendering the correct layout/style for the specific platform 9 | // instead of waiting for the JS to figure out the correct classes. 10 | 11 | var fs = require('fs'); 12 | var path = require('path'); 13 | 14 | var rootdir = process.argv[2]; 15 | 16 | function addPlatformBodyTag(indexPath, platform) { 17 | // add the platform class to the body tag 18 | try { 19 | var platformClass = 'platform-' + platform; 20 | var cordovaClass = 'platform-cordova platform-webview'; 21 | 22 | var html = fs.readFileSync(indexPath, 'utf8'); 23 | 24 | var bodyTag = findBodyTag(html); 25 | if(!bodyTag) return; // no opening body tag, something's wrong 26 | 27 | if(bodyTag.indexOf(platformClass) > -1) return; // already added 28 | 29 | var newBodyTag = bodyTag; 30 | 31 | var classAttr = findClassAttr(bodyTag); 32 | if(classAttr) { 33 | // body tag has existing class attribute, add the classname 34 | var endingQuote = classAttr.substring(classAttr.length-1); 35 | var newClassAttr = classAttr.substring(0, classAttr.length-1); 36 | newClassAttr += ' ' + platformClass + ' ' + cordovaClass + endingQuote; 37 | newBodyTag = bodyTag.replace(classAttr, newClassAttr); 38 | 39 | } else { 40 | // add class attribute to the body tag 41 | newBodyTag = bodyTag.replace('>', ' class="' + platformClass + ' ' + cordovaClass + '">'); 42 | } 43 | 44 | html = html.replace(bodyTag, newBodyTag); 45 | 46 | fs.writeFileSync(indexPath, html, 'utf8'); 47 | 48 | process.stdout.write('add to body class: ' + platformClass + '\n'); 49 | } catch(e) { 50 | process.stdout.write(e); 51 | } 52 | } 53 | 54 | function findBodyTag(html) { 55 | // get the body tag 56 | try{ 57 | return html.match(/])(.*?)>/gi)[0]; 58 | }catch(e){} 59 | } 60 | 61 | function findClassAttr(bodyTag) { 62 | // get the body tag's class attribute 63 | try{ 64 | return bodyTag.match(/ class=["|'](.*?)["|']/gi)[0]; 65 | }catch(e){} 66 | } 67 | 68 | if (rootdir) { 69 | 70 | // go through each of the platform directories that have been prepared 71 | var platforms = (process.env.CORDOVA_PLATFORMS ? process.env.CORDOVA_PLATFORMS.split(',') : []); 72 | 73 | for(var x=0; x', 35 | scope: { 36 | dirty: '=' 37 | }, 38 | link: function (scope) { 39 | var cleanUpFn = angular.noop, unwatch, checkScope = function () { 40 | 41 | if (scope.dirty) { 42 | 43 | cleanUpFn = $rootScope.$on('$stateChangeStart', 44 | 45 | function(event, toState, toParams, fromState, fromParams) { 46 | // we start by immediately preventing the state/page switch, otherwise we are too late (because the 47 | // "$ionicPopup.confirm()" call below works asynchronously so doesn't wait/block the default action) 48 | event.preventDefault(); 49 | 50 | $ionicPopup.confirm({ 51 | title: popupTexts[key + 'title'], 52 | template: popupTemplate, 53 | cssClass: 'info-popup', 54 | cancelText: popupTexts[key + 'cancel-button'], 55 | okText: popupTexts[key + 'ok-button'] 56 | 57 | }).then(function(res) { 58 | 59 | if (!res) { // user confirmed 60 | // cleanup the event hook so that we do not ask for confirmation again when switching the state 61 | cleanUpFn(); 62 | cleanUpFn = angular.noop; 63 | 64 | // now re-do the state switch 65 | $state.go(toState, toParams); 66 | } 67 | }); 68 | }); 69 | 70 | } else { 71 | cleanUpFn(); 72 | cleanUpFn = angular.noop; 73 | } 74 | }; 75 | 76 | unwatch = scope.$watch('dirty', checkScope); 77 | 78 | scope.$on('$destroy', function () { 79 | cleanUpFn(); 80 | unwatch(); 81 | }); 82 | } 83 | }; 84 | } 85 | ); 86 | 87 | }()); -------------------------------------------------------------------------------- /hooks/README.md: -------------------------------------------------------------------------------- 1 | 21 | # Cordova Hooks 22 | 23 | This directory may contain scripts used to customize cordova commands. This 24 | directory used to exist at `.cordova/hooks`, but has now been moved to the 25 | project root. Any scripts you add to these directories will be executed before 26 | and after the commands corresponding to the directory name. Useful for 27 | integrating your own build systems or integrating with version control systems. 28 | 29 | __Remember__: Make your scripts executable. 30 | 31 | ## Hook Directories 32 | The following subdirectories will be used for hooks: 33 | 34 | after_build/ 35 | after_compile/ 36 | after_docs/ 37 | after_emulate/ 38 | after_platform_add/ 39 | after_platform_rm/ 40 | after_platform_ls/ 41 | after_plugin_add/ 42 | after_plugin_ls/ 43 | after_plugin_rm/ 44 | after_plugin_search/ 45 | after_prepare/ 46 | after_run/ 47 | after_serve/ 48 | before_build/ 49 | before_compile/ 50 | before_docs/ 51 | before_emulate/ 52 | before_platform_add/ 53 | before_platform_rm/ 54 | before_platform_ls/ 55 | before_plugin_add/ 56 | before_plugin_ls/ 57 | before_plugin_rm/ 58 | before_plugin_search/ 59 | before_prepare/ 60 | before_run/ 61 | before_serve/ 62 | pre_package/ <-- Windows 8 and Windows Phone only. 63 | 64 | ## Script Interface 65 | 66 | All scripts are run from the project's root directory and have the root directory passes as the first argument. All other options are passed to the script using environment variables: 67 | 68 | * CORDOVA_VERSION - The version of the Cordova-CLI. 69 | * CORDOVA_PLATFORMS - Comma separated list of platforms that the command applies to (e.g.: android, ios). 70 | * CORDOVA_PLUGINS - Comma separated list of plugin IDs that the command applies to (e.g.: org.apache.cordova.file, org.apache.cordova.file-transfer) 71 | * CORDOVA_HOOK - Path to the hook that is being executed. 72 | * CORDOVA_CMDLINE - The exact command-line arguments passed to cordova (e.g.: cordova run ios --emulate) 73 | 74 | If a script returns a non-zero exit code, then the parent cordova command will be aborted. 75 | 76 | 77 | ## Writing hooks 78 | 79 | We highly recommend writting your hooks using Node.js so that they are 80 | cross-platform. Some good examples are shown here: 81 | 82 | [http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/](http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/) 83 | 84 | -------------------------------------------------------------------------------- /scss/app/_intro.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Intro/slider styles 3 | */ 4 | 5 | // base "slider" height for all the other calculations - DO NOT CHANGE THIS VALUE ! 6 | $intro-slider-base-height: 630px; 7 | 8 | // overall slide height - this determines the vertical area on which a 'swipe' gesture can be made, therefore it's 9 | // deliberately set to a very high value (more than what would normally be the height of the screen) - DO NOT CHANGE 10 | // UNLESS YOU HAVE A GOOD REASON TO DO SO 11 | $intro-slider-height: 2030px !default; 12 | 13 | // this is the distance from the 'bottom' of the slide at which the pager and the nav buttons are placed, so a SMALLER 14 | // value means that the buttons/pager are placed LOWER on the screen and you have there for more room for the slide 15 | // contents (texts and images) - however the nav buttons and pager may then 'fall off' the screen and hence require the 16 | // user to scroll up to see them; the default value below (390px) is therefore a compromise (with this value the nav 17 | // buttons and pager can be seen without scrolling even on an iPhone 4 in 'portrait' mode) 18 | $intro-slide-nav-bottom: 390px !default; 19 | 20 | // calculated values 21 | $intro-slide-nav-buttons-bottom: $intro-slide-nav-bottom + $intro-slider-height - $intro-slider-base-height; 22 | $intro-slider-pager-bottom: $intro-slide-nav-buttons-bottom - 160px; 23 | 24 | // header with an image at the top of the slides, 120px high 25 | $intro-header-height: 120px !default; 26 | 27 | .slider { 28 | height: $intro-slider-height; 29 | position: absolute !important; 30 | top: 0; 31 | bottom: 0; 32 | left: 0; 33 | right: 0; 34 | } 35 | 36 | .slider-slides { 37 | height: $intro-slider-height !important; 38 | position: relative; 39 | } 40 | 41 | .slider-slide { 42 | height: $intro-slider-height !important; 43 | padding-top: 80px; 44 | color: #000; 45 | background-color: #fff; 46 | text-align: center; 47 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 48 | font-weight: 300; 49 | } 50 | 51 | .slider-pager { 52 | position: relative; 53 | bottom: $intro-slider-pager-bottom !important; 54 | } 55 | 56 | .intro-body { 57 | position: relative; 58 | width: 100%; 59 | //height: $intro-slider-height - 100; 60 | height: $intro-slider-height !important; 61 | } 62 | 63 | .intro-nav-buttons { 64 | position: absolute; 65 | bottom: $intro-slide-nav-buttons-bottom; 66 | width: 100%; 67 | } 68 | 69 | .intro-nav-button { 70 | width: 90px; 71 | } 72 | 73 | .intro-slider-content { 74 | height: 100%; 75 | position: absolute; 76 | } 77 | 78 | .intro-slider-text { 79 | text-align: left; 80 | @extend .padding; 81 | } 82 | 83 | .intro-header { 84 | position: relative; 85 | width: 100%; 86 | height: $intro-header-height; 87 | padding: 0; 88 | } 89 | 90 | .intro-header img { 91 | width: 100%; 92 | max-height: $intro-header-height; 93 | opacity: 0.6; 94 | } 95 | 96 | .intro-header-logo { 97 | position: absolute; 98 | width: 100%; 99 | top: 25%; 100 | text-align: center; 101 | } 102 | 103 | .intro-header-logo img { 104 | width: auto; 105 | opacity: 1; 106 | } 107 | -------------------------------------------------------------------------------- /src/js/app/auth/signup/signup.ctrl.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | var SignupCtrl = /*@ngInject*/function ($scope, $state, Application, UserService) { 5 | // vm: the "Controller as vm" convention from: http://www.johnpapa.net/angularjss-controller-as-and-the-vm-variable/ 6 | var vm = this; 7 | var log = Application.getLogger('SignupCtrl'); 8 | 9 | $scope.$on('$ionicView.beforeEnter', function() { 10 | Application.resetForm(vm); 11 | vm.user = {}; 12 | }); 13 | 14 | vm.signup = function(form) { 15 | 16 | if(!form.$valid) { 17 | return; 18 | } 19 | 20 | // log in with the user's email address and a generated (temporary) password 21 | var email = ('' + vm.user.email).toLowerCase(); 22 | var password = generatePassword(); 23 | 24 | Application.showLoading(true); 25 | 26 | var userData = { 27 | userName: email, 28 | password: password, 29 | email: email, 30 | fullName: email 31 | }; 32 | 33 | UserService.signup(userData).then(function (signedupUser) { 34 | log.info("User signed up successfully"); 35 | 36 | // send a reset-password email with a (new) temporary password 37 | return UserService.resetPassword(email); 38 | }) 39 | .then(function (signedupUser) { 40 | Application.hideLoading(); 41 | 42 | Application.setEmail(email); 43 | 44 | log.info("User signed up successfully"); 45 | 46 | // go to the change-password page, displaying a message asking the user to verify their email 47 | Application.setState('mode', 'onboarding'); 48 | Application.gotoPage($state, 'changePassword', {mode: 'onboarding'}, true, true); 49 | }) 50 | .catch(function (error) { 51 | Application.hideLoading(); 52 | 53 | if (error === "invalid_email") { 54 | Application.errorMessage(vm, 'message.valid-email'); 55 | } else if (error === "already_registered") { 56 | Application.errorMessage(vm, 'message.already-registered'); 57 | } else { 58 | Application.errorMessage(vm, 'message.unknown-error'); 59 | } 60 | }); 61 | }; 62 | 63 | // 64 | // Generate a temporary password. 65 | // 66 | // Taken from: http://andreasmcdermott.com/web/2014/02/05/Email-verification-with-Firebase/#comment-1277405896 67 | // 68 | function generatePassword () { 69 | var possibleChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!?_-'; 70 | var password = ''; 71 | for(var i = 0; i < 16; i += 1) { 72 | password += possibleChars[ Math.floor(Math.random()*possibleChars.length)]; 73 | } 74 | return password; 75 | } 76 | 77 | vm.intro = function () { 78 | Application.gotoIntroPage($state); 79 | }; 80 | 81 | vm.login = function() { 82 | $state.go('login'); 83 | }; 84 | 85 | vm.loginWithTwitter = function () { 86 | Application.showLoading(true); 87 | 88 | UserService.loginWithTwitter().then(function (loggedinUser) { 89 | Application.hideLoading(); 90 | Application.gotoStartPage($state); 91 | }) 92 | .catch(function (error) { 93 | Application.hideLoading(); 94 | 95 | // login failed, check error to see why 96 | if (error == "invalid_credentials") { 97 | Application.errorMessage(vm, 'message.invalid-credentials'); 98 | } else { 99 | Application.errorMessage(vm, 'message.unknown-error'); 100 | } 101 | }); 102 | }; 103 | 104 | vm.canLoginWithTwitter = function () { 105 | return UserService.canLoginWithTwitter(); 106 | }; 107 | 108 | }; 109 | 110 | appModule('app.auth.signup').controller('SignupCtrl', SignupCtrl); 111 | }()); 112 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | var failFast = require('protractor-fail-fast'); 2 | 3 | /* global browser */ 4 | exports.config = { 5 | allScriptsTimeout: 20000, 6 | specs: [ 7 | // E2E test specs are organized by user stories, not necessarily reflecting the code structure of the project. 8 | // Imagine things your users might do, and write e2e tests around those behaviors. 9 | 'test/e2e/**/*.spec.js', 10 | ], 11 | capabilities: { 12 | // You can use other browsers like firefox, phantoms, safari, IE, etc. 13 | 'browserName': 'chrome' 14 | }, 15 | baseUrl: 'http://localhost:8100', 16 | // Configuration needed if you use a "permanently running" Selenium server (instead of starting a server each time): 17 | //seleniumAddress: 'http://localhost:4444/wd/hub', 18 | // http://stackoverflow.com/questions/31662828/how-to-access-chromedriver-logs-for-protractor-test/31662935 19 | //seleniumArgs: [ 20 | // '-Dwebdriver.chrome.logfile=_chromedriver.log', 21 | //], 22 | // http://stackoverflow.com/questions/30600738/difference-running-protractor-with-without-selenium 23 | //directConnect: false, 24 | directConnect: true, // BY DEFAULT WE NOW USE DIRECTCONNECT = TRUE, AND NO NPM/SELENIUM - YOUR TESTS RUN WAY FASTER! 25 | // http://stackoverflow.com/questions/31662828/how-to-access-chromedriver-logs-for-protractor-test/31840996#31840996 26 | chromeDriver: 'bin/protractor-chromedriver.sh', 27 | framework: 'jasmine', 28 | jasmineNodeOpts: { 29 | showColors: true, 30 | // 31 | // Increased "defaultTimeoutInterval" from 30000 to 60000 to prevent "Async callback was not invoked within timeout 32 | // specified by jasmine.DEFAULT_TIMEOUT_INTERVAL", see Stackoverflow post: 33 | // 34 | // stackoverflow.com/questions/29218981/jasmine-2-async-callback-was-not-invoked-within-timeout-specified-by-jasmine-d 35 | // 36 | defaultTimeoutInterval: 60000, 37 | //isVerbose: true, 38 | isVerbose: false, 39 | //stackoverflow.com/questions/28893436/how-to-stop-protractor-from-running-further-testcases-on-failure 40 | //realtimeFailure: true, 41 | realtimeFailure: false, 42 | // https://github.com/bcaudan/jasmine-spec-reporter/blob/master/docs/protractor-configuration.md 43 | print: function () {} 44 | }, 45 | 46 | plugins: [{ 47 | package: 'protractor-fail-fast' 48 | }], 49 | 50 | onPrepare: function () { 51 | jasmine.getEnv().addReporter(failFast.init()); 52 | 53 | // add jasmine spec reporter: https://github.com/bcaudan/jasmine-spec-reporter 54 | var SpecReporter = require('jasmine-spec-reporter'); 55 | 56 | var opts = { 57 | displayStacktrace: 'none', // display stacktrace for each failed assertion, values: (all|specs|summary|none) 58 | displayFailuresSummary: true, // display summary of all failures after execution 59 | displayPendingSummary: true, // display summary of all pending specs after execution 60 | displaySuccessfulSpec: true, // display each successful spec 61 | displayFailedSpec: true, // display each failed spec 62 | displayPendingSpec: false, // display each pending spec 63 | displaySpecDuration: false, // display each spec duration 64 | displaySuiteNumber: true, // display each suite number (hierarchical) 65 | colors: { 66 | success: 'green', 67 | failure: 'red', 68 | pending: 'yellow' 69 | }, 70 | prefixes: { 71 | success: '✓ ', 72 | failure: '✗ ', 73 | pending: '* ' 74 | }, 75 | customProcessors: [] 76 | }; 77 | 78 | jasmine.getEnv().addReporter(new SpecReporter(opts)); 79 | 80 | // Removed this because it caused the Protractor tests to fail ... 81 | //browser.driver.manage().window().setSize(900, 750); 82 | //browser.driver.manage().window().setPosition(400, 0); 83 | }, 84 | 85 | afterLaunch: function () { 86 | failFast.clean(); // cleans up the "fail file" 87 | } 88 | 89 | }; -------------------------------------------------------------------------------- /src/js/lib/logentries/le.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Logentries. 3 | Please view license at https://raw.github.com/logentries/le_js/master/LICENSE 4 | */ 5 | 'use strict';(function(b,e){"function"===typeof define&&define.amd?define(function(){return e(b)}):"object"===typeof exports?("object"===typeof global&&(b=global),module.exports=e(b)):b.LE=e(b)})(this,function(b){function e(a){var c=a.trace?(Math.random()+Math.PI).toString(36).substring(2,10):null,q=a.page_info,e=a.token,g=a.print,f=a.no_format,r;r="undefined"===typeof XDomainRequest?a.ssl:"https:"===b.location.protocol?!0:!1;var k;k=b.LEENDPOINT?b.LEENDPOINT:f?"webhook.logentries.com/noformat":"js.logentries.com/v1"; 6 | k=(r?"https://":"http://")+k+"/logs/"+e;var h=[],l=!1,s=!1;if(a.catchall){var t=b.onerror;b.onerror=function(a,b,d){m({error:a,line:d,location:b}).level("ERROR").send();return t?t(a,b,d):!1}}var p=function(){var a=b.navigator||{doNotTrack:void 0},c=b.screen||{};return{url:(b.location||{}).pathname,referrer:document.referrer,screen:{width:c.width,height:c.height},window:{width:b.innerWidth,height:b.innerHeight},browser:{name:a.appName,version:a.appVersion,cookie_enabled:a.cookieEnabled,do_not_track:a.doNotTrack}, 7 | platform:a.platform}},u=function(){var a=null,a=Array.prototype.slice.call(arguments);if(0===a.length)throw Error("No arguments!");return a=1===a.length?a[0]:a},m=function(a){var b=u.apply(this,arguments),d={event:b};"never"===q||s&&"per-entry"!==q||(s=!0,"undefined"===typeof b.screen&&"undefined"===typeof b.browser&&m(p()).level("PAGE").send());c&&(d.trace=c);return{level:function(a){if(g&&"undefined"!==typeof console&&"PAGE"!==a){var b=null;"undefined"!==typeof XDomainRequest&&(b=d.trace+" "+d.event); 8 | try{console[a.toLowerCase()].call(console,b||d)}catch(c){console.log(b||d)}}d.level=a;return{send:function(){var a=[],b=JSON.stringify(d,function(b,d){if("undefined"===typeof d)return"undefined";if("object"===typeof d&&null!==d){var c;a:{for(c=0;c";a.push(d)}return d});l?h.push(b):n(e,b)}}}}};this.log=m;var n=function(a,b){l=!0;var d;d="undefined"!==typeof XDomainRequest?new XDomainRequest:new XMLHttpRequest;d.constructor===XMLHttpRequest? 9 | d.onreadystatechange=function(){4===d.readyState&&(400<=d.status?(console.error("Couldn't submit events."),410===d.status&&console.warn("This version of le_js is no longer supported!")):(301===d.status&&console.warn("This version of le_js is deprecated! Consider upgrading."),0 2 | {{vm.title}} 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 |

manage.userProfile.title

12 | 13 |
14 | 15 | 16 | 17 | 18 | 30 | 31 |
32 |
33 |
34 | 35 | 45 | 46 |
47 |
manage.userProfile.fields.sex-message
48 |
49 | 50 |
51 | 52 | 60 | 61 |
62 | 63 |
64 | 65 |
66 | 67 | 81 | 82 |
83 |
84 | 85 | 88 | 89 |
90 |
91 | 92 | 93 | manage.userProfile.fields.other 94 | 95 | 96 | 97 | 107 | 108 | 109 | 110 |
111 | 114 |
115 | 116 |
117 |
118 | 119 |
120 | 121 | -------------------------------------------------------------------------------- /src/js/app/util/services/logging.service.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | // 5 | // Enhance the built-in angularjs logger with some extra features (e.g. printing the timestamp, and logging the number 6 | // of AngularJS watchers in debug mode). 7 | // 8 | 9 | appModule('app.util') 10 | 11 | .factory('loggingDecorator', function (loggingService, dateFilter, TrackLogLevels) { 12 | var decorate = function(log, appName) { 13 | 14 | log.log = enhanceLogging(log.log, appName, 'LOG', TrackLogLevels.log); 15 | log.info = enhanceLogging(log.info, appName, 'INFO', TrackLogLevels.info); 16 | log.warn = enhanceLogging(log.warn, appName, 'WARN', TrackLogLevels.warn); 17 | log.debug = enhanceLogging(log.debug, appName, 'DEBUG', TrackLogLevels.debug); 18 | log.error = enhanceLogging(log.error, appName, 'ERROR', TrackLogLevels.error); 19 | 20 | log.getLogger = function(context) { 21 | return { 22 | log : enhanceLogging(log.log, appName, 'LOG', TrackLogLevels.log, context), 23 | info : enhanceLogging(log.info, appName, 'INFO', TrackLogLevels.info, context), 24 | warn : enhanceLogging(log.warn, appName, 'WARN', TrackLogLevels.warn, context), 25 | debug : enhanceLogging(log.debug, appName, 'DEBUG', TrackLogLevels.debug, context, true), 26 | error : enhanceLogging(log.error, appName, 'ERROR', TrackLogLevels.error, context) 27 | }; 28 | }; 29 | }; 30 | 31 | // From: https://medium.com/@kentcdodds/counting-angularjs-watchers-11c5134dc2ef 32 | function getWatchers(root) { 33 | root = angular.element(root || document.documentElement); 34 | var watcherCount = 0; 35 | 36 | function getElemWatchers(element) { 37 | var isolateWatchers = getWatchersFromScope(element.data().$isolateScope); 38 | var scopeWatchers = getWatchersFromScope(element.data().$scope); 39 | var watchers = scopeWatchers.concat(isolateWatchers); 40 | angular.forEach(element.children(), function (childElement) { 41 | watchers = watchers.concat(getElemWatchers(angular.element(childElement))); 42 | }); 43 | return watchers; 44 | } 45 | 46 | function getWatchersFromScope(scope) { 47 | if (scope) { 48 | return scope.$$watchers || []; 49 | } else { 50 | return []; 51 | } 52 | } 53 | 54 | return getElemWatchers(root); 55 | } 56 | 57 | function enhanceLogging(loggingFunc, appName, logLevel, trackLogLevel, context, debug) { 58 | return function () { 59 | var modifiedArguments = [].slice.call(arguments); 60 | 61 | var prefix = ''; 62 | 63 | if (!trackLogLevel) { 64 | prefix = logLevel + ' - '; 65 | } 66 | 67 | if (appName) { 68 | prefix = prefix + appName + ' - '; 69 | } 70 | 71 | if (context) { 72 | prefix = prefix + '[' + context + ']'; 73 | } else { 74 | prefix = prefix + dateFilter(new Date(), 'yyyy-MM-dd HH:mm:ss'); 75 | } 76 | 77 | if (debug) { 78 | prefix += " {WATCHERS: " + getWatchers().length + "} "; 79 | } 80 | 81 | modifiedArguments[0] = prefix + " - " + modifiedArguments[0]; 82 | 83 | if (trackLogLevel) { 84 | loggingService.log(context || '(unknown)', loggingFunc, logLevel, modifiedArguments); 85 | } else { 86 | loggingFunc.apply(null, modifiedArguments); 87 | } 88 | }; 89 | } 90 | 91 | return { 92 | decorate: decorate 93 | }; 94 | }) 95 | 96 | // see: http://blog.pdsullivan.com/posts/2015/02/19/ionicframework-googleanalytics-log-errors.html 97 | .factory('loggingService', function (APP, TrackingService) { 98 | 99 | var deviceId = null; 100 | 101 | var setDeviceId = function(id) { 102 | deviceId = id; 103 | }; 104 | 105 | var log = function(context, loggingFunc, logLevel, args) { 106 | var message = args[0]; 107 | args[0] = logLevel + ' - ' + args[0]; 108 | 109 | loggingFunc.apply(null, args); 110 | 111 | if (APP.tracking) { 112 | trackEvent(logLevel, message); 113 | } 114 | }; 115 | 116 | // 117 | // Track an event using some kind of (remote) tracking/logging system. 118 | // 119 | function trackEvent(logLevel, message) { 120 | var prefix = deviceId ? deviceId + ' - ' : ''; 121 | 122 | TrackingService.trackEvent(logLevel, prefix + message); 123 | } 124 | 125 | return { 126 | setDeviceId: setDeviceId, 127 | log: log 128 | }; 129 | }) 130 | ; 131 | }()); 132 | -------------------------------------------------------------------------------- /src/js/app/user/services/firebase/oauthHelper.service.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | // 5 | // A utility service with Cordova Oauth helper funtions (currently only used for Twitter Oauth flows). 6 | // 7 | // This code was copied and amended from src/lib/ngCordova/dist/ng-cordovajs ("oauth.providers" module, "$cordovaOauth" 8 | // service). 9 | // 10 | 11 | angular.module('app.oauthUtil', ["oauth.utils"]) 12 | 13 | .factory("oauthHelper", ["$q", '$http', "$cordovaOauthUtility", function($q, $http, $cordovaOauthUtility) { 14 | 15 | return { 16 | 17 | /* 18 | * Initialize an auth against the Twitter service, but do not complete it (use a hidden InAppBrowser winndow, and 19 | * close the window after a few seconds and do not complete the request). 20 | * 21 | * The goal is to mimick a "logout" call (which the Twitter API does not officially support) by passing the URL 22 | * params "force_login=true" and "screen_name=" (empty Twitter user name). 23 | * 24 | * Note that this service requires jsSHA for generating HMAC-SHA1 Oauth 1.0 signatures 25 | * 26 | * @param string clientId 27 | * @param string clientSecret 28 | * @return promise 29 | */ 30 | twitter: function (clientId, clientSecret, options) { 31 | var deferred = $q.defer(); 32 | 33 | if (window.cordova) { 34 | var cordovaMetadata = cordova.require("cordova/plugin_list").metadata; 35 | 36 | if ($cordovaOauthUtility.isInAppBrowserInstalled(cordovaMetadata) === true) { 37 | var redirect_uri = "http://localhost/callback"; 38 | if (options !== undefined) { 39 | if (options.hasOwnProperty("redirect_uri")) { 40 | redirect_uri = options.redirect_uri; 41 | } 42 | } 43 | 44 | if (typeof jsSHA !== "undefined") { 45 | var oauthObject = { 46 | oauth_consumer_key: clientId, 47 | oauth_nonce: $cordovaOauthUtility.createNonce(10), 48 | oauth_signature_method: "HMAC-SHA1", 49 | oauth_timestamp: Math.round((new Date()).getTime() / 1000.0), 50 | oauth_version: "1.0" 51 | }; 52 | var signatureObj = $cordovaOauthUtility.createSignature("POST", "https://api.twitter.com/oauth/request_token", oauthObject, {oauth_callback: redirect_uri}, clientSecret); 53 | $http({ 54 | method: "post", 55 | url: "https://api.twitter.com/oauth/request_token", 56 | headers: { 57 | "Authorization": signatureObj.authorization_header, 58 | "Content-Type": "application/x-www-form-urlencoded" 59 | }, 60 | data: "oauth_callback=" + encodeURIComponent(redirect_uri) 61 | }) 62 | .then(function (response) { 63 | var requestTokenResult = response.data; 64 | var requestTokenParameters = (requestTokenResult).split("&"); 65 | var parameterMap = {}; 66 | for (var i = 0; i < requestTokenParameters.length; i++) { 67 | parameterMap[requestTokenParameters[i].split("=")[0]] = requestTokenParameters[i].split("=")[1]; 68 | } 69 | if (parameterMap.hasOwnProperty("oauth_token") === false) { 70 | deferred.reject("Oauth request token was not received"); 71 | } 72 | 73 | // Pass the URL params "screen_name=" and "force_login=true" to force re-authentication, but don't 74 | // complete the auth flow (the Twitter login form is displayed in a hidden Inappbrowser window), thus 75 | // in effect performing a "logout" 76 | var url = 'https://api.twitter.com/oauth/authenticate?screen_name=&force_login=true&oauth_token=' + parameterMap.oauth_token; 77 | 78 | // set "hidden=yes" because we don't want to show the login form to the user (we're not interested in 79 | // performing an actual login) 80 | var browserRef = window.open(url, '_blank', 'location=no,clearsessioncache=yes,clearcache=yes,hidden=yes'); 81 | 82 | // close the login popup after a second (again, we're not interested in performing an actual login) 83 | setTimeout(function () { 84 | browserRef.close(); 85 | }, 1000); 86 | deferred.resolve("The sign in flow was canceled on purpose"); 87 | }) 88 | .catch(function (response) { 89 | deferred.reject(response); 90 | }) 91 | } else { 92 | deferred.reject("Missing jsSHA JavaScript library"); 93 | } 94 | } else { 95 | deferred.reject("Could not find InAppBrowser plugin"); 96 | } 97 | } else { 98 | deferred.reject("Cannot authenticate via a web browser"); 99 | } 100 | return deferred.promise; 101 | } 102 | 103 | } 104 | 105 | }]); 106 | 107 | }()); 108 | 109 | -------------------------------------------------------------------------------- /src/js/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": { 3 | "home": "Home", 4 | "intro": "Intro", 5 | "user-profile": "User Profile", 6 | "login": "Log in", 7 | "logout": "Log out" 8 | }, 9 | "intro": { 10 | "title": "Welcome", 11 | "slide-1": { 12 | "title": "Welcome to this app", 13 | "text1": "This is the ionic-quickstarter app.", 14 | "text2": "We will give you a brief introduction." 15 | }, 16 | "slide-2": { 17 | "title": "Getting started with the app", 18 | "text1": "Please follow these steps:", 19 | "text2a": "Create an account", 20 | "text2b": "Log in with your account", 21 | "text2c": "Start using the app" 22 | }, 23 | "slide-3": { 24 | "title": "Questions?", 25 | "text1": "Then read the manual." 26 | } 27 | }, 28 | "auth": { 29 | "login-title": "Log in", 30 | "signup-title": "Step 1: Enter email", 31 | "changePassword-onboarding-title": "Step 2: Choose password", 32 | "changePassword-title": "Choose password", 33 | "forgot-password-title": "Forgot password", 34 | "changePassword-onboarding-success": "Signup successful, please complete your profile", 35 | "changePassword-success": "Password changed successfully" 36 | }, 37 | "manage": { 38 | "userProfile": { 39 | "title": "User Profile", 40 | "fields": { 41 | "name": "First and last name", 42 | "name-placeholder": "Your name (first and last)", 43 | "sex": "Sex (M/F)", 44 | "sex-message": "Select please", 45 | "sex-male": "Male", 46 | "sex-female": "Female", 47 | "images": "Profile picture", 48 | "images-hint1": "Click to change photo:", 49 | "images-hint2": "Uploading a photo is not mandatory.", 50 | "images-hint3": "NOTE: only available when running on a device (Cordova)", 51 | "images-profile-photo1": "Profile", 52 | "images-profile-photo2": "picture", 53 | "other": "Other data", 54 | "userName": "User name" 55 | }, 56 | "upload-profile-photo": "Upload profile picture" 57 | }, 58 | "image-crop-dialog": { 59 | "title": "Crop image", 60 | "subtitle1": "Select the desired portion of the image ny moving or resizing the frame.", 61 | "subtitle2": "This is what the image will look like:" 62 | } 63 | }, 64 | "field": { 65 | "email": "Email", 66 | "password": "Password", 67 | "login-email": "Email (any real or fake email address)", 68 | "login-password": "Password (e.g. 'password')", 69 | "passwordOld": "Enter the temporary password", 70 | "passwordNew": "Choose a new password, e.g. 'password'" 71 | }, 72 | "button": { 73 | "change-password": "Continue", 74 | "reset-password": "Reset password", 75 | "signup": "Create account", 76 | "login": "Log in", 77 | "login-with-twitter": "Login with Twitter", 78 | "previous": "Previous", 79 | "next": "Next", 80 | "close-intro": "Close intro", 81 | "save": "Save" 82 | }, 83 | "link": { 84 | "back": "Back", 85 | "forgot-password": "Forgot password?", 86 | "signup": "Sign up", 87 | "login": "Log in", 88 | "login-again": "Log in again" 89 | }, 90 | "prompt": { 91 | "no-account": "Don't have an account yet?", 92 | "have-an-account": "Already have an account?", 93 | "signup": "Enter your email address to create an account", 94 | "login": "Enter your email and password to log in", 95 | "login-with-twitter": "or", 96 | "forgot-password": "Enter your email to reset your password", 97 | "change-password": "Choose a new password here", 98 | "not-entered-yet": " not entered yet" 99 | }, 100 | "message": { 101 | "invalid-fields": "Please enter all required fields", 102 | "data-was-saved": "The data was saved", 103 | "image-was-saved": "The image was saved", 104 | "image-was-not-saved": "the image was not saved", 105 | "image-was-not-saved1": "The image was NOT saved.", 106 | "image-was-not-saved2": "Please check your network connection.", 107 | "image-was-not-saved3": "Then try again please.", 108 | "upload-profile-image": "Please upload a profile image.", 109 | "required": "This field is required.", 110 | "min-5": "Enter at least 5 characters.", 111 | "max-50": "Enter at most 50 characters.", 112 | "valid-email": "Enter a valid email address.", 113 | "email-not-verified": "Your email address isn't verified yet", 114 | "invalid-credentials": "Invalid user name or password", 115 | "already-registered": "This email adress is already registered", 116 | "not-registered": "This email adress is not registered yet", 117 | "unknown-error": "Unknown error, please check your network", 118 | "loggedout": "You are logged out", 119 | "check-your-email": "Check your email and follow the instructions", 120 | "check-your-email1": "Check your email", 121 | "check-your-email2": "to obtain your temporary password" 122 | }, 123 | "error-popup": { 124 | "title": "Unknown error", 125 | "text": 126 | "An unknown error occurred.

Please check your network connection, and retry.", 127 | "ok-button": "OK" 128 | }, 129 | "exit-popup": { 130 | "title": "Exit app", 131 | "text": "Are you sure you want to leave the app?", 132 | "cancel-button": "Cancel", 133 | "ok-button": "OK" 134 | }, 135 | "leave-page-popup": { 136 | "title": "Please confirm", 137 | "text": "You will lose the changes you've made if you leave the page.

Please select:", 138 | "cancel-button": "DO leave", 139 | "ok-button": "Do NOT leave" 140 | }, 141 | "leave-form-popup": { 142 | "title": "Please confirm", 143 | "text": "You will lose the changes you've made if you leave the form.

Please select:", 144 | "cancel-button": "DO leave", 145 | "ok-button": "Do NOT leave" 146 | }, 147 | "media-dialog": { 148 | "buttons": { 149 | "camera": "Take a picture", 150 | "library": "Select existing picture" 151 | }, 152 | "title-text": "Upload picture", 153 | "cancel-text": "Cancel" 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/index-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ionic-quickstarter 4 | 5 | An Ionic Framework and Cordova project. 6 | 7 | 8 | Ionic Framework Team 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 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 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/js/app/user/services/mock/user.service.mockImpl.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | "use strict"; 3 | 4 | appModule('app.user') 5 | 6 | .service('UserServiceMockImpl', function ($q, $log, $rootScope, User, AppHooks, StorageService) { 7 | 8 | var service; 9 | var currentLoggedinUser = null; 10 | var userDataLoaded = false; 11 | 12 | var userData = { 13 | provider: 'password', 14 | userName: 'ad@min.com', 15 | password: 'password', 16 | userRole: 'admin' // hard-coded 17 | }; 18 | 19 | function setCurrentUser(userData) { 20 | currentLoggedinUser = User.build(userData); 21 | 22 | if (currentLoggedinUser) { // we have a logged in user, load their data 23 | loadUnloadData(currentLoggedinUser, true); 24 | } 25 | 26 | return currentLoggedinUser; 27 | } 28 | 29 | var init = function () { 30 | 31 | setCurrentUser(userData); // set logged in user at app startup 32 | // comment out the line above and uncomment the next line to require login at startup 33 | //setCurrentUser(null); // no valid user at application init, forcing login at start up 34 | 35 | return currentLoggedinUser; 36 | }; 37 | 38 | var isUserLoggedIn = function () { 39 | return currentLoggedinUser !== null; 40 | }; 41 | 42 | var currentUser = function () { 43 | return currentLoggedinUser; 44 | }; 45 | 46 | var signup = function (user) { 47 | var deferred = $q.defer(); 48 | 49 | logout(); 50 | 51 | $log.debug("Signup start ..."); 52 | 53 | //if (user.password == userData.password) { 54 | $log.debug("Signup done"); 55 | 56 | // note: we don't set/change the current user because the new user isn't logged in yet 57 | deferred.resolve(User.build(userData)); 58 | //} else { 59 | // deferred.reject("unknown_error"); 60 | //} 61 | 62 | return deferred.promise; 63 | }; 64 | 65 | var login = function (username, password) { 66 | var deferred = $q.defer(); 67 | 68 | logout(); 69 | 70 | $log.debug("Login start ..."); 71 | 72 | if (password == userData.password) { 73 | $log.debug("Login done"); 74 | 75 | deferred.resolve(setCurrentUser(userData)); 76 | } else { 77 | deferred.reject("invalid_credentials"); 78 | } 79 | 80 | return deferred.promise; 81 | }; 82 | 83 | function logout() { 84 | setCurrentUser(null); 85 | } 86 | 87 | var logoutApp = function () { 88 | var deferred = $q.defer(); 89 | 90 | logout(); 91 | deferred.resolve(); 92 | 93 | return deferred.promise; 94 | }; 95 | 96 | var changePassword = function (email, passwordOld, passwordNew) { 97 | var deferred = $q.defer(); 98 | 99 | logout(); 100 | 101 | 102 | $log.debug("Password change start ..."); 103 | $log.debug("Password change done"); 104 | 105 | deferred.resolve(); 106 | 107 | return deferred.promise; 108 | }; 109 | 110 | var resetPassword = function (email) { 111 | var deferred = $q.defer(); 112 | 113 | logout(); 114 | 115 | $log.debug("Password reset start ..."); 116 | $log.debug("Password reset done"); 117 | 118 | deferred.resolve(); 119 | 120 | return deferred.promise; 121 | }; 122 | 123 | function loadUserData(user) { 124 | // create "fake" user data 125 | var defaultValues = { 126 | email: user.userName, 127 | userRole: user.userRole 128 | }; 129 | 130 | var loadedValues = StorageService.getObject('userProfile'); 131 | 132 | return angular.extend(defaultValues, loadedValues); 133 | } 134 | 135 | var loadUnload = function (user, prop, load, onSuccess, data) { 136 | var obj = user; 137 | 138 | if (load) { 139 | obj[prop] = data; 140 | 141 | if (onSuccess) { 142 | $rootScope.$broadcast(onSuccess, data); 143 | } 144 | } else { 145 | delete obj[prop]; 146 | } 147 | }; 148 | 149 | var loadUnloadData = function (user, load) { 150 | if (!user) { 151 | return; 152 | } 153 | 154 | var userService = service; 155 | userDataLoaded = false; 156 | 157 | userService.loadUnload(user, 'data', load, loadUserDataSuccess(), loadUserData(userData)); 158 | 159 | userDataLoaded = true; 160 | 161 | // call hook functions, if any 162 | AppHooks.loadUnloadUser(userService, user, load); 163 | }; 164 | 165 | var loadUserDataSuccess = function () { 166 | return 'on.loadUserDataSuccess'; 167 | }; 168 | 169 | var loadUserDataError = function () { 170 | return 'on.loadUserDataError'; 171 | }; 172 | 173 | var getUserProp = function (prop, user) { 174 | var theUser = user || currentLoggedinUser; 175 | 176 | if (!theUser) { 177 | return null; 178 | } 179 | 180 | return theUser[prop]; 181 | }; 182 | 183 | var setUserProp = function (prop, user, value) { 184 | var theUser = user || currentLoggedinUser; 185 | 186 | if (theUser) { 187 | theUser[prop] = value; 188 | } 189 | }; 190 | 191 | var getUserData = function (user) { 192 | return getUserProp('data', user); 193 | }; 194 | 195 | var setUserData = function (user, value) { 196 | setUserProp('data', user, value); 197 | }; 198 | 199 | var getUserRole = function (user) { 200 | var theUser = user || currentLoggedinUser; 201 | 202 | return theUser.getUserRole(); 203 | }; 204 | 205 | var setUserRole = function (role) { 206 | var theUser = currentLoggedinUser; 207 | 208 | theUser.setUserRole(role); 209 | }; 210 | 211 | var isUserDataLoaded = function () { 212 | return userDataLoaded; 213 | }; 214 | 215 | var retrieveProfile = function () { 216 | var user = currentLoggedinUser; 217 | 218 | var defaultValues = { 219 | email: user.userName 220 | }; 221 | 222 | var prof = angular.extend(defaultValues, getUserData()); 223 | 224 | return prof; 225 | }; 226 | 227 | var saveProfile = function (data) { 228 | var deferred = $q.defer(); 229 | 230 | setUserData(null, angular.extend(getUserData(), data)); 231 | StorageService.setObject('userProfile', data); 232 | 233 | deferred.resolve(); 234 | 235 | return deferred.promise; 236 | }; 237 | 238 | var canLoginWithTwitter = function () { 239 | return false; 240 | }; 241 | 242 | var canEditProfileImage = function () { 243 | return true; 244 | }; 245 | 246 | service = { 247 | init: init, 248 | loadUnload: loadUnload, 249 | loadUnloadData: loadUnloadData, 250 | loadUserDataSuccess: loadUserDataSuccess, 251 | loadUserDataError: loadUserDataError, 252 | isUserDataLoaded: isUserDataLoaded, 253 | isUserLoggedIn: isUserLoggedIn, 254 | currentUser: currentUser, 255 | getUserProp: getUserProp, 256 | setUserProp: setUserProp, 257 | getUserData: getUserData, 258 | setUserData: setUserData, 259 | getUserRole: getUserRole, 260 | setUserRole: setUserRole, 261 | signup: signup, 262 | login: login, 263 | logoutApp: logoutApp, 264 | changePassword: changePassword, 265 | resetPassword: resetPassword, 266 | retrieveProfile: retrieveProfile, 267 | saveProfile: saveProfile, 268 | canLoginWithTwitter: canLoginWithTwitter, 269 | canEditProfileImage: canEditProfileImage 270 | }; 271 | 272 | return service; 273 | }) 274 | ; 275 | }()); 276 | -------------------------------------------------------------------------------- /src/js/app/app.js: -------------------------------------------------------------------------------- 1 | ;(function () { 2 | "use strict"; 3 | 4 | // 5 | // app.js 6 | // 7 | // Main application script 8 | // 9 | 10 | // Declare the 'app.config' module, this is because config.js is generated and doesn't the app.config module itself 11 | angular.module('app.config', []); 12 | 13 | // 14 | // Declare the main 'app' module and state its dependencies. All of the other modules will "declare themselves". 15 | // 16 | // NOTE: looked at gulp-angular-modules (https://github.com/yagoferrer/gulp-angular-modules) which should make it 17 | // possible to get rid of manually managing the list of dependencies. However, I couldn't get this to work. 18 | // 19 | angular.module('app', [ 20 | // libraries 21 | 'ionic', 22 | "firebase", 23 | 'ngCordova', 'ngMessages', 'fusionMessages', 24 | // angular-translate 25 | 'pascalprecht.translate', 26 | // ionic-content-banner 27 | 'jett.ionic.content.banner', 28 | // iblank/ngImgCrop 29 | 'ngImgCrop', 30 | // config 31 | 'app.config', 32 | // generic services 33 | 'app.util', 'app.firebase', 'app.hooks', 'app.user', 'app.oauthUtil', 'app.image', 34 | // controllers and routers 35 | 'app.intro', 'app.auth.signup', 'app.auth.login', 'app.auth.forgotPassword', 'app.auth.changePassword', 36 | 'app.mainPage', 'app.manage', 37 | // ANGULAR-TEMPLATECACHE 38 | 'templates' 39 | ]) 40 | .config(function ($stateProvider) { 41 | 42 | // top level routes (all other routes are defined within their own module) 43 | $stateProvider 44 | 45 | .state('app', { 46 | url: "/app", 47 | abstract: true, 48 | templateUrl: "js/app/menu/menu.html" 49 | }) 50 | 51 | .state('app.auth', { 52 | url: "/auth", 53 | abstract: true, 54 | template: '' 55 | }); 56 | }) 57 | 58 | .config(function ($ionicConfigProvider) { 59 | 60 | // http://forum.ionicframework.com/t/change-hide-ion-nav-back-button-text/5260/14 61 | // remove back button text, use unicode em space characters to increase touch target area size of back button 62 | $ionicConfigProvider.backButton.previousTitleText(false).text('  '); 63 | 64 | // NOTE: we put the tabs at the top for both Android and iOS 65 | $ionicConfigProvider.tabs.position("top"); 66 | 67 | //$ionicConfigProvider.navBar.alignTitle('center'); 68 | // 69 | //$ionicConfigProvider.navBar.positionPrimaryButtons('left'); 70 | //$ionicConfigProvider.navBar.positionSecondaryButtons('right'); 71 | 72 | // Solution for "disableScroll not working" (keyboard scrolling the view up too much on iOS), see: 73 | // https://forum.ionicframework.com/t/ionic-keyboard-scroll-issue-ios/34420/4 74 | $ionicConfigProvider.scrolling.jsScrolling(false); 75 | 76 | }) 77 | 78 | .config(function ($logProvider, APP) { 79 | 80 | // switch off debug logging in production 81 | $logProvider.debugEnabled(APP.devMode); // default is true 82 | }) 83 | 84 | .config(function ($compileProvider, APP) { 85 | 86 | // switch off AngularJS debug info in production for better performance 87 | $compileProvider.debugInfoEnabled(APP.devMode); 88 | }) 89 | 90 | .config(function ($translateProvider) { 91 | $translateProvider 92 | .useStaticFilesLoader({ 93 | prefix: 'js/locales/', 94 | suffix: '.json' 95 | }) 96 | .registerAvailableLanguageKeys(['en'], { 97 | 'en': 'en', 'en_GB': 'en', 'en_US': 'en' 98 | }) 99 | .preferredLanguage('en') 100 | .fallbackLanguage('en') 101 | .useSanitizeValueStrategy('escapeParameters'); 102 | }) 103 | 104 | .factory('$exceptionHandler', function ($log) { 105 | 106 | // global AngularJS exception handler, see: 107 | // http://blog.pdsullivan.com/posts/2015/02/19/ionicframework-googleanalytics-log-errors.html 108 | return function (exception, cause) { 109 | $log.error("error: " + exception + ', caused by "' + cause + '", stack: ' + exception.stack); 110 | }; 111 | }) 112 | 113 | .run(function ($ionicPlatform, $ionicPopup, $ionicSideMenuDelegate, $ionicHistory, $state, $rootScope, $translate, 114 | $log, $timeout, $cordovaDevice, loggingDecorator, Application, APP, UserService, 115 | FirebaseConfiguration) { 116 | 117 | loggingDecorator.decorate($log); 118 | 119 | if (FirebaseConfiguration.debug === true) { 120 | Firebase.enableLogging(function (logMessage) { 121 | //$log.log(new Date().toISOString() + ': ' + logMessage); 122 | $log.log('FB: ' + logMessage); 123 | }); 124 | } 125 | 126 | $rootScope.$on('$stateChangeError', 127 | function (event, toState, toParams, fromState, fromParams, error) { 128 | 129 | $log.debug('$stateChangeError, to: ' + JSON.stringify(toState) + ' error: ' + JSON.stringify(error)); 130 | }); 131 | 132 | function isValidUser() { 133 | if (!UserService.isUserLoggedIn()) { 134 | return false; 135 | } 136 | 137 | // 138 | // TO DO: we might check for the user role here, e.g: 139 | // 140 | // var userRole = UserService.getUserRole(); 141 | // 142 | // Then if the page we want to go to requires a specific role (e.g. "admin") then we might block access (by 143 | // returning 'false' if the user does not have that role, see this page which explains the technique: 144 | // 145 | // www.jvandemo.com/how-to-use-areas-and-border-states-to-control-access-in-an-angular-application-with-ui-router 146 | // 147 | 148 | // 149 | // Sample code to illustrate this (commented out for now): 150 | // 151 | 152 | //var userType = UserService.getUserType(); 153 | // 154 | //// if the userType is not known (not specified, or user data not loaded yet), then say that the user is 'valid' 155 | //if (!userType) { 156 | // $log.log('User type not yet known'); 157 | // return true; 158 | //} 159 | // 160 | //// userType should start with "admin" 161 | //var userTypeValid = userType.match(/admin/); 162 | // 163 | //$log.log("userType = '" + userType + "' userTypeValid = " + userTypeValid); 164 | // 165 | //return userTypeValid; 166 | 167 | return true; 168 | } 169 | 170 | function checkValidUser() { 171 | 172 | if (!isValidUser()) { 173 | $log.debug('APP - no valid user, redirect to login'); 174 | 175 | // redirect to login page 176 | $timeout(function () { 177 | $state.go('login', {}); 178 | }, 0); 179 | 180 | return false; 181 | } 182 | 183 | return true; 184 | } 185 | 186 | // www.jvandemo.com/how-to-use-areas-and-border-states-to-control-access-in-an-angular-application-with-ui-router/ 187 | $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) { 188 | // when state name matches 'app.auth.*' then login is required 189 | if (toState.name && toState.name.match(/^app\.auth\./)) { 190 | 191 | if (!isValidUser()) { 192 | 193 | // cancel state change 194 | event.preventDefault(); 195 | 196 | // redirect to login page 197 | return $state.go('login', {}); 198 | } 199 | } 200 | }); 201 | 202 | $rootScope.$on(UserService.loadUserDataSuccess(), function (event, userData) { 203 | $log.info('APP - user data loaded, userRole: ' + userData.userRole); 204 | 205 | // store the userRole back into the user object 206 | UserService.setUserRole(userData.userRole); 207 | 208 | // check valid user now that the user data has been loaded (so the user's role is know) 209 | checkValidUser(); 210 | }); 211 | 212 | $rootScope.$on(UserService.loadUserDataError(), function (event, error) { 213 | $log.error("APP - error loading user data"); 214 | 215 | // check valid user now that the user data has been loaded (so the user's role is know) 216 | checkValidUser(); 217 | }); 218 | 219 | $ionicPlatform.ready(function () { 220 | $log.info('IONIC PLATFORM READY'); 221 | 222 | Application.setIonicPlatformReady(true); 223 | 224 | // hide or show the accessory bar by default (set the value to false to show the accessory bar above the keyboard 225 | // for form inputs - see: https://github.com/driftyco/ionic-plugin-keyboard/issues/97 and 226 | // http://forum.ionicframework.com/t/ionic-select-is-missing-the-top-confirm-part-in-ios/30538 227 | if (window.cordova && window.cordova.plugins && window.cordova.plugins.Keyboard) { 228 | cordova.plugins.Keyboard.hideKeyboardAccessoryBar(false); 229 | } 230 | if (window.StatusBar) { // org.apache.cordova.statusbar required 231 | StatusBar.styleLightContent(); //StatusBar.styleDefault(); // ? 232 | } 233 | 234 | // Add the ability to close the side menu by swiping to te right, see: 235 | // http://forum.ionicframework.com/t/bug-ionic-beta-14-cant-close-sidemenu-with-swipe/14236/17 236 | document.addEventListener('touchstart', function (event) { 237 | if ($ionicSideMenuDelegate.isOpenLeft()) { 238 | event.preventDefault(); 239 | } 240 | }); 241 | 242 | Application.registerBackbuttonHandler(); 243 | Application.init(); 244 | 245 | checkDeviceReady(); 246 | 247 | Application.gotoStartPage($state); 248 | }); 249 | 250 | function checkDeviceReady() { 251 | 252 | if (window.cordova) { 253 | document.addEventListener("deviceready", function () { 254 | $log.info("checkDeviceReady: device is ready"); 255 | 256 | Application.setDeviceReady(true); 257 | 258 | var device = $cordovaDevice.getDevice(); 259 | $log.info("DEVICE: " + JSON.stringify(device)); 260 | 261 | //if (device && device.uuid) { 262 | // loggingService.setDeviceId(device.uuid); 263 | //} 264 | }, false); 265 | } 266 | } 267 | 268 | }); 269 | }()); 270 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // 2 | // BEGIN - gulp.js 3 | // 4 | 5 | // 6 | // === DEPENDENCIES === 7 | // 8 | 9 | var gulp = require('gulp'); 10 | var gutil = require('gulp-util'); 11 | var bower = require('bower'); 12 | var concat = require('gulp-concat'); 13 | var del = require('del'); 14 | var jshint = require('gulp-jshint'); 15 | var sass = require('gulp-sass'); 16 | var minifyCss = require('gulp-minify-css'); 17 | var sourcemap = require('gulp-sourcemaps'); 18 | var vinylPaths = require('vinyl-paths'); 19 | var rename = require('gulp-rename'); 20 | var ngAnnotate = require('gulp-ng-annotate'); 21 | var uglify = require("gulp-uglify"); 22 | var imagemin = require('gulp-imagemin'); 23 | var htmlreplace = require('gulp-html-replace'); 24 | var replace = require('gulp-replace'); 25 | var sh = require('shelljs'); 26 | // We load the karma module conditionally (only when it is needed: wen we need to run tests) to avoid problems with the Ionic 2 CLI, see: 27 | // http://stackoverflow.com/questions/35597719/log4js-and-winston-loggers-conflicting-when-trying-to-run-karma-from-ionic-cli 28 | var karma = null; 29 | var ngConstant = require('gulp-ng-constant'); 30 | var extend = require('gulp-extend'); 31 | var gulpif = require('gulp-if'); 32 | var minifyHtml = require('gulp-minify-html'); 33 | var templateCache = require('gulp-angular-templatecache'); 34 | var inject = require('gulp-inject'); 35 | 36 | // 37 | // === PATHS === 38 | // 39 | 40 | // Files 41 | // 42 | // Note: change the 'ionicbundle' entry below from 'ionic.bundle.min.js' to 'ionic.bundle.js' to debug "moduleErr" 43 | // errors in a production build ("gulp build") - see explanation: 44 | // 45 | // http://www.chrisgmyr.com/2014/08/debugging-uncaught-error-injectormodulerr-in-angularjs/ 46 | // 47 | // (the unminified ionic.bundle.js needs to have the extra console.log statement as explained in the article) 48 | // 49 | var files = { 50 | jsbundle: 'app.bundle.min.js', 51 | appcss: 'app.css', 52 | ionicappmincss: 'ionic.app.min.css', 53 | ionicbundle: 'ionic.bundle.min.js' // change to 'ionic.bundle.js' for debugging moduleErr errors 54 | }; 55 | // Paths 56 | var paths = { 57 | sass: ['./scss/**/*.scss'], 58 | css: ['./src/css/**/*.css'], 59 | scripts: [ 60 | './src/js/**/*.js', 61 | '!./src/js/lib/ng-img-crop-customized/ng-img-crop.js', /* exclude ng-img-crop.js: handled separately */ 62 | '!./src/js/lib/ng-img-crop-customized/ng-img-crop.min.js', /* exclude ng-img-crop.min.js: handled separately */ 63 | '!./src/js/lib/logentries/le.min.js', 64 | '!./src/js/config/config.js' /* exclude config.js: handled separately */ 65 | ], 66 | injectedScripts: [ 67 | './src/js/app/**/*.js', 68 | '!./src/js/app/app.js', /* exclude the root module ('app' module) files */ 69 | '!./src/js/app/application.ctrl.js', /* exclude the root module ('app' module) files */ 70 | '!./src/js/app/application.service.js' /* exclude the root module ('app' module) files */ 71 | ], 72 | images: ['./src/img/**/*'], 73 | templates: ['./src/js/**/*.html'], 74 | indexTemplate: ['./src/index-template.html'], 75 | index: ['./src/index.html'], 76 | locales: [ 77 | './src/js/locales/*.json' 78 | ], 79 | ionicfonts: ['./src/lib/ionic/fonts/*'], 80 | lib: [ 81 | './src/lib/ionic/js/' + files.ionicbundle, 82 | './src/lib/angular-resource/angular-resource.min.js', 83 | './src/lib/angular-messages/angular-messages.min.js', 84 | './src/lib/angular-elastic/elastic.js', 85 | './src/lib/ngCordova/dist/ng-cordova.min.js', 86 | './src/lib/ng-cordova-oauth/dist/ng-cordova-oauth.min.js', 87 | './src/lib/ionic-content-banner/dist/ionic.content.banner.min.js', 88 | './src/lib/angular-translate/angular-translate.min.js', 89 | './src/lib/angular-translate-loader-static-files/angular-translate-loader-static-files.min.js', 90 | './src/lib/fus-messages/dist/fus-messages.js', 91 | './src/lib/firebase/firebase.js', 92 | './src/lib/angularfire/dist/angularfire.min.js', 93 | './src/lib/jsSHA/src/sha1.js', 94 | './src/js/lib/ng-img-crop-customized/ng-img-crop.min.js', 95 | './src/js/lib/logentries/le.min.js' 96 | ], 97 | dist: ['./www'] 98 | }; 99 | 100 | // 101 | // === TOP LEVEL TASKS (invoke with "gulp ") === 102 | // 103 | 104 | // NOTE: the "serve:before" task replaces the "gulpStartupTasks" setting in ionic.project when using the Ionic 2 CLI, 105 | // see: https://forum.ionicframework.com/t/gulp-task-not-running-with-ionic-serve/54457/12 106 | gulp.task('serve:before', ['watch']); 107 | 108 | // default task for DEV 109 | 110 | gulp.task('default', ['dev-config', 'dev-sass', 'inject-index']); 111 | 112 | // watch task for DEV 113 | // 114 | // NOTE: inject-index does not run automatically within the "watch" task - so, if you add or remove Javascript files 115 | // and you want to inject them into index.html, then you just need to restart "ionic serve" so that the "default" task 116 | // can re-run 'inject-index'. 117 | 118 | gulp.task('watch', ['default'], function() { 119 | gulp.watch(paths.sass, ['dev-sass']); 120 | }); 121 | 122 | // karma tasks for TEST 123 | 124 | var runtest = function (single) { 125 | 126 | if (karma == null) { 127 | karma = require('karma').server; 128 | } 129 | 130 | karma.start({ 131 | configFile: __dirname + '/karma.conf.js', 132 | singleRun: single, 133 | autoWatch: !single 134 | }); 135 | }; 136 | 137 | gulp.task('test', function (done) { 138 | runtest(false), done; 139 | }); 140 | 141 | gulp.task('test-single', function (done) { 142 | runtest(true), done; 143 | }); 144 | 145 | // build task for PROD 146 | 147 | // Note: use before 'ionic build' or 'ionic run'. 148 | // See: https://github.com/driftyco/ionic-cli/issues/345#issuecomment-88659079 149 | gulp.task('build', ['clean', 'sass', 'scripts', 'prod-config', 'imagemin', 'templates', 150 | 'inject-index', 'index', 'copy']); 151 | 152 | // utility tasks for DEV/PROD/TEST (whichever) 153 | 154 | gulp.task('jshint', function() { 155 | gulp.src(paths.scripts) 156 | .pipe(jshint()) 157 | .pipe(jshint.reporter('default')); 158 | }); 159 | 160 | // 161 | // === CHILD TASKS === 162 | // 163 | 164 | // use 'del' instead of 'clean', see: https://github.com/gulpjs/gulp/blob/master/docs/recipes/delete-files-folder.md 165 | gulp.task('clean', function (cb) { 166 | del([ 167 | paths.dist + '/**/*' 168 | ], cb); 169 | }); 170 | 171 | var dosass = function(minify, sourcemaps, done) { 172 | gulp.src('./scss/ionic.app.scss') 173 | .pipe(sass({includePaths: [ 'src/lib/ionic/scss/' ]})) 174 | // this keeps the gulp build from crashing when there are errors in your SASS file 175 | .on("error", function(err) { 176 | console.log(err.toString()); 177 | this.emit("end"); 178 | }) 179 | .pipe(gulp.dest(paths.dist + '/css/')) 180 | .pipe(gulpif(sourcemaps, sourcemap.init())) 181 | .pipe(gulpif(minify, minifyCss({ 182 | keepSpecialComments: 0 183 | }))) 184 | // delete the source file, see: https://github.com/gulpjs/gulp/blob/master/docs/recipes/delete-files-folder.md#delete-files-in-a-pipeline 185 | .pipe(vinylPaths(del)) 186 | .pipe(gulpif(sourcemaps, sourcemap.write())) 187 | .pipe(gulpif(minify, rename({ extname: '.min.css' }))) 188 | .pipe(gulp.dest('./src/css/')) 189 | .on('end', done); 190 | }; 191 | 192 | gulp.task('sass', ['clean'], function(done) { 193 | dosass( 194 | true, 195 | false, // set to TRUE to get source maps 196 | done 197 | ); 198 | }); 199 | 200 | gulp.task('dev-sass', function(done) { 201 | dosass( 202 | false, 203 | false, 204 | done 205 | ); 206 | }); 207 | 208 | // scripts - clean dist dir then annotate, uglify, concat 209 | gulp.task('scripts', ['clean'], function() { 210 | gulp.src(paths.scripts) 211 | .pipe(ngAnnotate({ 212 | remove: true, 213 | add: true, 214 | single_quotes: true, 215 | regexp: "appModule(.*)$" /* NOTE: this makes ngAnnotate work right even when defining modules with "appModule(...)" instead of "angular.module()" */ 216 | })) 217 | .pipe(uglify()) 218 | .pipe(concat(files.jsbundle)) 219 | .pipe(gulp.dest(paths.dist + '/js')); 220 | }); 221 | 222 | var config = function(src, dest) { 223 | gulp.src(['src/js/config/config-base.json', src]) 224 | .pipe(extend('config.json', true)) 225 | .pipe(ngConstant({ 226 | deps: false 227 | })) 228 | .pipe(rename(function(path) { 229 | path.basename = 'config'; 230 | path.extname = '.js'; 231 | })) 232 | .pipe(gulp.dest(dest)); 233 | }; 234 | 235 | gulp.task('prod-config', ['clean'], function() { 236 | config('src/js/config/config-prod.json', paths.dist + '/js/config') 237 | }); 238 | 239 | gulp.task('dev-config', function() { 240 | config('src/js/config/config-dev.json', 'src/js/config') 241 | }); 242 | 243 | // imagemin images and output them in dist 244 | gulp.task('imagemin', ['clean'], function() { 245 | gulp.src(paths.images) 246 | //.pipe(imagemin()) // NOTE: TOOK OUT IMAGE MINIFICATION BECAUSE IT DOES NOT WORK ANYMORE (DUE TO NODEJS UPGRADE?) 247 | .pipe(gulp.dest(paths.dist + '/img')); 248 | }); 249 | 250 | // inspired by: http://forum.ionicframework.com/t/could-i-set-up-ionic-in-such-a-way-that-it-downloads-an-entire-spa-upfront/7565/5 251 | gulp.task('templates', ['clean', 'scripts'], function() { 252 | gulp.src(paths.templates) 253 | .pipe(minifyHtml({empty: true})) 254 | .pipe(templateCache({ 255 | standalone: true, 256 | root: 'js' 257 | })) 258 | .pipe(gulp.dest(paths.dist + '/js')); 259 | }); 260 | 261 | // inject javascript paths into index-template.html, producing index.html 262 | gulp.task('inject-index', function() { 263 | gulp.src(paths.indexTemplate) 264 | .pipe(inject( 265 | gulp.src(paths.injectedScripts, 266 | {read: false}), {relative: true})) 267 | .pipe(rename(function(path) { 268 | path.basename = 'index'; 269 | path.extname = '.html'; 270 | })) 271 | .pipe(gulp.dest('./src/')); 272 | }); 273 | 274 | // prepare index.html for dist - i.e. using minimized files 275 | gulp.task('index', ['clean', 'inject-index'], function() { 276 | gulp.src(paths.index) 277 | .pipe(htmlreplace({ 278 | 'sass': 'css/ionic.app.min.css', 279 | 'css': 'css/app.min.css', 280 | 'js': 'js/app.bundle.min.js', 281 | 'ionic': 'lib/ionic/js/' + files.ionicbundle 282 | })) 283 | // https://www.airpair.com/ionic-framework/posts/production-ready-apps-with-ionic-framework 284 | .pipe(replace(/