├── .editorconfig ├── .gitignore ├── README.md ├── config.xml ├── hooks ├── README.md └── after_prepare │ └── 010_add_platform_class.js ├── ionic.config.json ├── licence ├── package.json ├── resources ├── android │ ├── icon │ │ ├── drawable-hdpi-icon.png │ │ ├── drawable-ldpi-icon.png │ │ ├── drawable-mdpi-icon.png │ │ ├── drawable-xhdpi-icon.png │ │ ├── drawable-xxhdpi-icon.png │ │ └── drawable-xxxhdpi-icon.png │ └── splash │ │ ├── drawable-land-hdpi-screen.png │ │ ├── drawable-land-ldpi-screen.png │ │ ├── drawable-land-mdpi-screen.png │ │ ├── drawable-land-xhdpi-screen.png │ │ ├── drawable-land-xxhdpi-screen.png │ │ ├── drawable-land-xxxhdpi-screen.png │ │ ├── drawable-port-hdpi-screen.png │ │ ├── drawable-port-ldpi-screen.png │ │ ├── drawable-port-mdpi-screen.png │ │ ├── drawable-port-xhdpi-screen.png │ │ ├── drawable-port-xxhdpi-screen.png │ │ └── drawable-port-xxxhdpi-screen.png ├── icon.png ├── ios │ ├── icon │ │ ├── icon-40.png │ │ ├── icon-40@2x.png │ │ ├── icon-50.png │ │ ├── icon-50@2x.png │ │ ├── icon-60.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-72.png │ │ ├── icon-72@2x.png │ │ ├── icon-76.png │ │ ├── icon-76@2x.png │ │ ├── icon-small.png │ │ ├── icon-small@2x.png │ │ ├── icon-small@3x.png │ │ ├── icon.png │ │ └── icon@2x.png │ └── splash │ │ ├── Default-568h@2x~iphone.png │ │ ├── Default-667h.png │ │ ├── Default-736h.png │ │ ├── Default-Landscape-736h.png │ │ ├── Default-Landscape@2x~ipad.png │ │ ├── Default-Landscape~ipad.png │ │ ├── Default-Portrait@2x~ipad.png │ │ ├── Default-Portrait~ipad.png │ │ ├── Default@2x~iphone.png │ │ └── Default~iphone.png └── splash.png ├── src ├── app │ ├── app.component.ts │ ├── app.html │ ├── app.module.ts │ ├── main.dev.ts │ └── main.prod.ts ├── assets │ ├── icon │ │ └── favicon.ico │ ├── images │ │ ├── balls.gif │ │ ├── firebase.png │ │ ├── flickr.gif │ │ ├── github.jpg │ │ ├── profile.png │ │ ├── ring.gif │ │ └── wordpress.png │ ├── manifest.json │ └── service-worker.js ├── declarations.d.ts ├── index.html ├── manifest.json ├── pages │ ├── about │ │ ├── about.html │ │ ├── about.scss │ │ └── about.ts │ ├── comment-create │ │ ├── comment-create.html │ │ ├── comment-create.ts │ │ └── comment.create.scss │ ├── login │ │ ├── login.html │ │ ├── login.scss │ │ └── login.ts │ ├── profile │ │ ├── profile.html │ │ ├── profile.scss │ │ └── profile.ts │ ├── signup │ │ ├── signup.html │ │ ├── signup.scss │ │ └── signup.ts │ ├── tabs │ │ ├── tabs.html │ │ ├── tabs.scss │ │ └── tabs.ts │ ├── thread-comments │ │ ├── thread-comments.html │ │ ├── thread-comments.scss │ │ └── thread-comments.ts │ ├── thread-create │ │ ├── thread-create.html │ │ ├── thread-create.scss │ │ └── thread-create.ts │ └── threads │ │ ├── threads.html │ │ ├── threads.scss │ │ └── threads.ts ├── providers │ └── app.providers.ts ├── service-worker.js ├── shared │ ├── components │ │ ├── thread.component.html │ │ ├── thread.component.ts │ │ └── user-avatar.component.ts │ ├── interfaces.ts │ ├── services │ │ ├── auth.service.ts │ │ ├── data.service.ts │ │ ├── items.service.ts │ │ ├── mappings.service.ts │ │ └── sqlite.service.ts │ └── validators │ │ ├── checked.validator.ts │ │ └── email.validator.ts └── theme │ ├── app.variables.scss │ ├── global.scss │ └── variables.scss ├── tsconfig.json ├── tslint.json ├── typings.json └── www └── index.html /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | 10 | # We recommend you to keep these unchanged 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/**/*.js 2 | app/**/*.map 3 | www/index.html 4 | www/manifest.json 5 | www/service-worker.js 6 | 7 | # Specifies intentionally untracked files to ignore when using Git 8 | # http://git-scm.com/docs/gitignore 9 | 10 | *~ 11 | *.sw[mnpcod] 12 | *.log 13 | *.tmp 14 | *.tmp.* 15 | log.txt 16 | *.sublime-project 17 | *.sublime-workspace 18 | .vscode/ 19 | npm-debug.log* 20 | 21 | .idea/ 22 | .sass-cache/ 23 | .tmp/ 24 | .versions/ 25 | coverage/ 26 | dist/ 27 | node_modules/ 28 | tmp/ 29 | temp/ 30 | hooks/ 31 | platforms/ 32 | plugins/ 33 | plugins/android.json 34 | plugins/ios.json 35 | www/assets 36 | www/build 37 | $RECYCLE.BIN/ 38 | 39 | .DS_Store 40 | Thumbs.db 41 | UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Building hybrid mobile applications using Ionic 2 and Firebase

2 | 3 | Blog post: Building hybrid mobile apps using Ionic 2 and Firebase 4 | 5 | aspnet5-agnular2-03 6 | 7 |

Frameworks - Tools - Libraries

8 | 14 | 15 |

Forum app's features

16 | 23 | 24 |

Installation instructions - Part 1 (Firebase)

25 | 26 | 1. Login in Firebase with your Google account. 27 | 2. Click the Go to console button and press CREATE NEW PROJECT. 28 | 3. Name the project ForumApp and choose your country. 29 | 4. While in the ForumApp console, click the Auth button and select the SIGN-IN METHOD tab. Enable the Email/Password provider and click SAVE. 30 | 5. Click Database from the left menu and select the RULES tab. Set the JSON object as follow: 31 | 32 | ```javascript 33 | { 34 | "rules": { 35 | ".read": "auth != null", 36 | ".write": "auth != null", 37 | "statistics" : { 38 | "threads": { 39 | // /statistics/threads is readable by the world 40 | ".read": true, 41 | // /statistics/threads is writable by the world 42 | ".write": true 43 | } 44 | }, 45 | "threads" : { 46 | // /threads is readable by the world 47 | ".read": true, 48 | // /threads is writable by the world 49 | ".write": true 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | 6. Click Storage from the left menu and select the RULES tab. Set the JSON object as follow: 56 | 57 | ```javascript 58 | function() { 59 | service firebase.storage { 60 | match /b/forumapp-your_id.appspot.com/o { 61 | match /{allPaths=**} { 62 | allow read; 63 | allow write: if request.auth != null; 64 | } 65 | } 66 | } 67 | ``` 68 | Make sure to replace the your_id with your's. 69 | 70 |

Installation instructions - Part 2 (Ionic 2 Forum app)

71 | 1. Clone or download the source code of this repository. 72 | 2. Open the forum app in your IDE of your preference. 73 | 3. Run the following commands in the exact order. 74 | 75 | ``` 76 | npm install -g ionic 77 | npm install -g cordova 78 | npm install 79 | ionic plugin add com-sarriaroman-photoviewer 80 | ionic plugin add cordova-plugin-camera 81 | ionic plugin add cordova-plugin-inappbrowser 82 | ionic plugin add cordova-sqlite-storage 83 | ionic plugin add cordova-plugin-network-information 84 | ionic plugin add cordova-plugin-splashscreen 85 | ``` 86 | 87 |

Running the app

88 | 121 | 122 |

Forum app preview

123 | 124 | 125 | 126 | 127 |

Donations

128 | For being part of open source projects and documenting my work here and on chsakell's blog I really do not charge anything. I try to avoid any type of ads also. 129 | 130 | If you think that any information you obtained here is worth of some money and are willing to pay for it, feel free to send any amount through paypal. 131 | 132 | 133 | 134 | 135 | 136 | 139 | 140 | 141 |
Paypal
137 | Buy me a beer 138 |
142 | 143 |

Follow chsakell's Blog

144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 161 | 164 | 165 | 166 |
FacebookTwitter
Microsoft Web Application Development
159 | facebook 160 | 162 | twitter-small 163 |
167 |

License

168 | Code released under the MIT license. 169 | -------------------------------------------------------------------------------- /config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | forum-app 4 | An Ionic Framework and Cordova project. 5 | Ionic Framework Team 6 | 7 | 8 | 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 | -------------------------------------------------------------------------------- /hooks/README.md: -------------------------------------------------------------------------------- 1 | 21 | # Cordova Hooks 22 | 23 | Cordova Hooks represent special scripts which could be added by application and plugin developers or even by your own build system to customize cordova commands. Hook scripts could be defined by adding them to the special predefined folder (`/hooks`) or via configuration files (`config.xml` and `plugin.xml`) and run serially in the following order: 24 | * Application hooks from `/hooks`; 25 | * Application hooks from `config.xml`; 26 | * Plugin hooks from `plugins/.../plugin.xml`. 27 | 28 | __Remember__: Make your scripts executable. 29 | 30 | __Note__: `.cordova/hooks` directory is also supported for backward compatibility, but we don't recommend using it as it is deprecated. 31 | 32 | ## Supported hook types 33 | The following hook types are supported: 34 | 35 | after_build/ 36 | after_compile/ 37 | after_docs/ 38 | after_emulate/ 39 | after_platform_add/ 40 | after_platform_rm/ 41 | after_platform_ls/ 42 | after_plugin_add/ 43 | after_plugin_ls/ 44 | after_plugin_rm/ 45 | after_plugin_search/ 46 | after_plugin_install/ <-- Plugin hooks defined in plugin.xml are executed exclusively for a plugin being installed 47 | after_prepare/ 48 | after_run/ 49 | after_serve/ 50 | before_build/ 51 | before_compile/ 52 | before_docs/ 53 | before_emulate/ 54 | before_platform_add/ 55 | before_platform_rm/ 56 | before_platform_ls/ 57 | before_plugin_add/ 58 | before_plugin_ls/ 59 | before_plugin_rm/ 60 | before_plugin_search/ 61 | before_plugin_install/ <-- Plugin hooks defined in plugin.xml are executed exclusively for a plugin being installed 62 | before_plugin_uninstall/ <-- Plugin hooks defined in plugin.xml are executed exclusively for a plugin being uninstalled 63 | before_prepare/ 64 | before_run/ 65 | before_serve/ 66 | pre_package/ <-- Windows 8 and Windows Phone only. 67 | 68 | ## Ways to define hooks 69 | ### Via '/hooks' directory 70 | To execute custom action when corresponding hook type is fired, use hook type as a name for a subfolder inside 'hooks' directory and place you script file here, for example: 71 | 72 | # script file will be automatically executed after each build 73 | hooks/after_build/after_build_custom_action.js 74 | 75 | 76 | ### Config.xml 77 | 78 | Hooks can be defined in project's `config.xml` using `` elements, for example: 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | ... 89 | 90 | 91 | 92 | 93 | 94 | 95 | ... 96 | 97 | 98 | ### Plugin hooks (plugin.xml) 99 | 100 | As a plugin developer you can define hook scripts using `` elements in a `plugin.xml` like that: 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | ... 109 | 110 | 111 | `before_plugin_install`, `after_plugin_install`, `before_plugin_uninstall` plugin hooks will be fired exclusively for the plugin being installed/uninstalled. 112 | 113 | ## Script Interface 114 | 115 | ### Javascript 116 | 117 | If you are writing hooks in Javascript you should use the following module definition: 118 | ```javascript 119 | module.exports = function(context) { 120 | ... 121 | } 122 | ``` 123 | 124 | You can make your scipts async using Q: 125 | ```javascript 126 | module.exports = function(context) { 127 | var Q = context.requireCordovaModule('q'); 128 | var deferral = new Q.defer(); 129 | 130 | setTimeout(function(){ 131 | console.log('hook.js>> end'); 132 | deferral.resolve(); 133 | }, 1000); 134 | 135 | return deferral.promise; 136 | } 137 | ``` 138 | 139 | `context` object contains hook type, executed script full path, hook options, command-line arguments passed to Cordova and top-level "cordova" object: 140 | ```json 141 | { 142 | "hook": "before_plugin_install", 143 | "scriptLocation": "c:\\script\\full\\path\\appBeforePluginInstall.js", 144 | "cmdLine": "The\\exact\\command\\cordova\\run\\with arguments", 145 | "opts": { 146 | "projectRoot":"C:\\path\\to\\the\\project", 147 | "cordova": { 148 | "platforms": ["wp8"], 149 | "plugins": ["com.plugin.withhooks"], 150 | "version": "0.21.7-dev" 151 | }, 152 | "plugin": { 153 | "id": "com.plugin.withhooks", 154 | "pluginInfo": { 155 | ... 156 | }, 157 | "platform": "wp8", 158 | "dir": "C:\\path\\to\\the\\project\\plugins\\com.plugin.withhooks" 159 | } 160 | }, 161 | "cordova": {...} 162 | } 163 | 164 | ``` 165 | `context.opts.plugin` object will only be passed to plugin hooks scripts. 166 | 167 | You can also require additional Cordova modules in your script using `context.requireCordovaModule` in the following way: 168 | ```javascript 169 | var Q = context.requireCordovaModule('q'); 170 | ``` 171 | 172 | __Note__: new module loader script interface is used for the `.js` files defined via `config.xml` or `plugin.xml` only. 173 | For compatibility reasons hook files specified via `/hooks` folders are run via Node child_process spawn, see 'Non-javascript' section below. 174 | 175 | ### Non-javascript 176 | 177 | Non-javascript scripts are run via Node child_process spawn 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: 178 | 179 | * CORDOVA_VERSION - The version of the Cordova-CLI. 180 | * CORDOVA_PLATFORMS - Comma separated list of platforms that the command applies to (e.g.: android, ios). 181 | * CORDOVA_PLUGINS - Comma separated list of plugin IDs that the command applies to (e.g.: org.apache.cordova.file, org.apache.cordova.file-transfer) 182 | * CORDOVA_HOOK - Path to the hook that is being executed. 183 | * CORDOVA_CMDLINE - The exact command-line arguments passed to cordova (e.g.: cordova run ios --emulate) 184 | 185 | If a script returns a non-zero exit code, then the parent cordova command will be aborted. 186 | 187 | ## Writing hooks 188 | 189 | We highly recommend writing your hooks using Node.js so that they are 190 | cross-platform. Some good examples are shown here: 191 | 192 | [http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/](http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/) 193 | 194 | Also, note that even if you are working on Windows, and in case your hook scripts aren't bat files (which is recommended, if you want your scripts to work in non-Windows operating systems) Cordova CLI will expect a shebang line as the first line for it to know the interpreter it needs to use to launch the script. The shebang line should match the following example: 195 | 196 | #!/usr/bin/env [name_of_interpreter_executable] 197 | -------------------------------------------------------------------------------- /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 { 38 | if (window.cordova) { 39 | // Okay, so the platform is ready and our plugins are available. 40 | // Here you can do any higher level native things you might need. 41 | StatusBar.styleDefault(); 42 | self.watchForConnection(); 43 | self.watchForDisconnect(); 44 | Splashscreen.hide(); 45 | 46 | console.log('in ready..'); 47 | let array: string[] = platform.platforms(); 48 | console.log(array); 49 | self.sqliteService.InitDatabase(); 50 | } 51 | }); 52 | } 53 | 54 | watchForConnection() { 55 | var self = this; 56 | Network.onConnect().subscribe(() => { 57 | console.log('network connected!'); 58 | // We just got a connection but we need to wait briefly 59 | // before we determine the connection type. Might need to wait 60 | // prior to doing any api requests as well. 61 | setTimeout(() => { 62 | console.log('we got a connection..'); 63 | console.log('Firebase: Go Online..'); 64 | self.dataService.goOnline(); 65 | self.events.publish('network:connected', true); 66 | }, 3000); 67 | }); 68 | } 69 | 70 | watchForDisconnect() { 71 | var self = this; 72 | // watch network for a disconnect 73 | Network.onDisconnect().subscribe(() => { 74 | console.log('network was disconnected :-('); 75 | console.log('Firebase: Go Offline..'); 76 | //self.sqliteService.resetDatabase(); 77 | self.dataService.goOffline(); 78 | self.events.publish('network:connected', false); 79 | }); 80 | } 81 | 82 | hideSplashScreen() { 83 | if (Splashscreen) { 84 | setTimeout(() => { 85 | Splashscreen.hide(); 86 | }, 100); 87 | } 88 | } 89 | 90 | ngOnInit() { 91 | 92 | } 93 | 94 | ngAfterViewInit() { 95 | var self = this; 96 | 97 | this.authService.onAuthStateChanged(function (user) { 98 | if (user === null) { 99 | self.menu.close(); 100 | //self.nav.setRoot(LoginPage); 101 | 102 | let loginodal = self.modalCtrl.create(LoginPage); 103 | loginodal.present(); 104 | } 105 | }); 106 | } 107 | 108 | openPage(page) { 109 | let viewCtrl: ViewController = this.nav.getActive(); 110 | // close the menu when clicking a link from the menu 111 | this.menu.close(); 112 | 113 | if (page === 'signup') { 114 | if (!(viewCtrl.instance instanceof SignupPage)) 115 | this.nav.push(SignupPage); 116 | } 117 | } 118 | 119 | signout() { 120 | var self = this; 121 | self.menu.close(); 122 | self.authService.signOut(); 123 | } 124 | 125 | isUserLoggedIn(): boolean { 126 | let user = this.authService.getLoggedInUser(); 127 | return user !== null; 128 | } 129 | } -------------------------------------------------------------------------------- /src/app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Menu 4 | 5 | 6 | 7 | 8 | Account 9 | 10 | 11 | 12 | 13 | Register 14 | 15 | 16 | 17 | 18 | Sign out 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { HttpModule } from '@angular/http'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { IonicApp, IonicModule } from 'ionic-angular'; 5 | import { ForumApp } from './app.component'; 6 | // Pages 7 | import { AboutPage } from '../pages/about/about'; 8 | import { CommentCreatePage } from '../pages/comment-create/comment-create'; 9 | import { LoginPage } from '../pages/login/login'; 10 | import { ProfilePage } from '../pages/profile/profile'; 11 | import { SignupPage } from '../pages/signup/signup'; 12 | import { TabsPage } from '../pages/tabs/tabs'; 13 | import { ThreadCommentsPage } from '../pages/thread-comments/thread-comments'; 14 | import { ThreadCreatePage } from '../pages/thread-create/thread-create'; 15 | import { ThreadsPage } from '../pages/threads/threads'; 16 | // Custom components 17 | import { ThreadComponent } from '../shared/components/thread.component'; 18 | import { UserAvatarComponent } from '../shared/components/user-avatar.component'; 19 | // providers 20 | import { APP_PROVIDERS } from '../providers/app.providers'; 21 | 22 | @NgModule({ 23 | declarations: [ 24 | ForumApp, 25 | AboutPage, 26 | CommentCreatePage, 27 | LoginPage, 28 | ProfilePage, 29 | SignupPage, 30 | TabsPage, 31 | ThreadCommentsPage, 32 | ThreadCreatePage, 33 | ThreadsPage, 34 | ThreadComponent, 35 | UserAvatarComponent 36 | ], 37 | imports: [ 38 | IonicModule.forRoot(ForumApp), 39 | HttpModule, 40 | FormsModule 41 | ], 42 | bootstrap: [IonicApp], 43 | entryComponents: [ 44 | ForumApp, 45 | AboutPage, 46 | CommentCreatePage, 47 | LoginPage, 48 | ProfilePage, 49 | SignupPage, 50 | TabsPage, 51 | ThreadCommentsPage, 52 | ThreadCreatePage, 53 | ThreadsPage 54 | ], 55 | providers: [APP_PROVIDERS] 56 | }) 57 | export class AppModule {} 58 | -------------------------------------------------------------------------------- /src/app/main.dev.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app.module'; 4 | 5 | platformBrowserDynamic().bootstrapModule(AppModule); 6 | -------------------------------------------------------------------------------- /src/app/main.prod.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowser } from '@angular/platform-browser'; 2 | import { enableProdMode } from '@angular/core'; 3 | 4 | import { AppModuleNgFactory } from './app.module.ngfactory'; 5 | 6 | enableProdMode(); 7 | platformBrowser().bootstrapModuleFactory(AppModuleNgFactory); 8 | -------------------------------------------------------------------------------- /src/assets/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chsakell/ionic2-angular2-firebase/c53229bc94fb19065fa0c75686c6e1a8f6e8c361/src/assets/icon/favicon.ico -------------------------------------------------------------------------------- /src/assets/images/balls.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chsakell/ionic2-angular2-firebase/c53229bc94fb19065fa0c75686c6e1a8f6e8c361/src/assets/images/balls.gif -------------------------------------------------------------------------------- /src/assets/images/firebase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chsakell/ionic2-angular2-firebase/c53229bc94fb19065fa0c75686c6e1a8f6e8c361/src/assets/images/firebase.png -------------------------------------------------------------------------------- /src/assets/images/flickr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chsakell/ionic2-angular2-firebase/c53229bc94fb19065fa0c75686c6e1a8f6e8c361/src/assets/images/flickr.gif -------------------------------------------------------------------------------- /src/assets/images/github.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chsakell/ionic2-angular2-firebase/c53229bc94fb19065fa0c75686c6e1a8f6e8c361/src/assets/images/github.jpg -------------------------------------------------------------------------------- /src/assets/images/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chsakell/ionic2-angular2-firebase/c53229bc94fb19065fa0c75686c6e1a8f6e8c361/src/assets/images/profile.png -------------------------------------------------------------------------------- /src/assets/images/ring.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chsakell/ionic2-angular2-firebase/c53229bc94fb19065fa0c75686c6e1a8f6e8c361/src/assets/images/ring.gif -------------------------------------------------------------------------------- /src/assets/images/wordpress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chsakell/ionic2-angular2-firebase/c53229bc94fb19065fa0c75686c6e1a8f6e8c361/src/assets/images/wordpress.png -------------------------------------------------------------------------------- /src/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ionic", 3 | "short_name": "Ionic", 4 | "start_url": "index.html", 5 | "display": "standalone", 6 | "icons": [{ 7 | "src": "assets/imgs/logo.png", 8 | "sizes": "512x512", 9 | "type": "image/png" 10 | }], 11 | "background_color": "#4e8ef7", 12 | "theme_color": "#4e8ef7" 13 | } -------------------------------------------------------------------------------- /src/assets/service-worker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('activate', function (event) { 2 | 3 | }); 4 | 5 | self.addEventListener('fetch', function (event) { 6 | 7 | }); 8 | 9 | self.addEventListener('push', function (event) { 10 | 11 | }); -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'lodash'; -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ionic App 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ionic", 3 | "short_name": "Ionic", 4 | "start_url": "index.html", 5 | "display": "standalone", 6 | "icons": [{ 7 | "src": "assets/imgs/logo.png", 8 | "sizes": "512x512", 9 | "type": "image/png" 10 | }], 11 | "background_color": "#4e8ef7", 12 | "theme_color": "#4e8ef7" 13 | } -------------------------------------------------------------------------------- /src/pages/about/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | About 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | chsakell's Blog 15 | 16 |

17 | This app is a genuine contribution by Chris Sakellarios. Step by step walkthrough on how to build hybrid-mobile apps using 18 | Ionic 2, Angular 2 and Firebase 19 |

20 |
21 | 22 | 23 | 27 | 28 | 29 | 33 | 34 | 35 | 39 | 40 | 41 |
42 | 43 | 44 | 45 | 46 | Github repository 47 | 48 |

49 | Application's source code is fully available on Github and distributed under MIT licence. 50 |

51 |
52 | 53 | 54 | 58 | 59 | 60 | 64 | 65 | 66 |
67 | 68 | 69 | 70 | 71 | Built on Firebase 72 | 73 |

74 | Application makes use of the powerfull Firebase data store. 75 |

76 |
77 |
78 |
-------------------------------------------------------------------------------- /src/pages/about/about.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chsakell/ionic2-angular2-firebase/c53229bc94fb19065fa0c75686c6e1a8f6e8c361/src/pages/about/about.scss -------------------------------------------------------------------------------- /src/pages/about/about.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {NavController} from 'ionic-angular'; 3 | import { InAppBrowser } from 'ionic-native'; 4 | 5 | @Component({ 6 | templateUrl: 'about.html' 7 | }) 8 | export class AboutPage { 9 | 10 | constructor(private navCtrl: NavController) { 11 | } 12 | 13 | openUrl(url) { 14 | let browser = new InAppBrowser(url, '_blank', 'location=yes'); 15 | } 16 | } -------------------------------------------------------------------------------- /src/pages/comment-create/comment-create.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | New Comment 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | Comment 16 | 17 | 18 |
* Comment is required.
19 |
* Type at least 100 characters.
20 |

21 | 22 |
23 |
-------------------------------------------------------------------------------- /src/pages/comment-create/comment-create.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NavController, ViewController, LoadingController, NavParams } from 'ionic-angular'; 3 | import { FormBuilder, FormGroup, Validators, AbstractControl} from '@angular/forms'; 4 | 5 | import { IComment, IUser } from '../../shared/interfaces'; 6 | import { AuthService } from '../../shared/services/auth.service'; 7 | import { DataService } from '../../shared/services/data.service'; 8 | 9 | @Component({ 10 | templateUrl: 'comment-create.html' 11 | }) 12 | export class CommentCreatePage implements OnInit { 13 | 14 | createCommentForm: FormGroup; 15 | comment: AbstractControl; 16 | threadKey: string; 17 | loaded: boolean = false; 18 | 19 | constructor(public nav: NavController, 20 | public navParams: NavParams, 21 | public loadingCtrl: LoadingController, 22 | public viewCtrl: ViewController, 23 | public fb: FormBuilder, 24 | public authService: AuthService, 25 | public dataService: DataService) { 26 | 27 | } 28 | 29 | ngOnInit() { 30 | this.threadKey = this.navParams.get('threadKey'); 31 | 32 | this.createCommentForm = this.fb.group({ 33 | 'comment': ['', Validators.compose([Validators.required, Validators.minLength(10)])] 34 | }); 35 | 36 | this.comment = this.createCommentForm.controls['comment']; 37 | this.loaded = true; 38 | } 39 | 40 | cancelNewComment() { 41 | this.viewCtrl.dismiss(); 42 | } 43 | 44 | onSubmit(commentForm: any): void { 45 | var self = this; 46 | if (this.createCommentForm.valid) { 47 | 48 | let loader = this.loadingCtrl.create({ 49 | content: 'Posting comment...', 50 | dismissOnPageChange: true 51 | }); 52 | 53 | loader.present(); 54 | 55 | let uid = self.authService.getLoggedInUser().uid; 56 | self.dataService.getUsername(uid).then(function (snapshot) { 57 | let username = snapshot.val(); 58 | 59 | let commentRef = self.dataService.getCommentsRef().push(); 60 | let commentkey: string = commentRef.key; 61 | let user: IUser = { uid: uid, username: username }; 62 | 63 | let newComment: IComment = { 64 | key: commentkey, 65 | text: commentForm.comment, 66 | thread: self.threadKey, 67 | user: user, 68 | dateCreated: new Date().toString(), 69 | votesUp: null, 70 | votesDown: null 71 | }; 72 | 73 | self.dataService.submitComment(self.threadKey, newComment) 74 | .then(function (snapshot) { 75 | loader.dismiss() 76 | .then(() => { 77 | self.viewCtrl.dismiss({ 78 | comment: newComment, 79 | user: user 80 | }); 81 | }); 82 | }, function (error) { 83 | // The Promise was rejected. 84 | console.error(error); 85 | loader.dismiss(); 86 | }); 87 | }); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/pages/comment-create/comment.create.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chsakell/ionic2-angular2-firebase/c53229bc94fb19065fa0c75686c6e1a8f6e8c361/src/pages/comment-create/comment.create.scss -------------------------------------------------------------------------------- /src/pages/login/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Login 4 | 5 | 6 | 7 |
8 | 9 | Email address 10 | 11 | 12 |
* Email is required.
13 |
* Enter a valid email address.
14 | 15 | Password 16 | 17 | 18 |
* Password is required.
19 |
* Minimum password length is 5.
20 |

21 | 22 |
23 | 26 | 27 | 28 | 29 | 30 | Built on Firebase 31 | 32 |

33 | Create a Firebase profile for free and use your email and password to sign in to Forum-App 34 |

35 |
36 |
37 |
38 |
-------------------------------------------------------------------------------- /src/pages/login/login.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chsakell/ionic2-angular2-firebase/c53229bc94fb19065fa0c75686c6e1a8f6e8c361/src/pages/login/login.scss -------------------------------------------------------------------------------- /src/pages/login/login.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NavController, LoadingController, ToastController } from 'ionic-angular'; 3 | import { FormBuilder, FormGroup, Validators, AbstractControl} from '@angular/forms'; 4 | 5 | import { TabsPage } from '../tabs/tabs'; 6 | import { SignupPage } from '../signup/signup'; 7 | import { UserCredentials } from '../../shared/interfaces'; 8 | import { DataService } from '../../shared/services/data.service'; 9 | import { AuthService } from '../../shared/services/auth.service'; 10 | 11 | @Component({ 12 | templateUrl: 'login.html' 13 | }) 14 | export class LoginPage implements OnInit { 15 | 16 | loginFirebaseAccountForm: FormGroup; 17 | email: AbstractControl; 18 | password: AbstractControl; 19 | 20 | constructor(public nav: NavController, 21 | public loadingCtrl: LoadingController, 22 | public toastCtrl: ToastController, 23 | public fb: FormBuilder, 24 | public dataService: DataService, 25 | public authService: AuthService) { } 26 | 27 | ngOnInit() { 28 | this.loginFirebaseAccountForm = this.fb.group({ 29 | 'email': ['', Validators.compose([Validators.required])], 30 | 'password': ['', Validators.compose([Validators.required, Validators.minLength(5)])] 31 | }); 32 | 33 | this.email = this.loginFirebaseAccountForm.controls['email']; 34 | this.password = this.loginFirebaseAccountForm.controls['password']; 35 | } 36 | 37 | onSubmit(signInForm: any): void { 38 | var self = this; 39 | if (this.loginFirebaseAccountForm.valid) { 40 | 41 | let loader = this.loadingCtrl.create({ 42 | content: 'Signing in firebase..', 43 | dismissOnPageChange: true 44 | }); 45 | 46 | loader.present(); 47 | 48 | let user: UserCredentials = { 49 | email: signInForm.email, 50 | password: signInForm.password 51 | }; 52 | 53 | console.log(user); 54 | this.authService.signInUser(user.email, user.password) 55 | .then(function (result) { 56 | self.nav.setRoot(TabsPage); 57 | }).catch(function (error) { 58 | // Handle Errors here. 59 | var errorCode = error.code; 60 | var errorMessage = error.message; 61 | loader.dismiss().then(() => { 62 | let toast = self.toastCtrl.create({ 63 | message: errorMessage, 64 | duration: 4000, 65 | position: 'top' 66 | }); 67 | toast.present(); 68 | }); 69 | }); 70 | } 71 | } 72 | 73 | register() { 74 | this.nav.push(SignupPage); 75 | } 76 | } -------------------------------------------------------------------------------- /src/pages/profile/profile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | Profile 7 | 8 | 11 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | 24 | 25 | Basic Info 26 | 27 | 28 | 29 | 30 | 31 | 32 |

{{userProfile.username}}

33 |

{{firebaseAccount.email}}

34 |
35 | 36 | 37 | 38 | Date of Birth 39 | 40 | {{userProfile.dateOfBirth}} 41 | 42 | 43 | 44 | 45 | 46 | 47 | {{firebaseAccount.U}} 48 | 49 | 50 | 51 |
52 | 53 | 54 | 55 | 56 | 57 | Activity 58 | 59 | 60 | 61 | # Threads 62 | 63 | {{userStatistics.totalThreads}} 64 | 65 | 66 | 67 | # Comments 68 | 69 | {{userStatistics.totalComments}} 70 | 71 | 72 | # Favorites 73 | 74 | {{userProfile.totalFavorites}} 75 | 76 | 77 | 78 |
-------------------------------------------------------------------------------- /src/pages/profile/profile.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chsakell/ionic2-angular2-firebase/c53229bc94fb19065fa0c75686c6e1a8f6e8c361/src/pages/profile/profile.scss -------------------------------------------------------------------------------- /src/pages/profile/profile.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {NavController, LoadingController, ActionSheetController } from 'ionic-angular'; 3 | import { Camera, CameraOptions } from 'ionic-native'; 4 | 5 | import { IUser } from '../../shared/interfaces'; 6 | import { AuthService } from '../../shared/services/auth.service'; 7 | import { DataService } from '../../shared/services/data.service'; 8 | 9 | @Component({ 10 | templateUrl: 'profile.html' 11 | }) 12 | export class ProfilePage implements OnInit { 13 | userDataLoaded: boolean = false; 14 | user: IUser; 15 | username: string; 16 | userProfile = {}; 17 | firebaseAccount: any = {}; 18 | userStatistics: any = {}; 19 | 20 | constructor(public navCtrl: NavController, 21 | public loadingCtrl: LoadingController, 22 | public actionSheeCtrl: ActionSheetController, 23 | public authService: AuthService, 24 | public dataService: DataService) { } 25 | 26 | ngOnInit() { 27 | this.loadUserProfile(); 28 | } 29 | 30 | loadUserProfile() { 31 | var self = this; 32 | self.userDataLoaded = false; 33 | 34 | self.getUserData().then(function (snapshot) { 35 | let userData: any = snapshot.val(); 36 | 37 | self.getUserImage().then(function (url) { 38 | self.userProfile = { 39 | username: userData.username, 40 | dateOfBirth: userData.dateOfBirth, 41 | image: url, 42 | totalFavorites: userData.hasOwnProperty('favorites') === true ? 43 | Object.keys(userData.favorites).length : 0 44 | }; 45 | 46 | self.user = { 47 | uid : self.firebaseAccount.uid, 48 | username : userData.username 49 | }; 50 | 51 | self.userDataLoaded = true; 52 | }).catch(function (error) { 53 | console.log(error.code); 54 | self.userProfile = { 55 | username: userData.username, 56 | dateOfBirth: userData.dateOfBirth, 57 | image: 'assets/images/profile.png', 58 | totalFavorites: userData.hasOwnProperty('favorites') === true ? 59 | Object.keys(userData.favorites).length : 0 60 | }; 61 | self.userDataLoaded = true; 62 | }); 63 | }); 64 | 65 | self.getUserThreads(); 66 | self.getUserComments(); 67 | } 68 | 69 | getUserData() { 70 | var self = this; 71 | 72 | self.firebaseAccount = self.authService.getLoggedInUser(); 73 | return self.dataService.getUser(self.authService.getLoggedInUser().uid); 74 | } 75 | 76 | getUserImage() { 77 | var self = this; 78 | 79 | return self.dataService.getStorageRef().child('images/' + self.firebaseAccount.uid + '/profile.png').getDownloadURL(); 80 | } 81 | 82 | getUserThreads() { 83 | var self = this; 84 | 85 | self.dataService.getUserThreads(self.authService.getLoggedInUser().uid) 86 | .then(function (snapshot) { 87 | let userThreads: any = snapshot.val(); 88 | if (userThreads !== null) { 89 | self.userStatistics.totalThreads = Object.keys(userThreads).length; 90 | } else { 91 | self.userStatistics.totalThread = 0; 92 | } 93 | }); 94 | } 95 | 96 | getUserComments() { 97 | var self = this; 98 | 99 | self.dataService.getUserComments(self.authService.getLoggedInUser().uid) 100 | .then(function (snapshot) { 101 | let userComments: any = snapshot.val(); 102 | if (userComments !== null) { 103 | self.userStatistics.totalComments = Object.keys(userComments).length; 104 | } else { 105 | self.userStatistics.totalComments = 0; 106 | } 107 | }); 108 | } 109 | 110 | openImageOptions() { 111 | var self = this; 112 | 113 | let actionSheet = self.actionSheeCtrl.create({ 114 | title: 'Upload new image from', 115 | buttons: [ 116 | { 117 | text: 'Camera', 118 | icon: 'camera', 119 | handler: () => { 120 | self.openCamera(Camera.PictureSourceType.CAMERA); 121 | } 122 | }, 123 | { 124 | text: 'Album', 125 | icon: 'folder-open', 126 | handler: () => { 127 | self.openCamera(Camera.PictureSourceType.PHOTOLIBRARY); 128 | } 129 | } 130 | ] 131 | }); 132 | 133 | actionSheet.present(); 134 | } 135 | 136 | openCamera(pictureSourceType: any) { 137 | var self = this; 138 | 139 | let options: CameraOptions = { 140 | quality: 95, 141 | destinationType: Camera.DestinationType.DATA_URL, 142 | sourceType: pictureSourceType, 143 | encodingType: Camera.EncodingType.PNG, 144 | targetWidth: 400, 145 | targetHeight: 400, 146 | saveToPhotoAlbum: true, 147 | correctOrientation: true 148 | }; 149 | 150 | Camera.getPicture(options).then(imageData => { 151 | const b64toBlob = (b64Data, contentType = '', sliceSize = 512) => { 152 | const byteCharacters = atob(b64Data); 153 | const byteArrays = []; 154 | 155 | for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { 156 | const slice = byteCharacters.slice(offset, offset + sliceSize); 157 | 158 | const byteNumbers = new Array(slice.length); 159 | for (let i = 0; i < slice.length; i++) { 160 | byteNumbers[i] = slice.charCodeAt(i); 161 | } 162 | 163 | const byteArray = new Uint8Array(byteNumbers); 164 | 165 | byteArrays.push(byteArray); 166 | } 167 | 168 | const blob = new Blob(byteArrays, { type: contentType }); 169 | return blob; 170 | }; 171 | 172 | let capturedImage: Blob = b64toBlob(imageData, 'image/png'); 173 | self.startUploading(capturedImage); 174 | }, error => { 175 | console.log('ERROR -> ' + JSON.stringify(error)); 176 | }); 177 | } 178 | 179 | reload() { 180 | this.loadUserProfile(); 181 | } 182 | 183 | startUploading(file) { 184 | 185 | let self = this; 186 | let uid = self.authService.getLoggedInUser().uid; 187 | let progress: number = 0; 188 | // display loader 189 | let loader = this.loadingCtrl.create({ 190 | content: 'Uploading image..', 191 | }); 192 | loader.present(); 193 | 194 | // Upload file and metadata to the object 'images/mountains.jpg' 195 | var metadata = { 196 | contentType: 'image/png', 197 | name: 'profile.png', 198 | cacheControl: 'no-cache', 199 | }; 200 | 201 | var uploadTask = self.dataService.getStorageRef().child('images/' + uid + '/profile.png').put(file, metadata); 202 | 203 | // Listen for state changes, errors, and completion of the upload. 204 | uploadTask.on('state_changed', 205 | function (snapshot) { 206 | // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded 207 | progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; 208 | }, function (error) { 209 | loader.dismiss().then(() => { 210 | switch (error.code) { 211 | case 'storage/unauthorized': 212 | // User doesn't have permission to access the object 213 | break; 214 | 215 | case 'storage/canceled': 216 | // User canceled the upload 217 | break; 218 | 219 | case 'storage/unknown': 220 | // Unknown error occurred, inspect error.serverResponse 221 | break; 222 | } 223 | }); 224 | }, function () { 225 | loader.dismiss().then(() => { 226 | // Upload completed successfully, now we can get the download URL 227 | var downloadURL = uploadTask.snapshot.downloadURL; 228 | self.dataService.setUserImage(uid); 229 | self.reload(); 230 | }); 231 | }); 232 | } 233 | } -------------------------------------------------------------------------------- /src/pages/signup/signup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Signup 4 | 5 | 6 | 7 |
8 | 9 | 10 | Firebase account 11 | 12 | 13 | Email address 14 | 15 | 16 |
* Email is required.
17 |
* Enter a valid email address.
18 | 19 | Password 20 | 21 | 22 |
* Password is required.
23 |
* Minimum password length is 5.
24 |
25 | 26 | 27 | Basic info 28 | 29 | 30 | Username 31 | 32 | 33 |
* Username is required.
34 |
* Minimum password length is 8.
35 | 36 | Date of Birth 37 | 38 | 39 | 40 | I accept terms of use 41 | 42 | 43 |
* You need to accept the terms of use.
44 |
45 | 46 |
47 |
-------------------------------------------------------------------------------- /src/pages/signup/signup.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chsakell/ionic2-angular2-firebase/c53229bc94fb19065fa0c75686c6e1a8f6e8c361/src/pages/signup/signup.scss -------------------------------------------------------------------------------- /src/pages/signup/signup.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NavController, ViewController, LoadingController, ToastController } from 'ionic-angular'; 3 | import { FormBuilder, FormGroup, Validators, AbstractControl} from '@angular/forms'; 4 | 5 | import { UserCredentials } from '../../shared/interfaces'; 6 | import { DataService } from '../../shared/services/data.service'; 7 | import { AuthService } from '../../shared/services/auth.service'; 8 | import { CheckedValidator } from '../../shared/validators/checked.validator'; 9 | import { EmailValidator } from '../../shared/validators/email.validator'; 10 | 11 | @Component({ 12 | templateUrl: 'signup.html' 13 | }) 14 | export class SignupPage implements OnInit { 15 | 16 | createFirebaseAccountForm: FormGroup; 17 | username: AbstractControl; 18 | email: AbstractControl; 19 | password: AbstractControl; 20 | dateOfBirth: AbstractControl; 21 | terms: AbstractControl; 22 | 23 | constructor(public nav: NavController, 24 | public loadingCtrl: LoadingController, 25 | public toastCtrl: ToastController, 26 | public viewCtrl: ViewController, 27 | public fb: FormBuilder, 28 | public dataService: DataService, 29 | public authService: AuthService) { } 30 | 31 | ngOnInit() { 32 | this.createFirebaseAccountForm = this.fb.group({ 33 | 'username': ['', Validators.compose([Validators.required, Validators.minLength(8)])], 34 | 'email': ['', Validators.compose([Validators.required, EmailValidator.isValid])], 35 | 'password': ['', Validators.compose([Validators.required, Validators.minLength(5)])], 36 | 'dateOfBirth': [new Date().toISOString().slice(0, 10), Validators.compose([Validators.required])], 37 | 'terms': [false, CheckedValidator.isChecked] 38 | }); 39 | 40 | this.username = this.createFirebaseAccountForm.controls['username']; 41 | this.email = this.createFirebaseAccountForm.controls['email']; 42 | this.password = this.createFirebaseAccountForm.controls['password']; 43 | this.dateOfBirth = this.createFirebaseAccountForm.controls['dateOfBirth']; 44 | this.terms = this.createFirebaseAccountForm.controls['terms']; 45 | } 46 | 47 | getFormattedDate(): string { 48 | let now = new Date(); 49 | let mm = now.getMonth() + 1; 50 | let dd = now.getDate(); 51 | 52 | let formattedDate = [now.getFullYear(), !mm[1] && '0', mm, !dd[1] && '0', dd].join('-'); 53 | return formattedDate; 54 | } 55 | 56 | onSubmit(signupForm: any): void { 57 | var self = this; 58 | 59 | if (this.createFirebaseAccountForm.valid) { 60 | 61 | let loader = this.loadingCtrl.create({ 62 | content: 'Creating account...', 63 | dismissOnPageChange: true 64 | }); 65 | 66 | let newUser: UserCredentials = { 67 | email: signupForm.email, 68 | password: signupForm.password 69 | }; 70 | 71 | loader.present(); 72 | 73 | this.authService.registerUser(newUser) 74 | .then(function (result) { 75 | self.authService.addUser(signupForm.username, signupForm.dateOfBirth, self.authService.getLoggedInUser().uid); 76 | loader.dismiss() 77 | .then(() => { 78 | self.viewCtrl.dismiss({ 79 | user: newUser 80 | }).then(() => { 81 | let toast = self.toastCtrl.create({ 82 | message: 'Account created successfully', 83 | duration: 4000, 84 | position: 'top' 85 | }); 86 | toast.present(); 87 | self.CreateAndUploadDefaultImage(); 88 | }); 89 | }); 90 | }).catch(function (error) { 91 | // Handle Errors here. 92 | var errorCode = error.code; 93 | var errorMessage = error.message; 94 | console.error(error); 95 | loader.dismiss().then(() => { 96 | let toast = self.toastCtrl.create({ 97 | message: errorMessage, 98 | duration: 4000, 99 | position: 'top' 100 | }); 101 | toast.present(); 102 | }); 103 | }); 104 | } 105 | } 106 | 107 | CreateAndUploadDefaultImage() { 108 | let self = this; 109 | let imageData = 'assets/images/profile.png'; 110 | 111 | var xhr = new XMLHttpRequest(); 112 | xhr.open('GET', imageData, true); 113 | xhr.responseType = 'blob'; 114 | xhr.onload = function (e) { 115 | if (this.status === 200) { 116 | var myBlob = this.response; 117 | // myBlob is now the blob that the object URL pointed to. 118 | self.startUploading(myBlob); 119 | } 120 | }; 121 | xhr.send(); 122 | } 123 | 124 | startUploading(file) { 125 | 126 | let self = this; 127 | let uid = self.authService.getLoggedInUser().uid; 128 | let progress: number = 0; 129 | // display loader 130 | let loader = this.loadingCtrl.create({ 131 | content: 'Uploading default image..', 132 | }); 133 | loader.present(); 134 | 135 | // Upload file and metadata to the object 'images/mountains.jpg' 136 | var metadata = { 137 | contentType: 'image/png', 138 | name: 'profile.png', 139 | cacheControl: 'no-cache', 140 | }; 141 | 142 | var uploadTask = self.dataService.getStorageRef().child('images/' + uid + '/profile.png').put(file, metadata); 143 | 144 | // Listen for state changes, errors, and completion of the upload. 145 | uploadTask.on('state_changed', 146 | function (snapshot) { 147 | // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded 148 | progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; 149 | }, function (error) { 150 | loader.dismiss().then(() => { 151 | switch (error.code) { 152 | case 'storage/unauthorized': 153 | // User doesn't have permission to access the object 154 | break; 155 | 156 | case 'storage/canceled': 157 | // User canceled the upload 158 | break; 159 | 160 | case 'storage/unknown': 161 | // Unknown error occurred, inspect error.serverResponse 162 | break; 163 | } 164 | }); 165 | }, function () { 166 | loader.dismiss().then(() => { 167 | // Upload completed successfully, now we can get the download URL 168 | var downloadURL = uploadTask.snapshot.downloadURL; 169 | self.dataService.setUserImage(uid); 170 | }); 171 | }); 172 | } 173 | 174 | } -------------------------------------------------------------------------------- /src/pages/tabs/tabs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/pages/tabs/tabs.scss: -------------------------------------------------------------------------------- 1 | ion-tabbar { 2 | background: #f4f4f4; 3 | } -------------------------------------------------------------------------------- /src/pages/tabs/tabs.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, ViewChild } from '@angular/core'; 2 | import { NavController, Events, Tabs } from 'ionic-angular'; 3 | 4 | import {ThreadsPage} from '../threads/threads'; 5 | import {ProfilePage} from '../profile/profile'; 6 | import {AboutPage} from '../about/about'; 7 | import { AuthService } from '../../shared/services/auth.service'; 8 | 9 | @Component({ 10 | templateUrl: 'tabs.html' 11 | }) 12 | export class TabsPage implements OnInit { 13 | @ViewChild('forumTabs') tabRef: Tabs; 14 | 15 | public threadsPage: any; 16 | public profilePage: any; 17 | public aboutPage: any; 18 | 19 | public newThreads: string = ''; 20 | public selectedTab: number = -1; 21 | 22 | constructor(public navCtrl: NavController, 23 | public authService: AuthService, 24 | public events: Events) { 25 | // this tells the tabs component which Pages 26 | // should be each tab's root Page 27 | this.threadsPage = ThreadsPage; 28 | this.profilePage = ProfilePage; 29 | this.aboutPage = AboutPage; 30 | } 31 | 32 | ngOnInit() { 33 | this.startListening(); 34 | } 35 | 36 | startListening() { 37 | var self = this; 38 | 39 | self.events.subscribe('thread:created', (threadData) => { 40 | if (self.newThreads === '') { 41 | self.newThreads = '1'; 42 | } else { 43 | self.newThreads = (+self.newThreads + 1).toString(); 44 | } 45 | }); 46 | 47 | self.events.subscribe('threads:viewed', (threadData) => { 48 | self.newThreads = ''; 49 | }); 50 | } 51 | 52 | clicked() { 53 | var self = this; 54 | 55 | if (self.newThreads !== '') { 56 | self.events.publish('threads:add'); 57 | self.newThreads = ''; 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/pages/thread-comments/thread-comments.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Comments 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

{{comment.user.username}}

25 |

{{comment.dateCreated | date:'medium'}}

26 |
27 | 28 | 29 |

{{comment.text}}

30 |
31 | 32 | 33 | 34 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | {{comment.dateCreated | date:"MM/dd/yy"}} 48 | 49 | 50 | 51 | 52 |
53 |
54 |
55 | 56 | 57 | 58 | 59 |
-------------------------------------------------------------------------------- /src/pages/thread-comments/thread-comments.scss: -------------------------------------------------------------------------------- 1 | .platform-ios .fixed-div { 2 | right: 0; 3 | bottom: 0; 4 | margin-bottom: 42px; 5 | } 6 | 7 | .platform-android .fixed-div { 8 | right: 0; 9 | bottom: 0; 10 | margin-bottom: 56px; 11 | } 12 | 13 | .platform-windows .fixed-div { 14 | right: 0; 15 | bottom: 0; 16 | } 17 | 18 | ion-card .item + ion-card-content { 19 | padding-top: 7px; 20 | } -------------------------------------------------------------------------------- /src/pages/thread-comments/thread-comments.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { ActionSheetController, ModalController, ToastController, LoadingController, NavParams, Content } from 'ionic-angular'; 3 | 4 | import { CommentCreatePage } from '../comment-create/comment-create'; 5 | import { IComment } from '../../shared/interfaces'; 6 | import { AuthService } from '../../shared/services/auth.service'; 7 | import { DataService } from '../../shared/services/data.service'; 8 | import { ItemsService } from '../../shared/services/items.service'; 9 | import { MappingsService } from '../../shared/services/mappings.service'; 10 | 11 | @Component({ 12 | templateUrl: 'thread-comments.html' 13 | }) 14 | export class ThreadCommentsPage implements OnInit { 15 | @ViewChild(Content) content: Content; 16 | threadKey: string; 17 | commentsLoaded: boolean = false; 18 | comments: IComment[]; 19 | 20 | constructor(public actionSheeCtrl: ActionSheetController, 21 | public modalCtrl: ModalController, 22 | public toastCtrl: ToastController, 23 | public loadingCtrl: LoadingController, 24 | public navParams: NavParams, 25 | public authService: AuthService, 26 | public itemsService: ItemsService, 27 | public dataService: DataService, 28 | public mappingsService: MappingsService) { } 29 | 30 | ngOnInit() { 31 | var self = this; 32 | self.threadKey = self.navParams.get('threadKey'); 33 | self.commentsLoaded = false; 34 | 35 | self.dataService.getThreadCommentsRef(self.threadKey).once('value', function (snapshot) { 36 | self.comments = self.mappingsService.getComments(snapshot); 37 | self.commentsLoaded = true; 38 | }, function (error) {}); 39 | } 40 | 41 | createComment() { 42 | let self = this; 43 | 44 | let modalPage = this.modalCtrl.create(CommentCreatePage, { 45 | threadKey: this.threadKey 46 | }); 47 | 48 | modalPage.onDidDismiss((commentData: any) => { 49 | if (commentData) { 50 | let commentVals = commentData.comment; 51 | let commentUser = commentData.user; 52 | 53 | let createdComment: IComment = { 54 | key: commentVals.key, 55 | thread: commentVals.thread, 56 | text: commentVals.text, 57 | user: commentUser, 58 | dateCreated: commentVals.dateCreated, 59 | votesUp: null, 60 | votesDown: null 61 | }; 62 | 63 | self.comments.push(createdComment); 64 | self.scrollToBottom(); 65 | 66 | let toast = this.toastCtrl.create({ 67 | message: 'Comment created', 68 | duration: 2000, 69 | position: 'top' 70 | }); 71 | toast.present(); 72 | } 73 | }); 74 | 75 | modalPage.present(); 76 | } 77 | 78 | scrollToBottom() { 79 | this.content.scrollToBottom(); 80 | } 81 | 82 | vote(like: boolean, comment: IComment) { 83 | var self = this; 84 | 85 | self.dataService.voteComment(comment.key, like, self.authService.getLoggedInUser().uid).then(function () { 86 | self.dataService.getCommentsRef().child(comment.key).once('value').then(function (snapshot) { 87 | comment = self.mappingsService.getComment(snapshot, comment.key); 88 | self.itemsService.setItem(self.comments, c => c.key === comment.key, comment); 89 | }); 90 | }); 91 | } 92 | 93 | showCommentActions() { 94 | var self = this; 95 | let actionSheet = self.actionSheeCtrl.create({ 96 | title: 'Thread Actions', 97 | buttons: [ 98 | { 99 | text: 'Add to favorites', 100 | icon: 'heart', 101 | handler: () => { 102 | self.addThreadToFavorites(); 103 | } 104 | }, 105 | { 106 | text: 'Cancel', 107 | icon: 'close-circle', 108 | role: 'cancel', 109 | handler: () => { } 110 | } 111 | ] 112 | }); 113 | 114 | actionSheet.present(); 115 | } 116 | 117 | addThreadToFavorites() { 118 | var self = this; 119 | let currentUser = self.authService.getLoggedInUser(); 120 | if (currentUser != null) { 121 | self.dataService.addThreadToFavorites(currentUser.uid, self.threadKey) 122 | .then(function () { 123 | let toast = self.toastCtrl.create({ 124 | message: 'Added to favorites', 125 | duration: 3000, 126 | position: 'top' 127 | }); 128 | toast.present(); 129 | }); 130 | } else { 131 | let toast = self.toastCtrl.create({ 132 | message: 'This action is available only for authenticated users', 133 | duration: 3000, 134 | position: 'top' 135 | }); 136 | toast.present(); 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /src/pages/thread-create/thread-create.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | New Thread 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | Title 16 | 17 | 18 |
* Title is required.
19 |
* Minimum password length is 8.
20 | 21 | Question 22 | 23 | 24 |
* Question is required.
25 |
* Type at least 100 characters.
26 | 27 | Category 28 | 29 | Components 30 | Native 31 | Theming 32 | Ionicons 33 | CLI 34 | 35 | 36 |
* Select at least one category.
37 |

38 | 39 |
40 |
-------------------------------------------------------------------------------- /src/pages/thread-create/thread-create.scss: -------------------------------------------------------------------------------- 1 | .error-box { 2 | color: color($colors, danger); 3 | padding: 10px; 4 | } -------------------------------------------------------------------------------- /src/pages/thread-create/thread-create.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NavController, ViewController, LoadingController } from 'ionic-angular'; 3 | import { FormBuilder, FormGroup, Validators, AbstractControl} from '@angular/forms'; 4 | 5 | import { IThread } from '../../shared/interfaces'; 6 | import { AuthService } from '../../shared/services/auth.service'; 7 | import { DataService } from '../../shared/services/data.service'; 8 | 9 | @Component({ 10 | templateUrl: 'thread-create.html' 11 | }) 12 | export class ThreadCreatePage implements OnInit { 13 | 14 | createThreadForm: FormGroup; 15 | title: AbstractControl; 16 | question: AbstractControl; 17 | category: AbstractControl; 18 | 19 | constructor(public nav: NavController, 20 | public loadingCtrl: LoadingController, 21 | public viewCtrl: ViewController, 22 | public fb: FormBuilder, 23 | public authService: AuthService, 24 | public dataService: DataService) { } 25 | 26 | ngOnInit() { 27 | console.log('in thread create..'); 28 | this.createThreadForm = this.fb.group({ 29 | 'title': ['', Validators.compose([Validators.required, Validators.minLength(8)])], 30 | 'question': ['', Validators.compose([Validators.required, Validators.minLength(10)])], 31 | 'category': ['', Validators.compose([Validators.required, Validators.minLength(1)])] 32 | }); 33 | 34 | this.title = this.createThreadForm.controls['title']; 35 | this.question = this.createThreadForm.controls['question']; 36 | this.category = this.createThreadForm.controls['category']; 37 | } 38 | 39 | cancelNewThread() { 40 | this.viewCtrl.dismiss(); 41 | } 42 | 43 | onSubmit(thread: any): void { 44 | var self = this; 45 | if (this.createThreadForm.valid) { 46 | 47 | let loader = this.loadingCtrl.create({ 48 | content: 'Posting thread...', 49 | dismissOnPageChange: true 50 | }); 51 | 52 | loader.present(); 53 | 54 | let uid = self.authService.getLoggedInUser().uid; 55 | self.dataService.getUsername(uid).then(function (snapshot) { 56 | let username = snapshot.val(); 57 | 58 | self.dataService.getTotalThreads().then(function (snapshot) { 59 | let currentNumber = snapshot.val(); 60 | let newPriority: number = currentNumber === null ? 1 : (currentNumber + 1); 61 | 62 | let newThread: IThread = { 63 | key: null, 64 | title: thread.title, 65 | question: thread.question, 66 | category: thread.category, 67 | user: { uid: uid, username: username }, 68 | dateCreated: new Date().toString(), 69 | comments: null 70 | }; 71 | 72 | self.dataService.submitThread(newThread, newPriority) 73 | .then(function (snapshot) { 74 | loader.dismiss() 75 | .then(() => { 76 | self.viewCtrl.dismiss({ 77 | thread: newThread, 78 | priority: newPriority 79 | }); 80 | }); 81 | }, function (error) { 82 | // The Promise was rejected. 83 | console.error(error); 84 | loader.dismiss(); 85 | }); 86 | }); 87 | }); 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/pages/threads/threads.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | All 9 | 10 | 11 | Favorites 12 | 13 | 14 | 15 | 16 | 19 | 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 |
-------------------------------------------------------------------------------- /src/pages/threads/threads.scss: -------------------------------------------------------------------------------- 1 | .thread-card-title { 2 | font-size: 14x; 3 | width: 100%; 4 | font-weight: bold; 5 | color: black; 6 | padding: 0px 6px; 7 | margin-top: 6px; 8 | } 9 | 10 | .thread-card-question { 11 | font-size: 1.0em; 12 | width: 100%; 13 | padding: 0 10px 0 12px; 14 | margin-top: 7px; 15 | color: #424242; 16 | } 17 | 18 | .wordwrap { 19 | white-space: normal; /* CSS3 */ 20 | white-space: -moz-pre-wrap; /* Firefox */ 21 | white-space: -pre-wrap; /* Opera <7 */ 22 | white-space: -o-pre-wrap; /* Opera 7 */ 23 | word-wrap: break-word; /* IE */ 24 | } 25 | 26 | .segment-button.segment-activated { 27 | color: black; 28 | background-color: #f4f4f4;// #ffdd00; 29 | } 30 | 31 | .toolbar ion-searchbar .searchbar-input { 32 | background-color: white; 33 | } 34 | 35 | .segment-button { 36 | color: black; 37 | } -------------------------------------------------------------------------------- /src/pages/threads/threads.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { NavController, ModalController, ToastController, Content, Events } from 'ionic-angular'; 3 | 4 | import { IThread } from '../../shared/interfaces'; 5 | import { ThreadCreatePage } from '../thread-create/thread-create'; 6 | import { ThreadCommentsPage } from '../thread-comments/thread-comments'; 7 | import { AuthService } from '../../shared/services/auth.service'; 8 | import { DataService } from '../../shared/services/data.service'; 9 | import { MappingsService } from '../../shared/services/mappings.service'; 10 | import { ItemsService } from '../../shared/services/items.service'; 11 | import { SqliteService } from '../../shared/services/sqlite.service'; 12 | 13 | @Component({ 14 | templateUrl: 'threads.html' 15 | }) 16 | export class ThreadsPage implements OnInit { 17 | @ViewChild(Content) content: Content; 18 | segment: string = 'all'; 19 | selectedSegment: string = this.segment; 20 | queryText: string = ''; 21 | public start: number; 22 | public pageSize: number = 3; 23 | public loading: boolean = true; 24 | public internetConnected: boolean = true; 25 | 26 | public threads: Array = []; 27 | public newThreads: Array = []; 28 | public favoriteThreadKeys: string[]; 29 | 30 | public firebaseConnectionAttempts: number = 0; 31 | 32 | constructor(public navCtrl: NavController, 33 | public modalCtrl: ModalController, 34 | public toastCtrl: ToastController, 35 | public authService: AuthService, 36 | public dataService: DataService, 37 | public sqliteService: SqliteService, 38 | public mappingsService: MappingsService, 39 | public itemsService: ItemsService, 40 | public events: Events) { } 41 | 42 | ngOnInit() { 43 | var self = this; 44 | self.segment = 'all'; 45 | self.events.subscribe('network:connected', self.networkConnected); 46 | self.events.subscribe('threads:add', self.addNewThreads); 47 | 48 | self.checkFirebase(); 49 | } 50 | 51 | checkFirebase() { 52 | let self = this; 53 | if (!self.dataService.isFirebaseConnected()) { 54 | setTimeout(function () { 55 | console.log('Retry : ' + self.firebaseConnectionAttempts); 56 | self.firebaseConnectionAttempts++; 57 | if (self.firebaseConnectionAttempts < 5) { 58 | self.checkFirebase(); 59 | } else { 60 | self.internetConnected = false; 61 | self.dataService.goOffline(); 62 | self.loadSqliteThreads(); 63 | } 64 | }, 1000); 65 | } else { 66 | console.log('Firebase connection found (threads.ts) - attempt: ' + self.firebaseConnectionAttempts); 67 | self.dataService.getStatisticsRef().on('child_changed', self.onThreadAdded); 68 | if (self.authService.getLoggedInUser() === null) { 69 | // 70 | } else { 71 | self.loadThreads(true); 72 | } 73 | } 74 | } 75 | 76 | loadSqliteThreads() { 77 | let self = this; 78 | 79 | if (self.threads.length > 0) 80 | return; 81 | 82 | self.threads = []; 83 | console.log('Loading from db..'); 84 | self.sqliteService.getThreads().then((data) => { 85 | console.log('Found in db: ' + data.rows.length + ' threads'); 86 | if (data.rows.length > 0) { 87 | for (var i = 0; i < data.rows.length; i++) { 88 | let thread: IThread = { 89 | key: data.rows.item(i).key, 90 | title: data.rows.item(i).title, 91 | question: data.rows.item(i).question, 92 | category: data.rows.item(i).category, 93 | dateCreated: data.rows.item(i).datecreated, 94 | user: { uid: data.rows.item(i).user, username: data.rows.item(i).username }, 95 | comments: data.rows.item(i).comments 96 | }; 97 | 98 | self.threads.push(thread); 99 | console.log('Thread added from db:' + thread.key); 100 | console.log(thread); 101 | } 102 | self.loading = false; 103 | } 104 | }, (error) => { 105 | console.log('Error: ' + JSON.stringify(error)); 106 | self.loading = true; 107 | }); 108 | } 109 | 110 | public networkConnected = (connection) => { 111 | var self = this; 112 | self.internetConnected = connection[0]; 113 | console.log('NetworkConnected event: ' + self.internetConnected); 114 | 115 | if (self.internetConnected) { 116 | self.threads = []; 117 | self.loadThreads(true); 118 | } else { 119 | self.notify('Connection lost. Working offline..'); 120 | // save current threads.. 121 | setTimeout(function () { 122 | console.log(self.threads.length); 123 | self.sqliteService.saveThreads(self.threads); 124 | self.loadSqliteThreads(); 125 | }, 1000); 126 | } 127 | } 128 | 129 | // Notice function declarion to keep the right this reference 130 | public onThreadAdded = (childSnapshot, prevChildKey) => { 131 | let priority = childSnapshot.val(); // priority.. 132 | var self = this; 133 | self.events.publish('thread:created'); 134 | // fetch new thread.. 135 | self.dataService.getThreadsRef().orderByPriority().equalTo(priority).once('value').then(function (dataSnapshot) { 136 | let key = Object.keys(dataSnapshot.val())[0]; 137 | let newThread: IThread = self.mappingsService.getThread(dataSnapshot.val()[key], key); 138 | self.newThreads.push(newThread); 139 | }); 140 | } 141 | 142 | public addNewThreads = () => { 143 | var self = this; 144 | self.newThreads.forEach(function (thread: IThread) { 145 | self.threads.unshift(thread); 146 | }); 147 | 148 | self.newThreads = []; 149 | self.scrollToTop(); 150 | self.events.publish('threads:viewed'); 151 | } 152 | 153 | loadThreads(fromStart: boolean) { 154 | var self = this; 155 | 156 | if (fromStart) { 157 | self.loading = true; 158 | self.threads = []; 159 | self.newThreads = []; 160 | 161 | if (self.segment === 'all') { 162 | this.dataService.getTotalThreads().then(function (snapshot) { 163 | self.start = snapshot.val(); 164 | self.getThreads(); 165 | }); 166 | } else { 167 | self.start = 0; 168 | self.favoriteThreadKeys = []; 169 | self.dataService.getFavoriteThreads(self.authService.getLoggedInUser().uid).then(function (dataSnapshot) { 170 | let favoriteThreads = dataSnapshot.val(); 171 | self.itemsService.getKeys(favoriteThreads).forEach(function (threadKey) { 172 | self.start++; 173 | self.favoriteThreadKeys.push(threadKey); 174 | }); 175 | self.getThreads(); 176 | }); 177 | } 178 | } else { 179 | self.getThreads(); 180 | } 181 | } 182 | 183 | getThreads() { 184 | var self = this; 185 | let startFrom: number = self.start - self.pageSize; 186 | if (startFrom < 0) 187 | startFrom = 0; 188 | if (self.segment === 'all') { 189 | this.dataService.getThreadsRef().orderByPriority().startAt(startFrom).endAt(self.start).once('value', function (snapshot) { 190 | self.itemsService.reversedItems(self.mappingsService.getThreads(snapshot)).forEach(function (thread) { 191 | self.threads.push(thread); 192 | }); 193 | self.start -= (self.pageSize + 1); 194 | self.events.publish('threads:viewed'); 195 | self.loading = false; 196 | }); 197 | } else { 198 | self.favoriteThreadKeys.forEach(key => { 199 | this.dataService.getThreadsRef().child(key).once('value') 200 | .then(function (dataSnapshot) { 201 | self.threads.unshift(self.mappingsService.getThread(dataSnapshot.val(), key)); 202 | }); 203 | }); 204 | self.events.publish('threads:viewed'); 205 | self.loading = false; 206 | } 207 | 208 | } 209 | 210 | filterThreads(segment) { 211 | if (this.selectedSegment !== this.segment) { 212 | this.selectedSegment = this.segment; 213 | if (this.selectedSegment === 'favorites') 214 | this.queryText = ''; 215 | if (this.internetConnected) 216 | // Initialize 217 | this.loadThreads(true); 218 | } else { 219 | this.scrollToTop(); 220 | } 221 | } 222 | 223 | searchThreads() { 224 | var self = this; 225 | if (self.queryText.trim().length !== 0) { 226 | self.segment = 'all'; 227 | // empty current threads 228 | self.threads = []; 229 | self.dataService.loadThreads().then(function (snapshot) { 230 | self.itemsService.reversedItems(self.mappingsService.getThreads(snapshot)).forEach(function (thread) { 231 | if (thread.title.toLowerCase().includes(self.queryText.toLowerCase())) 232 | self.threads.push(thread); 233 | }); 234 | }); 235 | } else { // text cleared.. 236 | this.loadThreads(true); 237 | } 238 | } 239 | 240 | createThread() { 241 | var self = this; 242 | let modalPage = this.modalCtrl.create(ThreadCreatePage); 243 | 244 | modalPage.onDidDismiss((data: any) => { 245 | if (data) { 246 | let toast = this.toastCtrl.create({ 247 | message: 'Thread created', 248 | duration: 3000, 249 | position: 'bottom' 250 | }); 251 | toast.present(); 252 | 253 | if (data.priority === 1) 254 | self.newThreads.push(data.thread); 255 | 256 | self.addNewThreads(); 257 | } 258 | }); 259 | 260 | modalPage.present(); 261 | } 262 | 263 | viewComments(key: string) { 264 | if (this.internetConnected) { 265 | this.navCtrl.push(ThreadCommentsPage, { 266 | threadKey: key 267 | }); 268 | } else { 269 | this.notify('Network not found..'); 270 | } 271 | } 272 | 273 | reloadThreads(refresher) { 274 | this.queryText = ''; 275 | if (this.internetConnected) { 276 | this.loadThreads(true); 277 | refresher.complete(); 278 | } else { 279 | refresher.complete(); 280 | } 281 | } 282 | 283 | fetchNextThreads(infiniteScroll) { 284 | if (this.start > 0 && this.internetConnected) { 285 | this.loadThreads(false); 286 | infiniteScroll.complete(); 287 | } else { 288 | infiniteScroll.complete(); 289 | } 290 | } 291 | 292 | scrollToTop() { 293 | var self = this; 294 | setTimeout(function () { 295 | self.content.scrollToTop(); 296 | }, 1500); 297 | } 298 | 299 | notify(message: string) { 300 | let toast = this.toastCtrl.create({ 301 | message: message, 302 | duration: 3000, 303 | position: 'top' 304 | }); 305 | toast.present(); 306 | } 307 | } -------------------------------------------------------------------------------- /src/providers/app.providers.ts: -------------------------------------------------------------------------------- 1 | import { AuthService } from '../shared/services/auth.service'; 2 | import { DataService } from '../shared/services/data.service'; 3 | import { SqliteService } from '../shared/services/sqlite.service'; 4 | import { MappingsService } from '../shared/services/mappings.service'; 5 | import { ItemsService } from '../shared/services/items.service'; 6 | 7 | export const APP_PROVIDERS = [ 8 | AuthService, 9 | DataService, 10 | ItemsService, 11 | SqliteService, 12 | MappingsService 13 | ]; -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | // tick this to make the cache invalidate and update 2 | const CACHE_VERSION = 1; 3 | const CURRENT_CACHES = { 4 | 'read-through': 'read-through-cache-v' + CACHE_VERSION 5 | }; 6 | 7 | self.addEventListener('activate', (event) => { 8 | // Delete all caches that aren't named in CURRENT_CACHES. 9 | // While there is only one cache in this example, the same logic will handle the case where 10 | // there are multiple versioned caches. 11 | const expectedCacheNames = Object.keys(CURRENT_CACHES).map((key) => { 12 | return CURRENT_CACHES[key]; 13 | }); 14 | 15 | event.waitUntil( 16 | caches.keys().then((cacheNames) => { 17 | return Promise.all( 18 | cacheNames.map((cacheName) => { 19 | if (expectedCacheNames.indexOf(cacheName) === -1) { 20 | // If this cache name isn't present in the array of "expected" cache names, then delete it. 21 | console.log('Deleting out of date cache:', cacheName); 22 | return caches.delete(cacheName); 23 | } 24 | }) 25 | ); 26 | }) 27 | ); 28 | }); 29 | 30 | // This sample illustrates an aggressive approach to caching, in which every valid response is 31 | // cached and every request is first checked against the cache. 32 | // This may not be an appropriate approach if your web application makes requests for 33 | // arbitrary URLs as part of its normal operation (e.g. a RSS client or a news aggregator), 34 | // as the cache could end up containing large responses that might not end up ever being accessed. 35 | // Other approaches, like selectively caching based on response headers or only caching 36 | // responses served from a specific domain, might be more appropriate for those use cases. 37 | self.addEventListener('fetch', (event) => { 38 | 39 | event.respondWith( 40 | caches.open(CURRENT_CACHES['read-through']).then((cache) => { 41 | return cache.match(event.request).then((response) => { 42 | if (response) { 43 | // If there is an entry in the cache for event.request, then response will be defined 44 | // and we can just return it. 45 | 46 | return response; 47 | } 48 | 49 | // Otherwise, if there is no entry in the cache for event.request, response will be 50 | // undefined, and we need to fetch() the resource. 51 | console.log(' No response for %s found in cache. ' + 52 | 'About to fetch from network...', event.request.url); 53 | 54 | // We call .clone() on the request since we might use it in the call to cache.put() later on. 55 | // Both fetch() and cache.put() "consume" the request, so we need to make a copy. 56 | // (see https://fetch.spec.whatwg.org/#dom-request-clone) 57 | return fetch(event.request.clone()).then((response) => { 58 | 59 | // Optional: add in extra conditions here, e.g. response.type == 'basic' to only cache 60 | // responses from the same domain. See https://fetch.spec.whatwg.org/#concept-response-type 61 | if (response.status < 400 && response.type === 'basic') { 62 | // We need to call .clone() on the response object to save a copy of it to the cache. 63 | // (https://fetch.spec.whatwg.org/#dom-request-clone) 64 | cache.put(event.request, response.clone()); 65 | } 66 | 67 | // Return the original response object, which will be used to fulfill the resource request. 68 | return response; 69 | }); 70 | }).catch((error) => { 71 | // This catch() will handle exceptions that arise from the match() or fetch() operations. 72 | // Note that a HTTP error response (e.g. 404) will NOT trigger an exception. 73 | // It will return a normal response object that has the appropriate error code set. 74 | console.error(' Read-through caching failed:', error); 75 | 76 | throw error; 77 | }); 78 | }) 79 | ); 80 | }); -------------------------------------------------------------------------------- /src/shared/components/thread.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

{{thread.user.username}}

9 |

{{thread.dateCreated | date:'medium'}}

10 |
11 | 12 | 13 |
14 | {{thread.title}} 15 |
16 |
17 | {{thread.question}} 18 |
19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | {{thread.category}} 30 | 31 | 32 | 33 |
34 |
-------------------------------------------------------------------------------- /src/shared/components/thread.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, OnInit, OnDestroy, Input, Output } from '@angular/core'; 2 | 3 | import { IThread } from '../interfaces'; 4 | import { DataService } from '../services/data.service'; 5 | 6 | @Component({ 7 | selector: 'forum-thread', 8 | templateUrl: 'thread.component.html' 9 | }) 10 | export class ThreadComponent implements OnInit, OnDestroy { 11 | @Input() thread: IThread; 12 | @Output() onViewComments = new EventEmitter(); 13 | 14 | constructor(private dataService: DataService) { } 15 | 16 | ngOnInit() { 17 | var self = this; 18 | self.dataService.getThreadsRef().child(self.thread.key).on('child_changed', self.onCommentAdded); 19 | } 20 | 21 | ngOnDestroy() { 22 | console.log('destroying..'); 23 | var self = this; 24 | self.dataService.getThreadsRef().child(self.thread.key).off('child_changed', self.onCommentAdded); 25 | } 26 | 27 | // Notice function declarion to keep the right this reference 28 | public onCommentAdded = (childSnapshot, prevChildKey) => { 29 | console.log(childSnapshot.val()); 30 | var self = this; 31 | // Attention: only number of comments is supposed to changed. 32 | // Otherwise you should run some checks.. 33 | self.thread.comments = childSnapshot.val(); 34 | } 35 | 36 | viewComments(key: string) { 37 | this.onViewComments.emit(key); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/shared/components/user-avatar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { PhotoViewer } from 'ionic-native'; 3 | 4 | import { IUser } from '../interfaces'; 5 | import { DataService } from '../services/data.service'; 6 | 7 | @Component({ 8 | selector: 'forum-user-avatar', 9 | template: ` ` 10 | }) 11 | export class UserAvatarComponent implements OnInit { 12 | @Input() user: IUser; 13 | imageLoaded: boolean = false; 14 | imageUrl: string; 15 | 16 | constructor(private dataService: DataService) { } 17 | 18 | ngOnInit() { 19 | let self = this; 20 | let firebaseConnected: boolean = self.dataService.isFirebaseConnected(); 21 | if (self.user.uid === 'default' || !firebaseConnected) { 22 | self.imageUrl = 'assets/images/profile.png'; 23 | self.imageLoaded = true; 24 | } else { 25 | self.dataService.getStorageRef().child('images/' + self.user.uid + '/profile.png').getDownloadURL().then(function (url) { 26 | self.imageUrl = url.split('?')[0] + '?alt=media' + '&t=' + (new Date().getTime()); 27 | self.imageLoaded = true; 28 | }); 29 | } 30 | /* 31 | let defaultUrl = self.dataService.getDefaultImageUrl(); 32 | if (defaultUrl == null) { 33 | self.imageUrl = 'images/profile.png'; 34 | self.imageLoaded = true; 35 | console.log('get from firebase'); 36 | /* 37 | self.dataService.getStorageRef().child('images/' + self.user.uid + '/profile.png').getDownloadURL().then(function (url) { 38 | self.imageUrl = url.split('?')[0] + '?alt=media' + '&t=' + (new Date().getTime()); 39 | self.imageLoaded = true; 40 | }); 41 | 42 | } else { 43 | this.imageUrl = defaultUrl.replace('default', self.user.uid) + '&t=' + (new Date().getTime()); 44 | self.imageLoaded = true; 45 | }*/ 46 | } 47 | 48 | zoom() { 49 | PhotoViewer.show(this.imageUrl, this.user.username, { share: false }); 50 | } 51 | 52 | getUserImage() { 53 | var self = this; 54 | 55 | return self.dataService.getStorageRef().child('images/' + self.user.uid + '/profile.png').getDownloadURL(); 56 | } 57 | } -------------------------------------------------------------------------------- /src/shared/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IThread { 2 | key: string; 3 | title: string; 4 | question: string; 5 | category: string; 6 | dateCreated: string; 7 | user: IUser; 8 | comments: number; 9 | } 10 | 11 | export interface IComment { 12 | key?: string; 13 | thread: string; 14 | text: string; 15 | user: IUser; 16 | dateCreated: string; 17 | votesUp: number; 18 | votesDown: number; 19 | } 20 | 21 | export interface UserCredentials { 22 | email: string; 23 | password: string; 24 | } 25 | 26 | export interface IUser { 27 | uid: string; 28 | username: string; 29 | } 30 | 31 | export interface Predicate { 32 | (item: T): boolean; 33 | } 34 | 35 | export interface ValidationResult { 36 | [key: string]: boolean; 37 | } -------------------------------------------------------------------------------- /src/shared/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs/Observable'; 3 | 4 | import { UserCredentials } from '../interfaces'; 5 | 6 | declare var firebase: any; 7 | 8 | @Injectable() 9 | export class AuthService { 10 | 11 | usersRef: any = firebase.database().ref('users'); 12 | 13 | constructor() { } 14 | 15 | registerUser(user: UserCredentials) { 16 | return firebase.auth().createUserWithEmailAndPassword(user.email, user.password); 17 | } 18 | 19 | signInUser(email: string, password: string) { 20 | return firebase.auth().signInWithEmailAndPassword(email, password); 21 | } 22 | 23 | signOut() { 24 | return firebase.auth().signOut(); 25 | } 26 | 27 | addUser(username: string, dateOfBirth: string, uid: string) { 28 | this.usersRef.child(uid).update({ 29 | username: username, 30 | dateOfBirth: dateOfBirth 31 | }); 32 | } 33 | 34 | getLoggedInUser() { 35 | return firebase.auth().currentUser; 36 | } 37 | 38 | onAuthStateChanged(callback) { 39 | return firebase.auth().onAuthStateChanged(callback); 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/shared/services/data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs/Observable'; 3 | 4 | import { IThread, IComment } from '../interfaces'; 5 | 6 | declare var firebase: any; 7 | 8 | @Injectable() 9 | export class DataService { 10 | databaseRef: any = firebase.database(); 11 | usersRef: any = firebase.database().ref('users'); 12 | threadsRef: any = firebase.database().ref('threads'); 13 | commentsRef: any = firebase.database().ref('comments'); 14 | statisticsRef: any = firebase.database().ref('statistics'); 15 | storageRef: any = firebase.storage().ref(); 16 | connectionRef: any = firebase.database().ref('.info/connected'); 17 | 18 | defaultImageUrl: string; 19 | connected: boolean = false; 20 | 21 | constructor() { 22 | var self = this; 23 | try { 24 | self.checkFirebaseConnection(); 25 | /* 26 | self.storageRef.child('images/default/profile.png').getDownloadURL().then(function (url) { 27 | self.defaultImageUrl = url.split('?')[0] + '?alt=media'; 28 | }); 29 | */ 30 | self.InitData(); 31 | } catch (error) { 32 | console.log('Data Service error:' + error); 33 | } 34 | } 35 | 36 | checkFirebaseConnection() { 37 | try { 38 | var self = this; 39 | var connectedRef = self.getConnectionRef(); 40 | connectedRef.on('value', function (snap) { 41 | console.log(snap.val()); 42 | if (snap.val() === true) { 43 | console.log('Firebase: Connected:'); 44 | self.connected = true; 45 | } else { 46 | console.log('Firebase: No connection:'); 47 | self.connected = false; 48 | } 49 | }); 50 | } catch (error) { 51 | self.connected = false; 52 | } 53 | } 54 | 55 | isFirebaseConnected() { 56 | return this.connected; 57 | } 58 | 59 | private InitData() { 60 | let self = this; 61 | // Set statistics/threads = 1 for the first time only 62 | self.getStatisticsRef().child('threads').transaction(function (currentRank) { 63 | if (currentRank === null) { 64 | return 1; 65 | } 66 | }, function (error, committed, snapshot) { 67 | if (error) { 68 | console.log('Transaction failed abnormally!', error); 69 | } else if (!committed) { 70 | console.log('We aborted the transaction because there is already one thread.'); 71 | } else { 72 | console.log('Threads number initialized!'); 73 | 74 | let thread: IThread = { 75 | key: null, 76 | title: 'Welcome to Forum!', 77 | question: 'Congratulations! It seems that you have successfully setup the Forum app.', 78 | category: 'welcome', 79 | dateCreated: new Date().toString(), 80 | user: { uid: 'default', username: 'Administrator' }, 81 | comments: 0 82 | }; 83 | 84 | let firstThreadRef = self.threadsRef.push(); 85 | firstThreadRef.setWithPriority(thread, 1).then(function (dataShapshot) { 86 | console.log('Congratulations! You have created the first thread!'); 87 | }); 88 | } 89 | console.log('commited', snapshot.val()); 90 | }, false); 91 | } 92 | 93 | getDatabaseRef() { 94 | return this.databaseRef; 95 | } 96 | 97 | getConnectionRef() { 98 | return this.connectionRef; 99 | } 100 | 101 | goOffline() { 102 | firebase.database().goOffline(); 103 | } 104 | 105 | goOnline() { 106 | firebase.database().goOnline(); 107 | } 108 | 109 | getDefaultImageUrl() { 110 | return this.defaultImageUrl; 111 | } 112 | 113 | getTotalThreads() { 114 | return this.statisticsRef.child('threads').once('value'); 115 | } 116 | 117 | getThreadsRef() { 118 | return this.threadsRef; 119 | } 120 | 121 | getCommentsRef() { 122 | return this.commentsRef; 123 | } 124 | 125 | getStatisticsRef() { 126 | return this.statisticsRef; 127 | } 128 | 129 | getUsersRef() { 130 | return this.usersRef; 131 | } 132 | 133 | getStorageRef() { 134 | return this.storageRef; 135 | } 136 | 137 | getThreadCommentsRef(threadKey: string) { 138 | return this.commentsRef.orderByChild('thread').equalTo(threadKey); 139 | } 140 | 141 | loadThreads() { 142 | return this.threadsRef.once('value'); 143 | } 144 | 145 | submitThread(thread: IThread, priority: number) { 146 | 147 | var newThreadRef = this.threadsRef.push(); 148 | this.statisticsRef.child('threads').set(priority); 149 | console.log(priority); 150 | return newThreadRef.setWithPriority(thread, priority); 151 | } 152 | 153 | addThreadToFavorites(userKey: string, threadKey: string) { 154 | return this.usersRef.child(userKey + '/favorites/' + threadKey).set(true); 155 | } 156 | 157 | getFavoriteThreads(user: string) { 158 | return this.usersRef.child(user + '/favorites/').once('value'); 159 | } 160 | 161 | setUserImage(uid: string) { 162 | this.usersRef.child(uid).update({ 163 | image: true 164 | }); 165 | } 166 | 167 | loadComments(threadKey: string) { 168 | return this.commentsRef.orderByChild('thread').equalTo(threadKey).once('value'); 169 | } 170 | 171 | submitComment(threadKey: string, comment: IComment) { 172 | // let commentRef = this.commentsRef.push(); 173 | // let commentkey: string = commentRef.key; 174 | this.commentsRef.child(comment.key).set(comment); 175 | 176 | return this.threadsRef.child(threadKey + '/comments').once('value') 177 | .then((snapshot) => { 178 | let numberOfComments = snapshot == null ? 0 : snapshot.val(); 179 | this.threadsRef.child(threadKey + '/comments').set(numberOfComments + 1); 180 | }); 181 | } 182 | 183 | voteComment(commentKey: string, like: boolean, user: string): any { 184 | let commentRef = this.commentsRef.child(commentKey + '/votes/' + user); 185 | return commentRef.set(like); 186 | } 187 | 188 | getUsername(userUid: string) { 189 | return this.usersRef.child(userUid + '/username').once('value'); 190 | } 191 | 192 | getUser(userUid: string) { 193 | return this.usersRef.child(userUid).once('value'); 194 | } 195 | 196 | getUserThreads(userUid: string) { 197 | return this.threadsRef.orderByChild('user/uid').equalTo(userUid).once('value'); 198 | } 199 | 200 | getUserComments(userUid: string) { 201 | return this.commentsRef.orderByChild('user/uid').equalTo(userUid).once('value'); 202 | } 203 | } -------------------------------------------------------------------------------- /src/shared/services/items.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Predicate } from '../interfaces'; 3 | 4 | import lodash from 'lodash'; 5 | 6 | @Injectable() 7 | export class ItemsService { 8 | 9 | constructor() { } 10 | 11 | getKeys(object): string[] { 12 | return lodash.keysIn(object); 13 | } 14 | 15 | reversedItems(array: T[]): T[] { 16 | return lodash.reverse(array); 17 | } 18 | 19 | groupByBoolean(object, value: boolean): number { 20 | let result: number = 0; 21 | if (object == null) 22 | return result; 23 | 24 | lodash.map(lodash.shuffle(object), function (val) { 25 | if (val === value) 26 | result++; 27 | }); 28 | 29 | return result; 30 | } 31 | 32 | /* 33 | Returns object's keys lenght 34 | */ 35 | getObjectKeysSize(obj: any): number { 36 | if (obj == null) { 37 | return 0; 38 | } else { 39 | return lodash.size(obj); 40 | } 41 | } 42 | /* 43 | Removes an item from an array using the lodash library 44 | */ 45 | removeItemFromArray(array: Array, item: any) { 46 | lodash.remove(array, function (current) { 47 | return JSON.stringify(current) === JSON.stringify(item); 48 | }); 49 | } 50 | 51 | removeItems(array: Array, predicate: Predicate) { 52 | lodash.remove(array, predicate); 53 | } 54 | 55 | includesItem(array: Array, predicate: Predicate) { 56 | let result = lodash.filter(array, predicate); 57 | return result.length > 0; 58 | } 59 | 60 | /* 61 | Finds a specific item in an array using a predicate and replaces it 62 | */ 63 | setItem(array: Array, predicate: Predicate, item: T) { 64 | var _oldItem = lodash.find(array, predicate); 65 | if (_oldItem) { 66 | var index = lodash.indexOf(array, _oldItem); 67 | array.splice(index, 1, item); 68 | } else { 69 | array.push(item); 70 | } 71 | } 72 | 73 | /* 74 | Adds an item to zero index 75 | */ 76 | addItemToStart(array: Array, item: any) { 77 | array.splice(0, 0, item); 78 | } 79 | 80 | /* 81 | From an array of type T, select all values of type R for property 82 | */ 83 | getPropertyValues(array: Array, property: string): R { 84 | var result = lodash.map(array, property); 85 | return result; 86 | } 87 | 88 | /* 89 | Util method to serialize a string to a specific Type 90 | */ 91 | getSerialized(arg: any): T { 92 | return JSON.parse(JSON.stringify(arg)); 93 | } 94 | } -------------------------------------------------------------------------------- /src/shared/services/mappings.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { IThread, IComment } from '../interfaces'; 4 | import { ItemsService } from '../services/items.service'; 5 | 6 | @Injectable() 7 | export class MappingsService { 8 | 9 | constructor(private itemsService: ItemsService) { } 10 | 11 | getThreads(snapshot: any): Array { 12 | let threads: Array = []; 13 | if (snapshot.val() == null) 14 | return threads; 15 | 16 | let list = snapshot.val(); 17 | 18 | Object.keys(snapshot.val()).map((key: any) => { 19 | let thread: any = list[key]; 20 | threads.push({ 21 | key: key, 22 | title: thread.title, 23 | question: thread.question, 24 | category: thread.category, 25 | dateCreated: thread.dateCreated, 26 | user: { uid: thread.user.uid, username: thread.user.username }, 27 | comments: thread.comments == null ? 0 : thread.comments 28 | }); 29 | }); 30 | 31 | return threads; 32 | } 33 | 34 | getThread(snapshot: any, key: string): IThread { 35 | 36 | let thread: IThread = { 37 | key: key, 38 | title: snapshot.title, 39 | question: snapshot.question, 40 | category: snapshot.category, 41 | dateCreated: snapshot.dateCreated, 42 | user: snapshot.user, 43 | comments: snapshot.comments == null ? 0 : snapshot.comments 44 | }; 45 | 46 | return thread; 47 | } 48 | 49 | getComments(snapshot: any): Array { 50 | let comments: Array = []; 51 | if (snapshot.val() == null) 52 | return comments; 53 | 54 | let list = snapshot.val(); 55 | 56 | Object.keys(snapshot.val()).map((key: any) => { 57 | let comment: any = list[key]; 58 | //console.log(comment.votes); 59 | this.itemsService.groupByBoolean(comment.votes, true); 60 | 61 | comments.push({ 62 | key: key, 63 | text: comment.text, 64 | thread: comment.thread, 65 | dateCreated: comment.dateCreated, 66 | user: comment.user, 67 | votesUp: this.itemsService.groupByBoolean(comment.votes, true), 68 | votesDown: this.itemsService.groupByBoolean(comment.votes, false) 69 | }); 70 | }); 71 | 72 | return comments; 73 | } 74 | 75 | getComment(snapshot: any, commentKey: string): IComment { 76 | let comment: IComment; 77 | 78 | if (snapshot.val() == null) 79 | return null; 80 | 81 | let snapshotComment = snapshot.val(); 82 | console.log(snapshotComment); 83 | comment = { 84 | key: commentKey, 85 | text: snapshotComment.text, 86 | thread: snapshotComment.thread, 87 | dateCreated: snapshotComment.dateCreated, 88 | user: snapshotComment.user, 89 | votesUp: this.itemsService.groupByBoolean(snapshotComment.votes, true), 90 | votesDown: this.itemsService.groupByBoolean(snapshotComment.votes, false) 91 | }; 92 | 93 | return comment; 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /src/shared/services/sqlite.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { SQLite } from 'ionic-native'; 3 | 4 | import { IThread, IComment, IUser } from '../interfaces'; 5 | import { ItemsService } from '../services/items.service'; 6 | 7 | @Injectable() 8 | export class SqliteService { 9 | db: SQLite; 10 | 11 | constructor(private itemsService: ItemsService) { 12 | 13 | } 14 | 15 | InitDatabase() { 16 | var self = this; 17 | this.db = new SQLite(); 18 | self.db.openDatabase({ 19 | name: 'forumdb.db', 20 | location: 'default' // the location field is required 21 | }).then(() => { 22 | self.createThreads(); 23 | self.createComments(); 24 | self.createUsers(); 25 | }, (err) => { 26 | console.error('Unable to open database: ', err); 27 | }); 28 | } 29 | 30 | resetDatabase() { 31 | var self = this; 32 | self.resetUsers(); 33 | self.resetThreads(); 34 | self.resetComments(); 35 | } 36 | 37 | resetUsers() { 38 | var self = this; 39 | let query = 'DELETE FROM Users'; 40 | self.db.executeSql(query, {}).then((data) => { 41 | console.log('Users removed'); 42 | }, (err) => { 43 | console.error('Unable to remove users: ', err); 44 | }); 45 | } 46 | 47 | resetThreads() { 48 | var self = this; 49 | let query = 'DELETE FROM Threads'; 50 | self.db.executeSql(query, {}).then((data) => { 51 | console.log('Threads removed'); 52 | }, (err) => { 53 | console.error('Unable to remove Threads: ', err); 54 | }); 55 | } 56 | 57 | resetComments() { 58 | var self = this; 59 | let query = 'DELETE FROM Comments'; 60 | self.db.executeSql(query, {}).then((data) => { 61 | console.log('Comments removed'); 62 | }, (err) => { 63 | console.error('Unable to remove Commments: ', err); 64 | }); 65 | } 66 | 67 | printThreads() { 68 | var self = this; 69 | self.db.executeSql('SELECT * FROM Threads', {}).then((data) => { 70 | if (data.rows.length > 0) { 71 | for (var i = 0; i < data.rows.length; i++) { 72 | console.log(data.rows.item(i)); 73 | console.log(data.rows.item(i).key); 74 | console.log(data.rows.item(i).title); 75 | console.log(data.rows.item(i).question); 76 | } 77 | } else { 78 | console.log('no threads found..'); 79 | } 80 | }, (err) => { 81 | console.error('Unable to print threads: ', err); 82 | }); 83 | } 84 | 85 | createThreads() { 86 | var self = this; 87 | self.db.executeSql('CREATE TABLE IF NOT EXISTS Threads ( key VARCHAR(255) PRIMARY KEY NOT NULL, title text NOT NULL, question text NOT NULL, category text NOT NULL, datecreated text, USER VARCHAR(255), comments INT NULL);', {}).then(() => { 88 | }, (err) => { 89 | console.error('Unable to create Threads table: ', err); 90 | }); 91 | } 92 | 93 | createComments() { 94 | var self = this; 95 | self.db.executeSql('CREATE TABLE IF NOT EXISTS Comments ( key VARCHAR(255) PRIMARY KEY NOT NULL, thread VARCHAR(255) NOT NULL, text text NOT NULL, USER VARCHAR(255) NOT NULL, datecreated text, votesUp INT NULL, votesDown INT NULL);', {}).then(() => { 96 | }, (err) => { 97 | console.error('Unable to create Comments table: ', err); 98 | }); 99 | } 100 | 101 | createUsers() { 102 | var self = this; 103 | self.db.executeSql('CREATE TABLE IF NOT EXISTS Users ( uid text PRIMARY KEY NOT NULL, username text NOT NULL); ', {}).then(() => { 104 | }, (err) => { 105 | console.error('Unable to create Users table: ', err); 106 | }); 107 | } 108 | 109 | saveUsers(users: IUser[]) { 110 | var self = this; 111 | 112 | users.forEach(user => { 113 | self.addUser(user); 114 | }); 115 | } 116 | 117 | addUser(user: IUser) { 118 | var self = this; 119 | let query: string = 'INSERT INTO Users (uid, username) Values (?,?)'; 120 | self.db.executeSql(query, [user.uid, user.username]).then((data) => { 121 | console.log('user ' + user.username + ' added'); 122 | }, (err) => { 123 | console.error('Unable to add user: ', err); 124 | }); 125 | } 126 | 127 | saveThreads(threads: IThread[]) { 128 | let self = this; 129 | let users: IUser[] = []; 130 | 131 | threads.forEach(thread => { 132 | if (!self.itemsService.includesItem(users, u => u.uid === thread.user.uid)) { 133 | console.log('in add user..' + thread.user.username); 134 | users.push(thread.user); 135 | } else { 136 | console.log('user found: ' + thread.user.username); 137 | } 138 | self.addThread(thread); 139 | }); 140 | 141 | self.saveUsers(users); 142 | } 143 | 144 | addThread(thread: IThread) { 145 | var self = this; 146 | 147 | let query: string = 'INSERT INTO Threads (key, title, question, category, datecreated, user, comments) VALUES (?,?,?,?,?,?,?)'; 148 | self.db.executeSql(query, [ 149 | thread.key, 150 | thread.title, 151 | thread.question, 152 | thread.category, 153 | thread.dateCreated, 154 | thread.user.uid, 155 | thread.comments 156 | ]).then((data) => { 157 | console.log('thread ' + thread.key + ' added'); 158 | }, (err) => { 159 | console.error('Unable to add thread: ', err); 160 | }); 161 | } 162 | 163 | getThreads(): any { 164 | var self = this; 165 | return self.db.executeSql('SELECT Threads.*, username FROM Threads INNER JOIN Users ON Threads.user = Users.uid', {}); 166 | } 167 | } -------------------------------------------------------------------------------- /src/shared/validators/checked.validator.ts: -------------------------------------------------------------------------------- 1 | import { FormControl } from '@angular/forms'; 2 | import { ValidationResult } from '../interfaces'; 3 | 4 | export class CheckedValidator { 5 | 6 | public static isChecked(control: FormControl): ValidationResult { 7 | var valid = control.value === false || control.value === 'false'; 8 | if (valid) { 9 | return { isChecked: true }; 10 | } 11 | return null; 12 | } 13 | } -------------------------------------------------------------------------------- /src/shared/validators/email.validator.ts: -------------------------------------------------------------------------------- 1 | import { FormControl } from '@angular/forms'; 2 | import { ValidationResult } from '../interfaces'; 3 | 4 | export class EmailValidator { 5 | 6 | public static isValid(control: FormControl): ValidationResult { 7 | var emailReg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 8 | 9 | let valid = emailReg.test(control.value); 10 | 11 | if (!valid) { 12 | return { isValid: true }; 13 | } 14 | return null; 15 | } 16 | } -------------------------------------------------------------------------------- /src/theme/app.variables.scss: -------------------------------------------------------------------------------- 1 | 2 | $toolbar-background : #0087be; 3 | //$list-background-color : white; 4 | $card-ios-background-color: #f4f4f4; 5 | $card-md-background-color: #f4f4f4; 6 | $card-wp-background-color: #f4f4f4; 7 | scroll-content { background-color: whitesmoke;} 8 | ion-list .item .item-inner { 9 | background: whitesmoke; 10 | } 11 | .item { 12 | background-color: whitesmoke !important; 13 | } 14 | 15 | ion-card { 16 | background: white !important; 17 | } 18 | 19 | .left-border-primary { 20 | border-left: 4px solid #0087be; 21 | } -------------------------------------------------------------------------------- /src/theme/global.scss: -------------------------------------------------------------------------------- 1 | // http://ionicframework.com/docs/v2/theming/ 2 | 3 | 4 | // Global CSS 5 | // -------------------------------------------------- 6 | // Put CSS rules here that you want to apply globally. 7 | // 8 | // To declare rules for a specific mode, create a child rule 9 | // for the .md, .ios, or .wp mode classes. The mode class is 10 | // automatically applied to the element in the app. 11 | // 12 | // App Shared Sass variables belong in app.variables.scss. 13 | -------------------------------------------------------------------------------- /src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | // Ionic Variables and Theming. For more info, please see: 2 | // http://ionicframework.com/docs/v2/theming/ 3 | @import "ionic.globals"; 4 | 5 | 6 | // Shared Variables 7 | // -------------------------------------------------- 8 | // To customize the look and feel of this app, you can override 9 | // the Sass variables found in Ionic's source scss files. 10 | // To view all the possible Ionic variables, see: 11 | // http://ionicframework.com/docs/v2/theming/overriding-ionic-variables/ 12 | 13 | $text-color: #000; 14 | $background-color: #fff; 15 | 16 | 17 | // Named Color Variables 18 | // -------------------------------------------------- 19 | // Named colors makes it easy to reuse colors on various components. 20 | // It's highly recommended to change the default colors 21 | // to match your app's branding. Ionic uses a Sass map of 22 | // colors so you can add, rename and remove colors as needed. 23 | // The "primary" color is the only required color in the map. 24 | 25 | $colors: ( 26 | primary: #0087be, 27 | secondary: #32db64, 28 | danger: #f53d3d, 29 | light: #f4f4f4, 30 | dark: #222, 31 | favorite: #69BB7B 32 | ); 33 | 34 | 35 | // App Theme 36 | // -------------------------------------------------- 37 | // Ionic apps can have different themes applied, which can 38 | // then be future customized. This import comes last 39 | // so that the above variables are used and Ionic's 40 | // default are overridden. 41 | 42 | @import "ionic.theme.default"; 43 | 44 | 45 | // Ionicons 46 | // -------------------------------------------------- 47 | // The premium icon font for Ionic. For more info, please see: 48 | // http://ionicframework.com/docs/v2/ionicons/ 49 | 50 | $ionicons-font-path: "../assets/fonts"; 51 | @import "ionicons"; 52 | 53 | 54 | //$toolbar-background : #0087be; 55 | //$list-background-color : white; 56 | $card-ios-background-color: #f4f4f4; 57 | $card-md-background-color: #f4f4f4; 58 | $card-wp-background-color: #f4f4f4; 59 | scroll-content { background-color: whitesmoke;} 60 | ion-list .item .item-inner { 61 | background: whitesmoke; 62 | } 63 | .item { 64 | background-color: whitesmoke !important; 65 | } 66 | 67 | ion-card { 68 | background: white !important; 69 | } 70 | 71 | .left-border-primary { 72 | border-left: 4px solid #0087be; 73 | } 74 | 75 | .segment-md .segment-button { 76 | color:black !important; 77 | } 78 | 79 | // App Shared Imports 80 | // -------------------------------------------------- 81 | // These are the imports which make up the design of this app. 82 | // By default each design mode includes these shared imports. 83 | // App Shared Sass variables belong in app.variables.scss. 84 | 85 | @import "../pages/tabs/tabs"; 86 | @import "../pages/threads/threads"; 87 | @import "../pages/thread-create/thread-create"; 88 | @import "../pages/thread-comments/thread-comments"; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": true, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "lib": [ 8 | "dom", 9 | "es2015" 10 | ], 11 | "module": "es2015", 12 | "moduleResolution": "node", 13 | "target": "es5" 14 | }, 15 | "exclude": [ 16 | "node_modules" 17 | ], 18 | "compileOnSave": false, 19 | "atom": { 20 | "rewriteTsconfig": false 21 | } 22 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-duplicate-variable": true, 4 | "no-unused-variable": [ 5 | true 6 | ] 7 | }, 8 | "rulesDirectory": [ 9 | "node_modules/tslint-eslint-rules/dist/rules" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "lodash": "registry:npm/lodash#4.0.0+20160416211519" 4 | }, 5 | "devDependencies": {}, 6 | "globalDependencies": { 7 | "jquery": "registry:dt/jquery#1.10.0+20160417213236", 8 | "es6-shim": "registry:dt/es6-shim#0.31.2+20160602141504" 9 | } 10 | } -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ionic App 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | --------------------------------------------------------------------------------