├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── angular-cli.json ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.json ├── firebase.json ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── qrcode.png ├── src ├── app │ ├── about │ │ ├── about-list.component.html │ │ ├── about-list.component.scss │ │ ├── about-list.component.spec.ts │ │ ├── about-list.component.ts │ │ ├── about-routing.module.ts │ │ ├── about.component.html │ │ ├── about.component.scss │ │ ├── about.component.spec.ts │ │ ├── about.component.ts │ │ ├── about.module.ts │ │ ├── index.ts │ │ └── interfaces │ │ │ ├── about-item.interface.ts │ │ │ └── index.ts │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── chat │ │ ├── chat.module.ts │ │ ├── entry │ │ │ ├── entry-routing.module.ts │ │ │ ├── entry.component.html │ │ │ ├── entry.component.scss │ │ │ ├── entry.component.spec.ts │ │ │ ├── entry.component.ts │ │ │ ├── entry.module.ts │ │ │ ├── guards │ │ │ │ ├── entry.guard.spec.ts │ │ │ │ ├── entry.guard.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── room │ │ │ ├── guards │ │ │ ├── index.ts │ │ │ ├── nick.guard.spec.ts │ │ │ └── nick.guard.ts │ │ │ ├── index.ts │ │ │ ├── interfaces │ │ │ ├── chat-item.interface.ts │ │ │ ├── index.ts │ │ │ └── resolves.interface.ts │ │ │ ├── resolves │ │ │ ├── index.ts │ │ │ ├── messages.resolver.ts │ │ │ ├── nick.resolver.spec.ts │ │ │ └── nick.resolver.ts │ │ │ ├── room-routing.module.ts │ │ │ ├── room.component.html │ │ │ ├── room.component.scss │ │ │ ├── room.component.spec.ts │ │ │ ├── room.component.ts │ │ │ └── room.module.ts │ ├── config │ │ ├── .gitignore │ │ └── config.ts_example │ ├── index.ts │ ├── layout │ │ ├── footer │ │ │ ├── footer-routing.module.ts │ │ │ ├── footer.component.html │ │ │ ├── footer.component.scss │ │ │ ├── footer.component.spec.ts │ │ │ ├── footer.component.ts │ │ │ ├── footer.module.ts │ │ │ └── index.ts │ │ ├── header │ │ │ ├── header-routing.module.ts │ │ │ ├── header.component.html │ │ │ ├── header.component.scss │ │ │ ├── header.component.spec.ts │ │ │ ├── header.component.ts │ │ │ ├── header.module.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── layout.module.ts │ ├── login │ │ ├── index.ts │ │ ├── login-routing.module.ts │ │ ├── login.component.html │ │ ├── login.component.scss │ │ ├── login.component.spec.ts │ │ ├── login.component.ts │ │ └── login.module.ts │ ├── shared │ │ ├── animations │ │ │ ├── animations.service.ts │ │ │ ├── animations.ts │ │ │ └── index.ts │ │ ├── authentication │ │ │ ├── authentication.guard.ts │ │ │ ├── authentication.module.ts │ │ │ └── index.ts │ │ ├── directives │ │ │ └── index.ts │ │ ├── index.ts │ │ └── shared.module.ts │ └── todos │ │ ├── index.ts │ │ ├── interfaces │ │ ├── index.ts │ │ └── todo-item.interface.ts │ │ ├── resolves │ │ ├── index.ts │ │ └── todos.resolver.ts │ │ ├── todos-routing.module.ts │ │ ├── todos.component.html │ │ ├── todos.component.scss │ │ ├── todos.component.spec.ts │ │ ├── todos.component.ts │ │ └── todos.module.ts ├── assets │ ├── .gitkeep │ ├── .npmignore │ ├── angular.png │ └── firebase.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.json └── typings.d.ts ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | *.launch 16 | .settings/ 17 | 18 | # misc 19 | /.sass-cache 20 | /connect.lock 21 | /coverage/* 22 | /libpeerconnection.log 23 | npm-debug.log 24 | testem.log 25 | /typings 26 | 27 | # e2e 28 | /e2e/*.js 29 | /e2e/*.map 30 | 31 | #System Files 32 | .DS_Store 33 | Thumbs.db 34 | /.firebaserc 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # We need sudo rights to setup testing environment correctly 2 | sudo: required 3 | 4 | # And the language is node.js :D 5 | language: node_js 6 | 7 | # Node.js versions that we're testing 8 | node_js: 9 | - "stable" 10 | - "6" 11 | 12 | # OS that we're testing 13 | os: 14 | - linux 15 | - osx 16 | 17 | # Before install we need to do some OS specified stuff 18 | before_install: 19 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi 20 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew outdated xctool || brew upgrade xctool; fi 21 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew cask install google-chrome; fi # Karma CI 22 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CHROME_BIN=chromium-browser; fi # Karma CI 23 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export DISPLAY=:99.0; fi 24 | - cp src/app/config/config.ts_example src/app/config/config.ts 25 | 26 | install: 27 | - npm install 28 | 29 | # Before actual tests we need to install angular-cli and finally start window manager if tests are run on linux 30 | before_script: 31 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh -e /etc/init.d/xvfb start; fi 32 | 33 | # Actual test script, note that we need to pass --watch=false option for this 34 | script: 35 | - ./node_modules/.bin/ng lint && ./node_modules/.bin/ng test --watch=false --code-coverage 36 | 37 | # And please send emails on errors 38 | notifications: 39 | email: true 40 | 41 | # whitelisted branches 42 | branches: 43 | only: 44 | - master 45 | 46 | after_success: 47 | - bash <(curl -s https://codecov.io/bash) 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2016 Tarmo Leppänen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular/Firebase/Material - Demo 2 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 3 | [![Build Status](https://travis-ci.org/tarlepp/angular2-firebase-material-demo.png?branch=master)](https://travis-ci.org/tarlepp/angular2-firebase-material-demo) 4 | [![codecov](https://codecov.io/gh/tarlepp/angular2-firebase-material-demo/branch/master/graph/badge.svg)](https://codecov.io/gh/tarlepp/angular2-firebase-material-demo) 5 | [![Dependency Status](https://david-dm.org/tarlepp/angular2-firebase-material-demo.svg)](https://david-dm.org/tarlepp/angular2-firebase-material-demo) 6 | [![devDependency Status](https://david-dm.org/tarlepp/angular2-firebase-material-demo/dev-status.svg)](https://david-dm.org/tarlepp/angular2-firebase-material-demo#info=devDependencies) 7 | 8 | ## Table of Contents 9 | * [What is this](#what-is-this) 10 | * [Demo](#demo) 11 | * [Used libraries, guides, etc.](#used-libraries-guides-etc) 12 | * [Libraries](#libraries) 13 | * [Guides](#guides) 14 | * [Other resources](#other-resources) 15 | * [Installation](#installation) 16 | * [Configuration](#configuration) 17 | * [Firebase](#firebase) 18 | * [Development](#development) 19 | * [Tests](#tests) 20 | * [Unit tests](#unit-tests) 21 | * [e2e tests](#e2e-tests) 22 | * [Build](#build) 23 | * [Author](#author) 24 | * [License](#license) 25 | 26 | ## What is this 27 | Just a small demo to show how to use [Angular2](https://angular.io/) + [Firebase](https://firebase.google.com/) + 28 | [Google Material Design](https://www.google.com/design/spec/material-design/introduction.html) together. Currently 29 | this demo application contains following features: 30 | * Social media login (Facebook, Twitter, Google+ and GitHub) 31 | * Personal 'Todo' item list 32 | * Chat with other users 33 | 34 | ## Demo 35 | Demo of this application can be found from [https://fir-todo-v3.firebaseapp.com/](https://fir-todo-v3.firebaseapp.com/). 36 | 37 | ![QR code to demo application](https://raw.github.com/tarlepp/angular2-firebase-material-demo/master/qrcode.png) 38 | 39 | ## Used libraries, guides, etc. 40 | 41 | ### Libraries 42 | * [Angular 2](https://github.com/angular/angular) 43 | * [Material Design for Angular 2](https://github.com/angular/material2) 44 | * [AngularFire2](https://github.com/angular/angularfire2) 45 | * [angular2-moment](https://github.com/urish/angular2-moment) 46 | * [Angular-CLI](https://github.com/angular/angular-cli) 47 | 48 | ### Guides 49 | * [Angular 2 style guide](https://angular.io/docs/ts/latest/guide/style-guide.html) 50 | 51 | ### Other resources 52 | * [Firebase](https://firebase.google.com/) 53 | * [Material design](https://www.google.com/design/spec/material-design/) 54 | 55 | ## Installation 56 | First of all you have to install ```npm``` and ```node.js``` to your box. Installation instructions can 57 | be found [here](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager). 58 | 59 | Note that ```node.js 6.x``` is required. 60 | 61 | ```bash 62 | $ git clone https://github.com/tarlepp/angular2-firebase-material-demo.git 63 | $ cd angular2-firebase-material-demo 64 | 65 | # install the project's dependencies 66 | $ npm install 67 | 68 | # fast install (via Yarn, https://yarnpkg.com) 69 | $ yarn install # or yarn 70 | ``` 71 | 72 | ### Configuration 73 | See ```/src/app/config/config.ts_example``` file and copy it to ```/src/app/config/config.ts``` file and make 74 | necessary changes to it. Note that you need a Firebase account to get all necessary config values. 75 | 76 | ### Firebase 77 | To get Firebase running as it should first you need to make new Firebase application. Which you can create easily from 78 | their website [https://firebase.google.com/](https://firebase.google.com/). 79 | 80 | After you have created new application you need to make some [security rules](https://firebase.google.com/docs/database/security/quickstart) 81 | for the used data storage. Below is configuration that this demo application uses, so you can use the same within your 82 | application. 83 | 84 | ``` 85 | { 86 | "rules": { 87 | "messages": { 88 | ".write": "auth !== null", 89 | ".read": "auth !== null" 90 | }, 91 | "todos": { 92 | "$uid": { 93 | // grants write access to the owner of this user account whose uid must exactly match the key ($uid) 94 | ".write": "auth !== null && auth.uid === $uid", 95 | // grants read access to any user who is logged in with Facebook 96 | ".read": "auth !== null && auth.uid === $uid" 97 | } 98 | } 99 | } 100 | } 101 | ``` 102 | 103 | These rules ensure that 'todo' items are show only to user who made those. Also chat messages requires that user is 104 | logged in to read / write those. 105 | 106 | ## Development 107 | To start developing in the project run: 108 | 109 | ```bash 110 | $ npm start 111 | # OR 112 | $ ng serve 113 | ``` 114 | 115 | Then head to `http://localhost:4200` in your browser. 116 | 117 | ## Tests 118 | 119 | ### Unit tests 120 | To run tests run: 121 | ```bash 122 | $ npm test 123 | # OR 124 | $ ng test 125 | ``` 126 | 127 | ### e2e tests 128 | To run tests run: 129 | ```bash 130 | $ npm run e2e 131 | # OR 132 | $ ng e2e 133 | ``` 134 | 135 | ## Build 136 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 137 | 138 | ## Author 139 | Tarmo Leppänen 140 | 141 | ## License 142 | [The MIT License (MIT)](LICENSE) 143 | 144 | Copyright (c) 2016 Tarmo Leppänen 145 | -------------------------------------------------------------------------------- /angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "version": "1.0.0-beta.32.3", 4 | "name": "angular2-firebase-material-demo" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.json", 19 | "prefix": "app", 20 | "styles": [ 21 | "styles.scss" 22 | ], 23 | "scripts": [ 24 | "../node_modules/firebase/firebase.js" 25 | ], 26 | "environmentSource": "environments/environment.ts", 27 | "environments": { 28 | "dev": "environments/environment.ts", 29 | "prod": "environments/environment.prod.ts" 30 | } 31 | } 32 | ], 33 | "addons": [], 34 | "packages": [], 35 | "e2e": { 36 | "protractor": { 37 | "config": "./protractor.conf.js" 38 | } 39 | }, 40 | "lint": [ 41 | { 42 | "files": "src/**/*.ts", 43 | "project": "src/tsconfig.json" 44 | }, 45 | { 46 | "files": "e2e/**/*.ts", 47 | "project": "e2e/tsconfig.json" 48 | } 49 | ], 50 | "test": { 51 | "karma": { 52 | "config": "./karma.conf.js" 53 | } 54 | }, 55 | "defaults": { 56 | "styleExt": "scss", 57 | "prefixInterfaces": false, 58 | "inline": { 59 | "style": false, 60 | "template": false 61 | }, 62 | "spec": { 63 | "class": false, 64 | "component": true, 65 | "directive": true, 66 | "module": false, 67 | "pipe": true, 68 | "service": true 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Angular2FirebaseMaterialDemoPage } from './app.po'; 2 | 3 | describe('angular2-firebase-material-demo App', function() { 4 | let page: Angular2FirebaseMaterialDemoPage; 5 | 6 | beforeEach(() => { 7 | page = new Angular2FirebaseMaterialDemoPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('app works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class Angular2FirebaseMaterialDemoPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "outDir": "../dist/out-tsc-e2e", 10 | "sourceMap": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "../node_modules/@types" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-mocha-reporter'), 11 | require('karma-chrome-launcher'), 12 | require('karma-phantomjs-launcher'), 13 | require('karma-remap-istanbul'), 14 | require('@angular/cli/plugins/karma') 15 | ], 16 | files: [ 17 | { pattern: './src/test.ts', watched: false } 18 | ], 19 | preprocessors: { 20 | './src/test.ts': ['@angular/cli'] 21 | }, 22 | mime: { 23 | 'text/x-typescript': ['ts','tsx'] 24 | }, 25 | remapIstanbulReporter: { 26 | reports: { 27 | html: 'coverage', 28 | lcovonly: './coverage/coverage.lcov' 29 | } 30 | }, 31 | angularCli: { 32 | config: './angular-cli.json', 33 | environment: 'dev' 34 | }, 35 | reporters: config.angularCli && config.angularCli.codeCoverage 36 | ? ['mocha', 'karma-remap-istanbul'] 37 | : ['mocha'], 38 | port: 9876, 39 | colors: true, 40 | logLevel: config.LOG_INFO, 41 | autoWatch: true, 42 | browsers: ['Chrome'], 43 | singleRun: false 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-firebase-material-demo", 3 | "version": "0.0.0", 4 | "author": "Tarmo Leppänen ", 5 | "license": "MIT", 6 | "keywords": [ 7 | "angular2", 8 | "firebase", 9 | "material" 10 | ], 11 | "angular-cli": {}, 12 | "scripts": { 13 | "start": "./node_modules/.bin/ng serve --host 0.0.0.0", 14 | "lint": "tslint \"src/**/*.ts\"", 15 | "test": "./node_modules/.bin/ng test", 16 | "pree2e": "webdriver-manager update", 17 | "e2e": "protractor" 18 | }, 19 | "private": true, 20 | "dependencies": { 21 | "@angular/common": "2.4.8", 22 | "@angular/compiler": "2.4.8", 23 | "@angular/core": "2.4.8", 24 | "@angular/flex-layout": "^2.0.0-beta.5", 25 | "@angular/forms": "2.4.8", 26 | "@angular/http": "2.4.8", 27 | "@angular/material": "2.0.0-beta.2", 28 | "@angular/platform-browser": "2.4.8", 29 | "@angular/platform-browser-dynamic": "2.4.8", 30 | "@angular/router": "3.4.8", 31 | "angular2-moment": "1.2.0", 32 | "angularfire2": "2.0.0-beta.8", 33 | "core-js": "2.4.1", 34 | "firebase": "3.6.10", 35 | "hammerjs": "2.0.8", 36 | "ng2-webstorage": "1.5.0", 37 | "rxjs": "5.2.0", 38 | "ts-helpers": "1.1.2", 39 | "zone.js": "0.7.7" 40 | }, 41 | "devDependencies": { 42 | "@angular/cli": "1.0.0-beta.32.3", 43 | "@angular/compiler-cli": "2.4.8", 44 | "@types/core-js": "0.9.35", 45 | "@types/jasmine": "2.5.43", 46 | "codelyzer": "2.0.1", 47 | "jasmine-core": "2.5.2", 48 | "jasmine-spec-reporter": "3.2.0", 49 | "karma": "1.5.0", 50 | "karma-chrome-launcher": "2.0.0", 51 | "karma-cli": "1.0.1", 52 | "karma-jasmine": "1.1.0", 53 | "karma-mocha-reporter": "2.2.2", 54 | "karma-remap-istanbul": "0.6.0", 55 | "karma-phantomjs-launcher": "1.0.2", 56 | "protractor": "5.1.1", 57 | "ts-node": "2.1.0", 58 | "tslint": "4.4.2", 59 | "typescript": "2.2.1" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/docs/referenceConf.js 3 | 4 | /*global jasmine */ 5 | var SpecReporter = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './e2e/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | useAllAngular2AppRoots: true, 24 | beforeLaunch: function() { 25 | require('ts-node').register({ 26 | project: 'e2e' 27 | }); 28 | }, 29 | onPrepare: function() { 30 | jasmine.getEnv().addReporter(new SpecReporter()); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarlepp/angular-firebase-material-demo/f5546d66ce8c174c925f1a6792b6fb024c53f5ea/qrcode.png -------------------------------------------------------------------------------- /src/app/about/about-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{item.name}} 4 | 5 | filter_none 6 | 7 |

{{item.name}}

8 | 9 |

10 | {{item.url}} 11 |

12 |
13 |
14 | -------------------------------------------------------------------------------- /src/app/about/about-list.component.scss: -------------------------------------------------------------------------------- 1 | md-list { 2 | padding: 0; 3 | margin-bottom: 16px; 4 | } 5 | 6 | .material-icons { 7 | font-size: 40px; 8 | color: lightgrey; 9 | } 10 | 11 | :host /deep/ .md-list-item { 12 | &:hover { 13 | background-color: rgba(0, 0, 0, .06); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/about/about-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, ComponentFixture, async } from '@angular/core/testing'; 2 | import { MaterialModule } from '@angular/material'; 3 | 4 | import { AboutListComponent } from './index'; 5 | 6 | describe('Component: /about/about-list.component.ts', () => { 7 | let component: AboutListComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ 13 | AboutListComponent, 14 | ], 15 | imports: [ 16 | MaterialModule.forRoot(), 17 | ], 18 | }) 19 | .compileComponents(); 20 | })); 21 | 22 | beforeEach(() => { 23 | fixture = TestBed.createComponent(AboutListComponent); 24 | component = fixture.componentInstance; 25 | 26 | fixture.detectChanges(); 27 | }); 28 | 29 | it('should create the component', () => { 30 | expect(component).toBeTruthy(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/about/about-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-about-list', 5 | templateUrl: './about-list.component.html', 6 | styleUrls: ['./about-list.component.scss'] 7 | }) 8 | 9 | export class AboutListComponent { 10 | @Input() 11 | items: any; 12 | 13 | @Input() 14 | className: string; 15 | 16 | public constructor() { } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/about/about-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { AboutComponent } from './index'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | { 10 | path: 'about', 11 | component: AboutComponent, 12 | }, 13 | ]), 14 | ], 15 | exports: [ 16 | RouterModule, 17 | ], 18 | }) 19 | 20 | export class AboutRoutingModule { } 21 | -------------------------------------------------------------------------------- /src/app/about/about.component.html: -------------------------------------------------------------------------------- 1 |
2 |

What is this?

3 | 4 |

5 | This is a small demo about Angular2, Firebase and Material design. Main purpose of this demo application is to 6 | show how powerful these components are together. 7 |

8 | 9 |
10 |
11 |

Used libraries

12 | 13 | 17 |
18 | 19 |
20 |

External links

21 | 22 | 26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /src/app/about/about.component.scss: -------------------------------------------------------------------------------- 1 | h4 { 2 | margin-bottom: 0; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/about/about.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, ComponentFixture, async } from '@angular/core/testing'; 2 | import { By } from '@angular/platform-browser'; 3 | import { MaterialModule } from '@angular/material'; 4 | 5 | import { AnimationsService } from '../shared/animations/index'; 6 | import { AboutComponent, AboutListComponent } from './index'; 7 | 8 | describe('Component: /about/about.component.ts', () => { 9 | let component: AboutComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [ 15 | AboutComponent, 16 | AboutListComponent, 17 | ], 18 | imports: [ 19 | MaterialModule.forRoot(), 20 | ], 21 | providers: [ 22 | AnimationsService, 23 | ], 24 | }) 25 | .compileComponents(); 26 | })); 27 | 28 | beforeEach(() => { 29 | fixture = TestBed.createComponent(AboutComponent); 30 | component = fixture.componentInstance; 31 | 32 | fixture.detectChanges(); 33 | }); 34 | 35 | it('should create the component', () => { 36 | expect(component).toBeTruthy(); 37 | }); 38 | 39 | it('should render expected count \'Used libraries\' items', () => { 40 | const mdListElements = fixture.debugElement.query(By.css('.used-libraries')).children; 41 | 42 | expect(mdListElements.length).toEqual(fixture.componentInstance.libraries.length); 43 | }); 44 | 45 | it('should render expected count \'External links\' items', () => { 46 | const mdListElements = fixture.debugElement.query(By.css('.external-links')).children; 47 | 48 | expect(mdListElements.length).toEqual(fixture.componentInstance.externalLinks.length); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/app/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { Animations, AnimationsService } from '../shared/'; 4 | import { AboutItem } from './interfaces/'; 5 | 6 | @Component({ 7 | selector: 'app-about', 8 | templateUrl: './about.component.html', 9 | styleUrls: ['./about.component.scss'], 10 | }) 11 | 12 | export class AboutComponent extends Animations { 13 | public libraries: AboutItem[] = [ 14 | { 15 | name: 'Angular 2', 16 | logo: '/assets/angular.png', 17 | url: 'https://github.com/angular/angular', 18 | }, 19 | { 20 | name: 'Material Design for Angular 2', 21 | logo: '/assets/angular.png', 22 | url: 'https://github.com/angular/material2', 23 | }, 24 | { 25 | name: 'AngularFire2', 26 | logo: '/assets/firebase.png', 27 | url: 'https://github.com/angular/angularfire2', 28 | }, 29 | { 30 | name: 'angular2-moment', 31 | url: 'https://github.com/urish/angular2-moment', 32 | }, 33 | ]; 34 | 35 | public externalLinks: AboutItem[] = [ 36 | { 37 | name: 'Angular 2', 38 | logo: '/assets/angular.png', 39 | url: 'https://angular.io', 40 | }, 41 | { 42 | name: 'Material design', 43 | url: 'https://material.google.com', 44 | }, 45 | { 46 | name: 'Firebase', 47 | logo: '/assets/firebase.png', 48 | url: 'https://firebase.google.com', 49 | }, 50 | { 51 | name: 'Angular 2 style guide', 52 | logo: '/assets/angular.png', 53 | url: 'https://angular.io/docs/ts/latest/guide/style-guide.html', 54 | }, 55 | { 56 | name: 'Moment.js', 57 | url: 'http://momentjs.com/', 58 | }, 59 | ]; 60 | 61 | /** 62 | * Constructor of the class 63 | * 64 | * @param {AnimationsService} animationsService 65 | */ 66 | public constructor( 67 | protected animationsService: AnimationsService 68 | ) { 69 | super(animationsService); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/about/about.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SharedModule } from './../shared/shared.module'; 4 | import { AboutComponent, AboutListComponent } from './index'; 5 | import { AboutRoutingModule } from './about-routing.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | AboutRoutingModule, 11 | ], 12 | declarations: [ 13 | AboutComponent, 14 | AboutListComponent, 15 | ], 16 | exports: [ 17 | AboutComponent, 18 | AboutListComponent, 19 | ], 20 | }) 21 | 22 | export class AboutModule { } 23 | -------------------------------------------------------------------------------- /src/app/about/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces'; 2 | export * from './about.component'; 3 | export * from './about-list.component'; 4 | -------------------------------------------------------------------------------- /src/app/about/interfaces/about-item.interface.ts: -------------------------------------------------------------------------------- 1 | export interface AboutItem { 2 | url: string; 3 | name: string; 4 | logo?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/about/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './about-item.interface'; 2 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | const appRoutes = [ 5 | { 6 | path: '', 7 | pathMatch: 'full', 8 | redirectTo: 'about', 9 | }, 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [ 14 | RouterModule.forRoot(appRoutes), 15 | ], 16 | exports: [ 17 | RouterModule, 18 | ], 19 | providers: [], 20 | }) 21 | 22 | export class AppRoutingModule { } 23 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |
10 | 11 |
12 | 13 |
14 | 15 |
16 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | header { 2 | order: 1; 3 | flex-shrink: 0; 4 | } 5 | 6 | article { 7 | order: 2; 8 | flex: 1; 9 | overflow: auto; 10 | } 11 | 12 | footer { 13 | order: 3; 14 | flex-shrink: 0; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { AppComponent } from './app.component'; 5 | 6 | describe('App: Angular2FirebaseMaterialDemo', () => { 7 | /* 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [ 11 | AppComponent 12 | ], 13 | }); 14 | }); 15 | 16 | it('should create the app', async(() => { 17 | let fixture = TestBed.createComponent(AppComponent); 18 | let app = fixture.debugElement.componentInstance; 19 | expect(app).toBeTruthy(); 20 | })); 21 | 22 | it(`should have as title 'app works!'`, async(() => { 23 | let fixture = TestBed.createComponent(AppComponent); 24 | let app = fixture.debugElement.componentInstance; 25 | expect(app.title).toEqual('app works!'); 26 | })); 27 | 28 | it('should render title in a h1 tag', async(() => { 29 | let fixture = TestBed.createComponent(AppComponent); 30 | fixture.detectChanges(); 31 | let compiled = fixture.debugElement.nativeElement; 32 | expect(compiled.querySelector('h1').textContent).toContain('app works!'); 33 | })); 34 | */ 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { AnimationsService } from './shared/animations/index'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.scss'], 9 | }) 10 | 11 | export class AppComponent { 12 | public activateAnimation = false; 13 | 14 | /** 15 | * Constructor of the class 16 | * 17 | * @param {AnimationsService} animationService 18 | */ 19 | public constructor( 20 | private animationService: AnimationsService 21 | ) { 22 | this.animationService.activateAnimation$.subscribe( 23 | (value) => this.activateAnimation = value 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { AppRoutingModule } from './app-routing.module'; 4 | import { AppComponent } from './app.component'; 5 | import { SharedModule } from './shared/shared.module'; 6 | import { LayoutModule } from './layout/layout.module'; 7 | import { AboutModule } from './about/about.module'; 8 | import { LoginModule } from './login/login.module'; 9 | import { TodosModule } from './todos/todos.module'; 10 | import { ChatModule } from './chat/chat.module'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | AppComponent, 15 | ], 16 | imports: [ 17 | AppRoutingModule, 18 | SharedModule, 19 | LayoutModule, 20 | AboutModule, 21 | LoginModule, 22 | TodosModule, 23 | ChatModule, 24 | ], 25 | bootstrap: [ 26 | AppComponent, 27 | ], 28 | }) 29 | 30 | export class AppModule { } 31 | -------------------------------------------------------------------------------- /src/app/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { RoomModule } from './room/room.module'; 4 | import { EntryModule } from './entry/entry.module'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | EntryModule, 9 | RoomModule, 10 | ], 11 | exports: [ 12 | EntryModule, 13 | RoomModule, 14 | ], 15 | }) 16 | 17 | export class ChatModule { } 18 | -------------------------------------------------------------------------------- /src/app/chat/entry/entry-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { AuthenticationGuard } from './../../shared'; 5 | import { EntryComponent, EntryGuard } from './index'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | RouterModule.forChild([ 10 | { 11 | path: 'chat/entry', 12 | component: EntryComponent, 13 | canActivate: [ 14 | AuthenticationGuard, 15 | EntryGuard, 16 | ], 17 | }, 18 | ]), 19 | ], 20 | exports: [ 21 | RouterModule, 22 | ], 23 | }) 24 | 25 | export class EntryRoutingModule { } 26 | -------------------------------------------------------------------------------- /src/app/chat/entry/entry.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
6 | 7 | 11 | 12 | 13 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /src/app/chat/entry/entry.component.scss: -------------------------------------------------------------------------------- 1 | .form-entry { 2 | padding: 24px 16px 16px 16px; 3 | background: #fafafa; 4 | border: 1px solid #ebebeb; 5 | box-shadow: rgba(0, 0, 0, 0.14902) 0 1px 1px 0, rgba(0, 0, 0, 0.09804) 0 1px 2px 0; 6 | } -------------------------------------------------------------------------------- /src/app/chat/entry/entry.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, ComponentFixture, async } from '@angular/core/testing'; 2 | import { By } from '@angular/platform-browser'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { Router } from '@angular/router'; 5 | import { MaterialModule } from '@angular/material'; 6 | import { LocalStorageService, Ng2Webstorage } from 'ng2-webstorage'; 7 | 8 | import 'hammerjs'; 9 | 10 | import { AnimationsService } from '../../shared/animations/index'; 11 | import { EntryComponent } from './entry.component'; 12 | 13 | describe('Component: /chat/entry/entry.component.ts', () => { 14 | let component: EntryComponent; 15 | let fixture: ComponentFixture; 16 | 17 | beforeEach(async(() => { 18 | const fakeLocalStorageService = { 19 | store: (nick: string) => nick, 20 | }; 21 | 22 | const fakeRouter = { 23 | navigate: (commands: any) => commands, 24 | }; 25 | 26 | TestBed.configureTestingModule({ 27 | declarations: [ 28 | EntryComponent, 29 | ], 30 | imports: [ 31 | FormsModule, 32 | Ng2Webstorage, 33 | MaterialModule.forRoot(), 34 | ], 35 | providers: [ 36 | AnimationsService, 37 | { 38 | provide: LocalStorageService, 39 | useValue: fakeLocalStorageService, 40 | }, 41 | { 42 | provide: Router, 43 | useValue: fakeRouter, 44 | }, 45 | ] 46 | }) 47 | .compileComponents(); 48 | })); 49 | 50 | beforeEach(() => { 51 | fixture = TestBed.createComponent(EntryComponent); 52 | component = fixture.componentInstance; 53 | 54 | fixture.detectChanges(); 55 | }); 56 | 57 | it('should create the component', () => { 58 | expect(component).toBeTruthy(); 59 | }); 60 | 61 | it('should not allow to click submit button if no nick given (button should be disabled)', () => { 62 | const button = fixture.debugElement.query(By.css('button')); 63 | 64 | expect(button.nativeElement.disabled).toBe(true, 'submit button is not disabled'); 65 | }); 66 | 67 | describe('After entering nick', () => { 68 | it('should allow to click submit button (button should not be disabled)', () => { 69 | const inputElement: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; 70 | const button: HTMLButtonElement = fixture.debugElement.query(By.css('button')).nativeElement; 71 | 72 | // Fake a `change` event being triggered. 73 | inputElement.value = 'new nick'; 74 | 75 | fixture.detectChanges(); 76 | 77 | expect(button.disabled).toBe(false, 'submit button is not enabled'); 78 | }); 79 | 80 | it('should store nick to local storage on submit', () => { 81 | const localStorageService = fixture.debugElement.injector.get(LocalStorageService); 82 | const inputElement: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; 83 | const form = fixture.debugElement.query(By.css('form')); 84 | 85 | spyOn(localStorageService, 'store'); 86 | 87 | // Fake a `change` event being triggered. 88 | inputElement.value = 'new nick'; 89 | 90 | form.triggerEventHandler('submit', null); 91 | 92 | fixture.detectChanges(); 93 | 94 | expect(localStorageService.store).toHaveBeenCalled(); 95 | }); 96 | 97 | it('should redirect user to chat on submit', () => { 98 | const router = fixture.debugElement.injector.get(Router); 99 | const inputElement: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; 100 | const form = fixture.debugElement.query(By.css('form')); 101 | 102 | spyOn(router, 'navigate'); 103 | 104 | // Fake a `change` event being triggered. 105 | inputElement.value = 'new nick'; 106 | 107 | form.triggerEventHandler('submit', null); 108 | 109 | fixture.detectChanges(); 110 | 111 | expect(router.navigate).toHaveBeenCalledWith(['/chat']); 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /src/app/chat/entry/entry.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { LocalStorageService } from 'ng2-webstorage'; 4 | 5 | import { Animations, AnimationsService } from '../../shared/'; 6 | 7 | @Component({ 8 | selector: 'app-entry', 9 | templateUrl: './entry.component.html', 10 | styleUrls: ['./entry.component.scss'], 11 | }) 12 | 13 | export class EntryComponent extends Animations implements OnInit { 14 | @ViewChild('nickControl') nickControl: any; 15 | 16 | /** 17 | * Constructor of the class. 18 | * 19 | * @param {AnimationsService} animationsService 20 | * @param {LocalStorageService} localStorage 21 | * @param {Router} router 22 | */ 23 | public constructor( 24 | protected animationsService: AnimationsService, 25 | private localStorage: LocalStorageService, 26 | private router: Router 27 | ) { 28 | super(animationsService); 29 | } 30 | 31 | /** 32 | * ngOnInit lifecycle hook. 33 | * 34 | * @see https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html 35 | */ 36 | public ngOnInit() { 37 | // this.nickControl.focus(); 38 | } 39 | 40 | /** 41 | * Method to store nick to local storage and redirect user. 42 | * 43 | * @param {string} nick 44 | */ 45 | public enterNick(nick: string) { 46 | this.localStorage.store('nick', nick); 47 | 48 | this.router.navigate(['/chat']); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/chat/entry/entry.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SharedModule } from '../../shared/shared.module'; 4 | import { EntryComponent, EntryGuard } from './index'; 5 | import { EntryRoutingModule } from './entry-routing.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | EntryRoutingModule, 11 | ], 12 | declarations: [ 13 | EntryComponent, 14 | ], 15 | exports: [ 16 | EntryComponent, 17 | ], 18 | providers: [ 19 | EntryGuard, 20 | ], 21 | }) 22 | 23 | export class EntryModule { } 24 | -------------------------------------------------------------------------------- /src/app/chat/entry/guards/entry.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject} from '@angular/core/testing'; 2 | import { Router } from '@angular/router'; 3 | import { LocalStorageService } from 'ng2-webstorage'; 4 | 5 | import { EntryGuard } from './entry.guard'; 6 | 7 | class StubLocalStorageService { 8 | private data = {}; 9 | 10 | store(key: string, value: any): void { 11 | this.data[key] = value; 12 | } 13 | 14 | retrieve(key: string): any { 15 | return this.data[key]; 16 | } 17 | } 18 | 19 | describe('Guard: /chat/entry/guards/entry.guard.ts', () => { 20 | const route: any = {}; 21 | const state: any = {}; 22 | 23 | beforeEach(() => { 24 | const fakeRouter = { 25 | navigate: (commands: any[]) => commands, 26 | }; 27 | 28 | TestBed.configureTestingModule({ 29 | providers: [ 30 | EntryGuard, 31 | { 32 | provide: LocalStorageService, 33 | useClass: StubLocalStorageService, 34 | }, 35 | { 36 | provide: Router, 37 | useValue: fakeRouter, 38 | }, 39 | ], 40 | }); 41 | }); 42 | 43 | it('should create the guard', inject([EntryGuard], (guard: EntryGuard) => { 44 | expect(guard).toBeTruthy(); 45 | })); 46 | 47 | it('should try retrieve nick from local storage', inject( 48 | [EntryGuard, LocalStorageService], 49 | (guard: EntryGuard, storage: LocalStorageService) => { 50 | spyOn(storage, 'retrieve'); 51 | 52 | guard.canActivate(route, state); 53 | 54 | expect(storage.retrieve).toHaveBeenCalledWith('nick'); 55 | }) 56 | ); 57 | 58 | it('should return true if nick isn\'t in local storage', inject([EntryGuard], (guard: EntryGuard) => { 59 | expect(guard.canActivate(route, state)).toBeTruthy(); 60 | })); 61 | 62 | it('should return false if nick is in local storage', inject( 63 | [EntryGuard, LocalStorageService], 64 | (guard: EntryGuard, storage: LocalStorageService) => { 65 | storage.store('nick', 'foo'); 66 | 67 | expect(guard.canActivate(route, state)).not.toBeTruthy(); 68 | }) 69 | ); 70 | 71 | it('should redirect user if nick is in local storage', inject( 72 | [EntryGuard, LocalStorageService, Router], 73 | (guard: EntryGuard, storage: LocalStorageService, router: Router) => { 74 | spyOn(router, 'navigate'); 75 | 76 | storage.store('nick', 'foo'); 77 | 78 | guard.canActivate(route, state); 79 | 80 | expect(router.navigate).toHaveBeenCalledWith(['/chat']); 81 | }) 82 | ); 83 | }); 84 | -------------------------------------------------------------------------------- /src/app/chat/entry/guards/entry.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; 3 | import { LocalStorageService } from 'ng2-webstorage'; 4 | 5 | @Injectable() 6 | export class EntryGuard implements CanActivate { 7 | /** 8 | * Constructor of the class. 9 | * 10 | * @param {LocalStorageService} localStorage 11 | * @param {Router} router 12 | */ 13 | public constructor( 14 | private localStorage: LocalStorageService, 15 | private router: Router 16 | ) { } 17 | 18 | /** 19 | * Purpose of this guard is check if current user has not given nick for chat. If nick is found from local storage 20 | * user will be redirected to actual chat and route that uses this guard cannot be activated. 21 | * 22 | * @param {ActivatedRouteSnapshot} route 23 | * @param {RouterStateSnapshot} state 24 | * @returns {boolean} 25 | */ 26 | public canActivate( 27 | route: ActivatedRouteSnapshot, 28 | state: RouterStateSnapshot 29 | ): boolean { 30 | if (!this.localStorage.retrieve('nick')) { 31 | return true; 32 | } 33 | 34 | this.router.navigate(['/chat']); 35 | 36 | return false; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/chat/entry/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entry.guard'; 2 | -------------------------------------------------------------------------------- /src/app/chat/entry/index.ts: -------------------------------------------------------------------------------- 1 | export * from './guards/'; 2 | export * from './entry.component'; 3 | -------------------------------------------------------------------------------- /src/app/chat/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entry/'; 2 | export * from './room/'; 3 | -------------------------------------------------------------------------------- /src/app/chat/room/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nick.guard'; 2 | -------------------------------------------------------------------------------- /src/app/chat/room/guards/nick.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject} from '@angular/core/testing'; 2 | import { Router } from '@angular/router'; 3 | import { LocalStorageService } from 'ng2-webstorage'; 4 | 5 | import { NickGuard } from './nick.guard'; 6 | 7 | class StubLocalStorageService { 8 | private data = {}; 9 | 10 | store(key: string, value: any): void { 11 | this.data[key] = value; 12 | } 13 | 14 | retrieve(key: string): any { 15 | return this.data[key]; 16 | } 17 | } 18 | 19 | describe('Guard: /chat/room/guards/nick.guard.ts', () => { 20 | const route: any = {}; 21 | const state: any = {}; 22 | 23 | beforeEach(() => { 24 | const fakeRouter = { 25 | navigate: (commands: any[]) => commands, 26 | }; 27 | 28 | TestBed.configureTestingModule({ 29 | providers: [ 30 | NickGuard, 31 | { 32 | provide: LocalStorageService, 33 | useClass: StubLocalStorageService, 34 | }, 35 | { 36 | provide: Router, 37 | useValue: fakeRouter, 38 | }, 39 | ], 40 | }); 41 | }); 42 | 43 | it('should create the guard', inject([NickGuard], (guard: NickGuard) => { 44 | expect(guard).toBeTruthy(); 45 | })); 46 | 47 | it('should try retrieve nick from local storage', inject( 48 | [NickGuard, LocalStorageService], 49 | (guard: NickGuard, storage: LocalStorageService) => { 50 | spyOn(storage, 'retrieve'); 51 | 52 | guard.canActivate(route, state); 53 | 54 | expect(storage.retrieve).toHaveBeenCalledWith('nick'); 55 | }) 56 | ); 57 | 58 | it('should return false if nick isn\'t in local storage', inject([NickGuard], (guard: NickGuard) => { 59 | expect(guard.canActivate(route, state)).not.toBeTruthy(); 60 | })); 61 | 62 | it('should redirect user if nick isn\'t in local storage', inject( 63 | [NickGuard, LocalStorageService, Router], 64 | (guard: NickGuard, storage: LocalStorageService, router: Router) => { 65 | spyOn(router, 'navigate'); 66 | 67 | guard.canActivate(route, state); 68 | 69 | expect(router.navigate).toHaveBeenCalledWith(['/chat/entry']); 70 | }) 71 | ); 72 | 73 | it('should return true if nick is in local storage', inject( 74 | [NickGuard, LocalStorageService], 75 | (guard: NickGuard, storage: LocalStorageService) => { 76 | storage.store('nick', 'foo'); 77 | 78 | expect(guard.canActivate(route, state)).toBeTruthy(); 79 | }) 80 | ); 81 | }); 82 | -------------------------------------------------------------------------------- /src/app/chat/room/guards/nick.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; 3 | import { LocalStorageService } from 'ng2-webstorage'; 4 | 5 | /** 6 | * This class implements a guard for routes that require successful authentication. 7 | */ 8 | @Injectable() 9 | export class NickGuard implements CanActivate { 10 | /** 11 | * Constructor of the class. 12 | * 13 | * @param {LocalStorageService} localStorage 14 | * @param {Router} router 15 | */ 16 | constructor( 17 | private localStorage: LocalStorageService, 18 | private router: Router 19 | ) { } 20 | 21 | /** 22 | * Purpose of this guard is check if current user has not given nick for chat. If nick is found from local storage 23 | * user will be allowed to enter chat. If nick isn't found from local storage user is redirected nick entry route. 24 | * 25 | * @param {ActivatedRouteSnapshot} route 26 | * @param {RouterStateSnapshot} state 27 | * @returns {boolean} 28 | */ 29 | canActivate( 30 | route: ActivatedRouteSnapshot, 31 | state: RouterStateSnapshot 32 | ): boolean { 33 | if (!!this.localStorage.retrieve('nick')) { 34 | return true; 35 | } 36 | 37 | this.router.navigate(['/chat/entry']); 38 | 39 | return false; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/chat/room/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces/'; 2 | export * from './guards/'; 3 | export * from './resolves/'; 4 | export * from './room.component'; 5 | -------------------------------------------------------------------------------- /src/app/chat/room/interfaces/chat-item.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ChatItem { 2 | $key: string; 3 | message: string; 4 | nick: boolean; 5 | createdAt: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/chat/room/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chat-item.interface'; 2 | export * from './resolves.interface'; 3 | -------------------------------------------------------------------------------- /src/app/chat/room/interfaces/resolves.interface.ts: -------------------------------------------------------------------------------- 1 | import { FirebaseListObservable } from 'angularfire2'; 2 | 3 | import { ChatItem } from './chat-item.interface'; 4 | 5 | export interface Resolves { 6 | messages: FirebaseListObservable; 7 | nick: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/chat/room/resolves/index.ts: -------------------------------------------------------------------------------- 1 | export * from './messages.resolver'; 2 | export * from './nick.resolver'; 3 | -------------------------------------------------------------------------------- /src/app/chat/room/resolves/messages.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | import { AngularFire, FirebaseListObservable } from 'angularfire2'; 4 | 5 | import { ChatItem } from '../interfaces/chat-item.interface'; 6 | 7 | @Injectable() 8 | export class MessagesResolver implements Resolve { 9 | /** 10 | * Constructor of the class. 11 | * 12 | * @param {AngularFire} angularFire 13 | */ 14 | constructor(private angularFire: AngularFire) { } 15 | 16 | /** 17 | * Resolve method to get latest chat messages from FireBase. 18 | * 19 | * Note that this method relies that 'AuthenticationGuard' is run within route 'canActivate' block. 20 | * 21 | * @param {ActivatedRouteSnapshot} route 22 | * @param {RouterStateSnapshot} state 23 | * @returns {Promise>} 24 | */ 25 | resolve( 26 | route: ActivatedRouteSnapshot, 27 | state: RouterStateSnapshot 28 | ): Promise> { 29 | const list = this.angularFire.database.list( 30 | '/messages/', 31 | { 32 | query: { 33 | limitToLast: 100 34 | } 35 | } 36 | ); 37 | 38 | return new Promise((resolve, reject) => { 39 | list.first().subscribe(() => resolve(list), reject); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/chat/room/resolves/nick.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject} from '@angular/core/testing'; 2 | import { NickResolver } from './nick.resolver'; 3 | import { LocalStorageService } from 'ng2-webstorage'; 4 | 5 | class StubLocalStorageService { 6 | private data = {}; 7 | 8 | store(key: string, value: any): void { 9 | this.data[key] = value; 10 | } 11 | 12 | retrieve(key: string): any { 13 | return this.data[key]; 14 | } 15 | } 16 | 17 | describe('Resolver: /chat/room/resolves/nick.resolver.ts', () => { 18 | const route: any = {}; 19 | const state: any = {}; 20 | 21 | beforeEach(() => { 22 | TestBed.configureTestingModule({ 23 | providers: [ 24 | NickResolver, 25 | { 26 | provide: LocalStorageService, 27 | useClass: StubLocalStorageService, 28 | }, 29 | ], 30 | }); 31 | }); 32 | 33 | it('should return nick when it is on local storage', inject( 34 | [NickResolver, LocalStorageService], 35 | (resolver: NickResolver, storage: LocalStorageService) => { 36 | storage.store('nick', 'foo'); 37 | 38 | expect(resolver.resolve(route, state)).toEqual('foo', 'Stored nick in the local storage is not expected one.'); 39 | }) 40 | ); 41 | 42 | it('should not return nick when it is not on local storage', inject([NickResolver], (resolver: NickResolver) => { 43 | expect(resolver.resolve(route, state)).toBeUndefined('Nick is not in local storage but resolver found something.'); 44 | }) 45 | ); 46 | }); 47 | -------------------------------------------------------------------------------- /src/app/chat/room/resolves/nick.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import { LocalStorageService } from 'ng2-webstorage'; 5 | 6 | @Injectable() 7 | export class NickResolver implements Resolve { 8 | /** 9 | * Constructor of the class. 10 | * 11 | * @param {LocalStorageService} localStorage 12 | */ 13 | constructor(private localStorage: LocalStorageService) { } 14 | 15 | /** 16 | * Resolve method to get user nick for chat. 17 | * 18 | * Note that this method relies that 'RoomGuard' is run within route 'canActivate' block. 19 | * 20 | * @param {ActivatedRouteSnapshot} route 21 | * @param {RouterStateSnapshot} state 22 | * @returns {any|string} 23 | */ 24 | resolve( 25 | route: ActivatedRouteSnapshot, 26 | state: RouterStateSnapshot 27 | ): Observable|Promise|any { 28 | return this.localStorage.retrieve('nick'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/chat/room/room-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { AuthenticationGuard } from './../../shared'; 5 | import { RoomComponent, NickGuard, MessagesResolver, NickResolver } from './index'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | RouterModule.forChild([ 10 | { 11 | path: 'chat', 12 | component: RoomComponent, 13 | canActivate: [ 14 | AuthenticationGuard, 15 | NickGuard, 16 | ], 17 | resolve: { 18 | messages: MessagesResolver, 19 | nick: NickResolver, 20 | }, 21 | }, 22 | ]), 23 | ], 24 | exports: [ 25 | RouterModule, 26 | ], 27 | }) 28 | 29 | export class RoomRoutingModule { } 30 | -------------------------------------------------------------------------------- /src/app/chat/room/room.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |

7 | {{((message.createdAt / 1000) | amFromUnix) | amDateFormat: 'DD.MM.YY HH:mm:ss'}} 8 | {{message.nick}} 9 |

10 |

11 | {{message.message}} 12 |

13 |
14 |
15 |
16 |
17 | 18 |
19 |
20 | 21 | 26 | 27 | 28 | 33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /src/app/chat/room/room.component.scss: -------------------------------------------------------------------------------- 1 | .outer { 2 | display: flex; 3 | flex-direction: column; 4 | flex: 1; 5 | height: 100%; 6 | } 7 | 8 | .container { 9 | display: flex; 10 | height: 100%; 11 | width: 100%; 12 | flex: 1; 13 | border-bottom: 1px solid rgba(0, 0, 0, 0.14902); 14 | overflow-y: scroll; 15 | } 16 | 17 | h3 { 18 | font-family: monospace; 19 | font-size: 14px; 20 | line-height: 14px; 21 | } 22 | 23 | h3, 24 | p { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/chat/room/room.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async, ComponentFixture } from '@angular/core/testing'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | import { LocalStorageService, Ng2Webstorage } from 'ng2-webstorage'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { MaterialModule } from '@angular/material'; 6 | import { MomentModule } from 'angular2-moment'; 7 | import { By } from '@angular/platform-browser'; 8 | 9 | import { AnimationsService } from '../../shared/animations/index'; 10 | import { RoomComponent } from './room.component'; 11 | 12 | class StubLocalStorageService { 13 | private data = {}; 14 | 15 | store(key: string, value: any): void { 16 | this.data[key] = value; 17 | } 18 | 19 | retrieve(key: string): any { 20 | return this.data[key]; 21 | } 22 | } 23 | 24 | class StubActivatedRoute { 25 | public data = { 26 | subscribe: StubActivatedRoute.subscribe 27 | }; 28 | 29 | static subscribe() { 30 | return { 31 | messages: [], 32 | nick: 'foo bar', 33 | }; 34 | } 35 | } 36 | 37 | describe('Component: /chat/room/room.component.ts', () => { 38 | let component: RoomComponent; 39 | let fixture: ComponentFixture; 40 | 41 | beforeEach(async(() => { 42 | const fakeRouter = { 43 | navigateByUrl: (url: string) => url, 44 | }; 45 | 46 | TestBed.configureTestingModule({ 47 | declarations: [ 48 | RoomComponent, 49 | ], 50 | imports: [ 51 | FormsModule, 52 | Ng2Webstorage, 53 | MomentModule, 54 | MaterialModule.forRoot(), 55 | ], 56 | providers: [ 57 | AnimationsService, 58 | { 59 | provide: Router, 60 | useValue: fakeRouter, 61 | }, 62 | { 63 | provide: ActivatedRoute, 64 | useClass: StubActivatedRoute, 65 | }, 66 | { 67 | provide: LocalStorageService, 68 | useClass: StubLocalStorageService, 69 | }, 70 | ], 71 | }) 72 | .compileComponents(); 73 | })); 74 | 75 | beforeEach(() => { 76 | fixture = TestBed.createComponent(RoomComponent); 77 | component = fixture.componentInstance; 78 | 79 | fixture.detectChanges(); 80 | }); 81 | 82 | it('should create the component', async(() => { 83 | expect(fixture.debugElement.componentInstance).toBeTruthy(); 84 | })); 85 | 86 | it('should not allow to click submit button if no message given (button should be disabled)', () => { 87 | const button: HTMLButtonElement = fixture.debugElement.query(By.css('button')).nativeElement; 88 | 89 | expect(button.disabled).toBe(true, 'submit button is not disabled'); 90 | }); 91 | 92 | describe('After entering message', () => { 93 | it('should allow to click submit button (button should not be disabled)', () => { 94 | const inputElement: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; 95 | const button: HTMLButtonElement = fixture.debugElement.query(By.css('button')).nativeElement; 96 | 97 | // Fake a `change` event being triggered. 98 | inputElement.value = 'awesome message!'; 99 | 100 | fixture.detectChanges(); 101 | 102 | expect(button.disabled).toBe(false, 'submit button is not enabled'); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /src/app/chat/room/room.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { FirebaseListObservable } from 'angularfire2'; 4 | import { LocalStorageService } from 'ng2-webstorage'; 5 | 6 | import { ChatItem, Resolves } from './interfaces/'; 7 | import { Animations, AnimationsService } from '../../shared/'; 8 | 9 | @Component({ 10 | selector: 'app-chat-room', 11 | templateUrl: './room.component.html', 12 | styleUrls: ['./room.component.scss'], 13 | }) 14 | 15 | export class RoomComponent extends Animations implements OnInit { 16 | @ViewChild('messageControl') messageControl: any; 17 | 18 | public messages: FirebaseListObservable; 19 | public message: string; 20 | private nick: string; 21 | 22 | /** 23 | * Constructor of the class 24 | * 25 | * @param {AnimationsService} animationsService 26 | * @param {Router} router 27 | * @param {ActivatedRoute} activatedRoute 28 | * @param {LocalStorageService} localStorage 29 | */ 30 | public constructor( 31 | protected animationsService: AnimationsService, 32 | private router: Router, 33 | private activatedRoute: ActivatedRoute, 34 | private localStorage: LocalStorageService 35 | ) { 36 | super(animationsService); 37 | } 38 | 39 | /** 40 | * ngOnInit lifecycle hook. 41 | * 42 | * @see https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html 43 | */ 44 | public ngOnInit() { 45 | this.activatedRoute.data.subscribe((data: Resolves) => { 46 | this.messages = data.messages; 47 | this.nick = data.nick; 48 | }); 49 | 50 | // this.messageControl.focus(); 51 | } 52 | 53 | /** 54 | * Method to add new message to chat. 55 | * 56 | * Special cases for messages: 57 | * Message Action 58 | * /quit Quits from chat room and redirect to chat entry 59 | */ 60 | public addNewMessage() { 61 | if (this.message === '/quit') { 62 | this.localStorage.clear('nick'); 63 | 64 | this.router.navigateByUrl('/chat/entry'); 65 | 66 | return; 67 | } 68 | 69 | this.messages.push({ 70 | nick: this.nick, 71 | message: this.message, 72 | createdAt: firebase.database.ServerValue.TIMESTAMP, 73 | }); 74 | 75 | this.message = ''; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/chat/room/room.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SharedModule } from './../../shared/shared.module'; 4 | import { RoomComponent, MessagesResolver, NickGuard, NickResolver } from './index'; 5 | import { RoomRoutingModule } from './room-routing.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | RoomRoutingModule, 11 | ], 12 | declarations: [ 13 | RoomComponent, 14 | ], 15 | exports: [ 16 | RoomComponent, 17 | ], 18 | providers: [ 19 | MessagesResolver, 20 | NickGuard, 21 | NickResolver, 22 | ], 23 | }) 24 | 25 | export class RoomModule { } 26 | -------------------------------------------------------------------------------- /src/app/config/.gitignore: -------------------------------------------------------------------------------- 1 | config.ts -------------------------------------------------------------------------------- /src/app/config/config.ts_example: -------------------------------------------------------------------------------- 1 | import * as firebase from 'firebase'; // See https://github.com/angular/angularfire2/issues/529 2 | import { AuthProviders, AuthMethods } from 'angularfire2'; 3 | 4 | export const FIREBASE_CONFIG = { 5 | apiKey: '', 6 | authDomain: '', 7 | databaseURL: '', 8 | storageBucket: '', 9 | messagingSenderId: '', 10 | }; 11 | 12 | export const FIREBASE_AUTH_CONFIG = { 13 | provider: AuthProviders.Google, 14 | method: AuthMethods.Redirect, 15 | }; 16 | 17 | export class Config { 18 | public static FIREBASE_CONFIG = FIREBASE_CONFIG; 19 | public static FIREBASE_AUTH_CONFIG = FIREBASE_AUTH_CONFIG; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.component'; 2 | export * from './app.module'; 3 | -------------------------------------------------------------------------------- /src/app/layout/footer/footer-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { FooterComponent } from './index'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | { 10 | path: '', 11 | component: FooterComponent, 12 | outlet: 'footer', 13 | }, 14 | ]), 15 | ], 16 | exports: [ 17 | RouterModule, 18 | ], 19 | }) 20 | 21 | export class FooterRoutingModule { } 22 | -------------------------------------------------------------------------------- /src/app/layout/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | {{link.icon}} 8 | 9 | {{link.name}} 10 | 11 | 12 | 13 |
14 | 15 |
-------------------------------------------------------------------------------- /src/app/layout/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | span { 2 | padding: 0 16px; 3 | } 4 | 5 | a { 6 | color: #eee; 7 | font-size: 16px; 8 | text-decoration: none; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | 13 | span { 14 | padding: 0; 15 | } 16 | 17 | &:hover { 18 | span { 19 | text-decoration: underline; 20 | } 21 | } 22 | } 23 | 24 | md-icon { 25 | margin-right: 8px; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/layout/footer/footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { FooterComponent } from './footer.component'; 5 | 6 | describe('Component: Footer', () => { 7 | /* 8 | it('should create an instance', () => { 9 | let component = new FooterComponent(); 10 | expect(component).toBeTruthy(); 11 | }); 12 | */ 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/layout/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | templateUrl: './footer.component.html', 6 | styleUrls: ['./footer.component.scss'] 7 | }) 8 | 9 | export class FooterComponent implements OnInit { 10 | public links: any[] = [ 11 | { 12 | name: 'GitHub', 13 | url: 'https://github.com/tarlepp/angular2-firebase-material-demo', 14 | icon: 'web', 15 | }, 16 | { 17 | name: 'Issues', 18 | url: 'https://github.com/tarlepp/angular2-firebase-material-demo/issues', 19 | icon: 'bug_report', 20 | }, 21 | { 22 | name: 'Tarmo Leppänen', 23 | url: 'https://github.com/tarlepp', 24 | icon: 'person', 25 | }, 26 | ]; 27 | 28 | constructor() { } 29 | 30 | ngOnInit() { } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/layout/footer/footer.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SharedModule } from '../../shared/shared.module'; 4 | import { FooterComponent } from './index'; 5 | import { FooterRoutingModule } from './footer-routing.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | FooterRoutingModule, 11 | ], 12 | declarations: [ 13 | FooterComponent, 14 | ], 15 | exports: [ 16 | FooterComponent, 17 | ], 18 | }) 19 | 20 | export class FooterModule { } 21 | -------------------------------------------------------------------------------- /src/app/layout/footer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './footer.component'; 2 | -------------------------------------------------------------------------------- /src/app/layout/header/header-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { HeaderComponent } from './index'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | { 10 | path: '', 11 | component: HeaderComponent, 12 | outlet: 'header', 13 | }, 14 | ]), 15 | ], 16 | exports: [ 17 | RouterModule, 18 | ], 19 | }) 20 | 21 | export class HeaderRoutingModule { } 22 | -------------------------------------------------------------------------------- /src/app/layout/header/header.component.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 |
18 |
19 | 20 | angular2-firebase-material-demo 21 | 22 |
23 | 24 |
25 | 33 |
34 | 35 |
36 | 44 |
45 |
46 | -------------------------------------------------------------------------------- /src/app/layout/header/header.component.scss: -------------------------------------------------------------------------------- 1 | span { 2 | margin-left: 8px; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/layout/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { HeaderComponent } from './header.component'; 5 | 6 | describe('Component: Header', () => { 7 | /* 8 | it('should create an instance', () => { 9 | let component = new HeaderComponent(); 10 | expect(component).toBeTruthy(); 11 | }); 12 | */ 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/layout/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AngularFire } from 'angularfire2'; 3 | import { Router } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-header', 7 | templateUrl: './header.component.html', 8 | styleUrls: ['./header.component.scss'] 9 | }) 10 | 11 | export class HeaderComponent implements OnInit { 12 | constructor( 13 | public af: AngularFire, 14 | public router: Router 15 | ) { } 16 | 17 | ngOnInit() {} 18 | 19 | logout() { 20 | this.af.auth.logout(); 21 | 22 | this.router.navigateByUrl('/about'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/layout/header/header.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SharedModule } from '../../shared/shared.module'; 4 | import { HeaderComponent } from './index'; 5 | import { HeaderRoutingModule } from './header-routing.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | HeaderRoutingModule, 11 | ], 12 | declarations: [ 13 | HeaderComponent, 14 | ], 15 | exports: [ 16 | HeaderComponent, 17 | ], 18 | }) 19 | 20 | export class HeaderModule { } 21 | -------------------------------------------------------------------------------- /src/app/layout/header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './header.component'; 2 | -------------------------------------------------------------------------------- /src/app/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './footer/'; 2 | export * from './header/'; 3 | -------------------------------------------------------------------------------- /src/app/layout/layout.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { FooterModule } from './footer/footer.module'; 4 | import { HeaderModule } from './header/header.module'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | FooterModule, 9 | HeaderModule, 10 | ], 11 | exports: [ 12 | FooterModule, 13 | HeaderModule, 14 | ], 15 | }) 16 | 17 | export class LayoutModule { } 18 | -------------------------------------------------------------------------------- /src/app/login/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.component'; 2 | -------------------------------------------------------------------------------- /src/app/login/login-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { LoginComponent } from './index'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | { 10 | path: 'login', 11 | component: LoginComponent, 12 | }, 13 | ]), 14 | ], 15 | exports: [ 16 | RouterModule, 17 | ], 18 | }) 19 | 20 | export class LoginRoutingModule { } 21 | -------------------------------------------------------------------------------- /src/app/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 | 14 |
15 | -------------------------------------------------------------------------------- /src/app/login/login.component.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | width: 440px; 3 | margin: 0 auto; 4 | padding: 16px 8px; 5 | background: #fafafa; 6 | border: 1px solid #ebebeb; 7 | box-shadow: rgba(0, 0, 0, 0.14902) 0 1px 1px 0, rgba(0, 0, 0, 0.09804) 0 1px 2px 0; 8 | text-align: center; 9 | 10 | .text { 11 | margin-bottom: 16px; 12 | } 13 | 14 | button { 15 | margin: 0 4px; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { LoginComponent } from './login.component'; 5 | 6 | describe('Component: Login', () => { 7 | /* 8 | it('should create an instance', () => { 9 | let component = new LoginComponent(); 10 | expect(component).toBeTruthy(); 11 | }); 12 | */ 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { AngularFire, AuthMethods, AuthProviders } from 'angularfire2'; 4 | 5 | import { Animations, AnimationsService } from '../shared/'; 6 | 7 | @Component({ 8 | selector: 'app-login', 9 | templateUrl: './login.component.html', 10 | styleUrls: ['./login.component.scss'], 11 | }) 12 | 13 | export class LoginComponent extends Animations implements OnInit { 14 | /** 15 | * Constructor of the class. 16 | * 17 | * @param {AnimationsService} animationsService 18 | * @param {AngularFire} angularFire 19 | * @param {Router} router 20 | */ 21 | public constructor( 22 | protected animationsService: AnimationsService, 23 | private angularFire: AngularFire, 24 | private router: Router 25 | ) { 26 | super(animationsService); 27 | } 28 | 29 | /** 30 | * ngOnInit lifecycle hook. 31 | * 32 | * @see https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html 33 | */ 34 | public ngOnInit() { 35 | this.angularFire.auth.subscribe(auth => { 36 | if (auth && auth.uid) { 37 | this.router.navigateByUrl('/about'); 38 | } 39 | }); 40 | } 41 | 42 | /** 43 | * Method to login with specified provider. 44 | * 45 | * @param {string} provider 46 | */ 47 | public login(provider: string) { 48 | this.angularFire.auth 49 | .login({ 50 | provider: AuthProviders[provider], 51 | method: AuthMethods.Popup, 52 | }) 53 | .then(() => { 54 | this.router.navigate(['/todos']); 55 | }) 56 | .catch(error => { 57 | alert(error); 58 | }) 59 | ; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SharedModule } from '../shared/shared.module'; 4 | import { LoginComponent } from './index'; 5 | import { LoginRoutingModule } from './login-routing.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | LoginRoutingModule, 11 | ], 12 | declarations: [ 13 | LoginComponent, 14 | ], 15 | exports: [ 16 | LoginComponent, 17 | ], 18 | }) 19 | 20 | export class LoginModule { } 21 | -------------------------------------------------------------------------------- /src/app/shared/animations/animations.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs/Subject'; 3 | 4 | @Injectable() 5 | export class AnimationsService { 6 | // Observable source 7 | private activateAnimation = new Subject(); 8 | 9 | // Observable stream 10 | public activateAnimation$ = this.activateAnimation.asObservable(); 11 | 12 | // Service message commands 13 | announceMission(value: boolean) { 14 | if (!value) { 15 | setTimeout(() => { 16 | this.activateAnimation.next(value); 17 | }, 0); 18 | } else { 19 | this.activateAnimation.next(value); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/shared/animations/animations.ts: -------------------------------------------------------------------------------- 1 | import { OnDestroy, AfterViewInit } from '@angular/core'; 2 | 3 | import { AnimationsService } from './index'; 4 | 5 | export abstract class Animations implements OnDestroy, AfterViewInit { 6 | public activateAnimation = true; 7 | 8 | /** 9 | * Constructor of the class. 10 | * 11 | * @param {AnimationsService} animationsService 12 | */ 13 | public constructor( 14 | protected animationsService: AnimationsService 15 | ) { } 16 | 17 | /** 18 | * ngOnDestroy lifecycle hook. 19 | * 20 | * @see https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html 21 | */ 22 | public ngOnDestroy(): void { 23 | this.animationsService.announceMission(true); 24 | } 25 | 26 | /** 27 | * ngAfterViewInit lifecycle hook. 28 | * 29 | * @see https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html 30 | */ 31 | public ngAfterViewInit(): void { 32 | this.animationsService.announceMission(false); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/shared/animations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './animations.service'; 2 | export * from './animations'; 3 | -------------------------------------------------------------------------------- /src/app/shared/authentication/authentication.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanActivate } from '@angular/router'; 3 | import { AngularFire, FirebaseAuthState } from 'angularfire2'; 4 | import { Observable } from 'rxjs/Observable'; 5 | 6 | import 'rxjs/add/operator/do'; 7 | import 'rxjs/add/operator/map'; 8 | import 'rxjs/add/operator/take'; 9 | 10 | /** 11 | * This class implements a guard for routes that require successful authentication. 12 | */ 13 | @Injectable() 14 | export class AuthenticationGuard implements CanActivate { 15 | /** 16 | * Constructor of the class 17 | * 18 | * @param {AngularFire} angularFire 19 | * @param {Router} router 20 | */ 21 | constructor( 22 | private angularFire: AngularFire, 23 | private router: Router 24 | ) {} 25 | 26 | /** 27 | * To protect routes from being accessible without authentication, the `canActivate()` method checks that current 28 | * user has been authenticated via FireBaseAuth service and current auth state is valid. Only then navigation will 29 | * pass on the requested route. Otherwise user will be redirected to login page. 30 | * 31 | * @returns {Observable} 32 | */ 33 | canActivate(): Observable { 34 | return this.angularFire.auth 35 | .take(1) 36 | .map((authState: FirebaseAuthState) => { 37 | !!authState ? localStorage.setItem('uid', authState.uid) : localStorage.removeItem('uid'); 38 | 39 | return !!authState; 40 | }) 41 | .do(authenticated => { 42 | if (!authenticated) { 43 | this.router.navigate(['/login']); 44 | } 45 | }) 46 | ; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/shared/authentication/authentication.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { AngularFireModule } from 'angularfire2'; 3 | 4 | import { Config } from './../../config/config'; 5 | import { AuthenticationGuard } from './authentication.guard'; 6 | 7 | /** 8 | * Do not specify providers for modules that might be imported by a lazy loaded module. 9 | */ 10 | @NgModule({ 11 | imports: [ 12 | AngularFireModule.initializeApp(Config.FIREBASE_CONFIG, Config.FIREBASE_AUTH_CONFIG), 13 | ], 14 | }) 15 | 16 | export class AuthenticationModule { 17 | static forRoot(): ModuleWithProviders { 18 | return { 19 | ngModule: AuthenticationModule, 20 | providers: [ 21 | AuthenticationGuard 22 | ] 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/shared/authentication/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication.guard'; 2 | -------------------------------------------------------------------------------- /src/app/shared/directives/index.ts: -------------------------------------------------------------------------------- 1 | export const Directives = []; 2 | -------------------------------------------------------------------------------- /src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication/'; 2 | export * from './directives/'; 3 | export * from './animations/'; 4 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { RouterModule } from '@angular/router'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { HttpModule } from '@angular/http'; 6 | import { NgModule } from '@angular/core'; 7 | import { MaterialModule, MdIconRegistry } from '@angular/material'; 8 | import { FlexLayoutModule } from '@angular/flex-layout'; 9 | import { AngularFireModule } from 'angularfire2'; 10 | import { MomentModule } from 'angular2-moment'; 11 | import { Ng2Webstorage } from 'ng2-webstorage'; 12 | 13 | import 'hammerjs'; 14 | 15 | import { AuthenticationModule } from './authentication/authentication.module'; 16 | import { Directives } from './directives/'; 17 | import { Config } from '../config/config'; 18 | import { AnimationsService } from './animations/animations.service'; 19 | 20 | @NgModule({ 21 | declarations: [ 22 | ...Directives, 23 | ], 24 | imports: [ 25 | CommonModule, 26 | BrowserModule, 27 | FormsModule, 28 | HttpModule, 29 | RouterModule, 30 | AuthenticationModule.forRoot(), 31 | MaterialModule, 32 | FlexLayoutModule, 33 | AngularFireModule.initializeApp(Config.FIREBASE_CONFIG, Config.FIREBASE_AUTH_CONFIG), 34 | MomentModule, 35 | Ng2Webstorage, 36 | ], 37 | providers: [ 38 | MdIconRegistry, 39 | AnimationsService, 40 | ], 41 | exports: [ 42 | CommonModule, 43 | BrowserModule, 44 | FormsModule, 45 | HttpModule, 46 | RouterModule, 47 | AuthenticationModule, 48 | MaterialModule, 49 | FlexLayoutModule, 50 | AngularFireModule, 51 | MomentModule, 52 | Ng2Webstorage, 53 | ...Directives, 54 | ], 55 | }) 56 | 57 | export class SharedModule { } 58 | -------------------------------------------------------------------------------- /src/app/todos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces'; 2 | export * from './resolves'; 3 | export * from './todos.component'; 4 | -------------------------------------------------------------------------------- /src/app/todos/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-item.interface'; 2 | -------------------------------------------------------------------------------- /src/app/todos/interfaces/todo-item.interface.ts: -------------------------------------------------------------------------------- 1 | export interface TodoItem { 2 | $key: string; 3 | todo: string; 4 | done: boolean; 5 | createdAt: number; 6 | updatedAt: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/todos/resolves/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todos.resolver'; 2 | -------------------------------------------------------------------------------- /src/app/todos/resolves/todos.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | import { AngularFire, FirebaseListObservable } from 'angularfire2'; 4 | 5 | import { TodoItem } from '../interfaces/'; 6 | 7 | @Injectable() 8 | export class TodosResolver implements Resolve { 9 | /** 10 | * Constructor of the class. 11 | * 12 | * @param {AngularFire} angularFire 13 | */ 14 | constructor(private angularFire: AngularFire) { } 15 | 16 | /** 17 | * Resolve method to get current user to-do items from Firebase. 18 | * 19 | * Note that this method relies that 'AuthenticationGuard' is run within route 'canActivate' block. 20 | * 21 | * @param {ActivatedRouteSnapshot} route 22 | * @param {RouterStateSnapshot} state 23 | * @returns {Promise>} 24 | */ 25 | resolve( 26 | route: ActivatedRouteSnapshot, 27 | state: RouterStateSnapshot 28 | ): Promise> { 29 | const list = this.angularFire.database.list('/todos/' + localStorage.getItem('uid')); 30 | 31 | return new Promise((resolve, reject) => { 32 | list.first().subscribe(() => resolve(list), reject); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/todos/todos-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { AuthenticationGuard } from './../shared'; 5 | import { TodosComponent, TodosResolver } from './index'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | RouterModule.forChild([ 10 | { 11 | path: 'todos', 12 | component: TodosComponent, 13 | canActivate: [ 14 | AuthenticationGuard, 15 | ], 16 | resolve: { 17 | todos: TodosResolver, 18 | }, 19 | }, 20 | ]), 21 | ], 22 | exports: [ 23 | RouterModule, 24 | ], 25 | }) 26 | 27 | export class TodosRoutingModule { } 28 | -------------------------------------------------------------------------------- /src/app/todos/todos.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Add new todo

3 | 4 |
5 |
6 | 7 | 12 | 13 | 14 | 19 |
20 |
21 | 22 |

Current todo items

23 | 24 | 25 | 32 |
33 | 37 | 38 |
39 |

{{item.todo}}

40 |

41 | Created: {{((item.createdAt / 1000) | amFromUnix) | amDateFormat: 'DD.MM.YY HH:mm'}} 42 | 43 | 44 | Updated: {{((item.updatedAt / 1000) | amFromUnix) | amDateFormat: 'DD.MM.YY HH:mm'}} 45 | 46 |

47 |
48 | 49 |
50 | 55 |
56 |
57 |
58 |
59 |
60 | -------------------------------------------------------------------------------- /src/app/todos/todos.component.scss: -------------------------------------------------------------------------------- 1 | h2 { 2 | margin-bottom: 0; 3 | } 4 | 5 | h3 { 6 | margin: 0; 7 | padding: 0; 8 | font-size: 16px; 9 | line-height: 14px; 10 | } 11 | 12 | p { 13 | margin: 0; 14 | } 15 | 16 | .mat-checkbox { 17 | margin-right: 8px; 18 | } 19 | 20 | .done { 21 | h3 { 22 | text-decoration: line-through; 23 | } 24 | } 25 | 26 | .content { 27 | h3, p { 28 | margin: 0; 29 | } 30 | } 31 | 32 | .mat-list-item { 33 | height: auto; 34 | padding: 8px 0; 35 | 36 | &:hover { 37 | background-color: rgba(0, 0, 0, .06); 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/app/todos/todos.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { TodosComponent } from './todos.component'; 5 | 6 | describe('Component: List', () => { 7 | /* 8 | it('should create an instance', () => { 9 | let component = new ListComponent(); 10 | expect(component).toBeTruthy(); 11 | }); 12 | */ 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/todos/todos.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { FirebaseListObservable } from 'angularfire2'; 4 | 5 | import { TodoItem } from './interfaces/'; 6 | import { Animations, AnimationsService } from '../shared/'; 7 | 8 | @Component({ 9 | selector: 'app-todos', 10 | templateUrl: './todos.component.html', 11 | styleUrls: ['./todos.component.scss'], 12 | }) 13 | 14 | export class TodosComponent extends Animations implements OnInit { 15 | public todos: FirebaseListObservable; 16 | public todo: string; 17 | 18 | /** 19 | * Constructor of the class 20 | * 21 | * @param {ActivatedRoute} activatedRoute 22 | * @param {AnimationsService} animationsService 23 | */ 24 | constructor( 25 | protected animationsService: AnimationsService, 26 | private activatedRoute: ActivatedRoute 27 | ) { 28 | super(animationsService); 29 | } 30 | 31 | public ngOnInit() { 32 | this.activatedRoute.data.subscribe(data => { 33 | this.todos = data['todos']; 34 | }); 35 | } 36 | 37 | public addNewTodo() { 38 | const item = { 39 | todo: this.todo, 40 | done: false, 41 | createdAt: firebase.database.ServerValue.TIMESTAMP, 42 | updatedAt: firebase.database.ServerValue.TIMESTAMP, 43 | }; 44 | 45 | this.todos.push(item); 46 | this.todo = ''; 47 | } 48 | 49 | public toggleStatus(item: TodoItem) { 50 | this.todos.update( 51 | item.$key, 52 | { 53 | done: item.done, 54 | updatedAt: firebase.database.ServerValue.TIMESTAMP, 55 | } 56 | ); 57 | } 58 | 59 | public remove(item: TodoItem) { 60 | this.todos.remove(item.$key); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app/todos/todos.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SharedModule } from './../shared/shared.module'; 4 | import { TodosComponent, TodosResolver } from './index'; 5 | import { TodosRoutingModule } from './todos-routing.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | TodosRoutingModule, 11 | ], 12 | declarations: [ 13 | TodosComponent, 14 | ], 15 | exports: [ 16 | TodosComponent, 17 | ], 18 | providers: [ 19 | TodosResolver, 20 | ], 21 | }) 22 | 23 | export class TodosModule { } 24 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarlepp/angular-firebase-material-demo/f5546d66ce8c174c925f1a6792b6fb024c53f5ea/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarlepp/angular-firebase-material-demo/f5546d66ce8c174c925f1a6792b6fb024c53f5ea/src/assets/.npmignore -------------------------------------------------------------------------------- /src/assets/angular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarlepp/angular-firebase-material-demo/f5546d66ce8c174c925f1a6792b6fb024c53f5ea/src/assets/angular.png -------------------------------------------------------------------------------- /src/assets/firebase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarlepp/angular-firebase-material-demo/f5546d66ce8c174c925f1a6792b6fb024c53f5ea/src/assets/firebase.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarlepp/angular-firebase-material-demo/f5546d66ce8c174c925f1a6792b6fb024c53f5ea/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular2-Firebase-Material Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 44 | 45 | 46 | 47 | 48 | 49 |
50 |

Loading...

51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | import { enableProdMode } from '@angular/core'; 5 | import { environment } from './environments/environment'; 6 | import { AppModule } from './app/'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | // This file includes polyfills needed by Angular and is loaded before the app. 2 | // You can add your own extra polyfills to this file. 3 | import 'core-js/es6/symbol'; 4 | import 'core-js/es6/object'; 5 | import 'core-js/es6/function'; 6 | import 'core-js/es6/parse-int'; 7 | import 'core-js/es6/parse-float'; 8 | import 'core-js/es6/number'; 9 | import 'core-js/es6/math'; 10 | import 'core-js/es6/string'; 11 | import 'core-js/es6/date'; 12 | import 'core-js/es6/array'; 13 | import 'core-js/es6/regexp'; 14 | import 'core-js/es6/map'; 15 | import 'core-js/es6/set'; 16 | import 'core-js/es6/reflect'; 17 | 18 | import 'core-js/es7/reflect'; 19 | import 'zone.js/dist/zone'; 20 | 21 | // If you need to support the browsers/features below, uncomment the import 22 | // and run `npm install import-name-here'; 23 | // Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 24 | 25 | // Needed for: IE9 26 | // import 'classlist.js'; 27 | 28 | // Animations 29 | // Needed for: All but Chrome and Firefox, Not supported in IE9 30 | // import 'web-animations-js'; 31 | 32 | // Date, currency, decimal and percent pipes 33 | // Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 34 | // import 'intl'; 35 | 36 | // NgClass on SVG elements 37 | // Needed for: IE10, IE11 38 | // import 'classlist.js'; 39 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '~@angular/material/core/theming/all-theme'; 3 | 4 | // Include the base styles for Angular Material core. We include this here so that you only 5 | // have to load a single css file for Angular Material in your app. 6 | @include mat-core(); 7 | 8 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 9 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 10 | // hue. 11 | $frontend-app-primary: mat-palette($mat-blue-grey); 12 | $frontend-app-accent: mat-palette($mat-blue, A200, A100, A400); 13 | 14 | // The warn palette is optional (defaults to red). 15 | $frontend-app-warn: mat-palette($mat-red); 16 | 17 | // Create the theme object (a Sass map containing all of the palettes). 18 | $frontend-app-theme: mat-light-theme($frontend-app-primary, $frontend-app-accent, $frontend-app-warn); 19 | 20 | // Include theme styles for core and each component used in your app. 21 | // Alternatively, you can import and @include the theme mixins for each component 22 | // that you are using. 23 | @include angular-material-theme($frontend-app-theme); 24 | 25 | html, body { 26 | margin: 0; 27 | height: 100%; 28 | min-height: 100%; 29 | overflow: hidden; 30 | } 31 | 32 | body { 33 | margin: 0; 34 | display: flex; 35 | flex-direction: column; 36 | font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif; 37 | } 38 | 39 | app-root { 40 | display: flex; 41 | flex-direction: column; 42 | flex: 1; 43 | } 44 | 45 | .normal-content { 46 | padding: 0 16px; 47 | } 48 | 49 | .fill { 50 | display: flex; 51 | flex: 1 1 auto; 52 | } 53 | 54 | .animation-content-out { 55 | opacity: 0; 56 | transition: 0ms ease-out; 57 | } 58 | 59 | .animation-content-in { 60 | opacity: 1; 61 | transition: 500ms ease-in; 62 | } 63 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare var __karma__: any; 17 | declare var require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "lib": [ 7 | "es6", 8 | "dom" 9 | ], 10 | "mapRoot": "./", 11 | "module": "es6", 12 | "moduleResolution": "node", 13 | "outDir": "../dist/out-tsc", 14 | "sourceMap": true, 15 | "target": "es5", 16 | "types": [ 17 | "hammerjs", 18 | "jasmine" 19 | ], 20 | "typeRoots": [ 21 | "../node_modules/@types" 22 | ], 23 | "files": [ 24 | "../node_modules/firebase/firebase.d.ts" 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | // Typings reference file, see links for more information 2 | // https://github.com/typings/typings 3 | // https://www.typescriptlang.org/docs/handbook/writing-declaration-files.html 4 | 5 | declare const System: any; 6 | declare const require: any; 7 | declare const module: any; 8 | declare namespace firebase.database.ServerValue { 9 | const TIMESTAMP: any; 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true, "rxjs"], 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | false, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-construct": true, 44 | "no-debugger": true, 45 | "no-duplicate-variable": true, 46 | "no-empty": false, 47 | "no-empty-interface": true, 48 | "no-eval": true, 49 | "no-inferrable-types": true, 50 | "no-shadowed-variable": true, 51 | "no-string-literal": false, 52 | "no-string-throw": true, 53 | "no-switch-case-fall-through": true, 54 | "no-trailing-whitespace": true, 55 | "no-unused-expression": true, 56 | "no-use-before-declare": true, 57 | "no-var-keyword": true, 58 | "object-literal-sort-keys": false, 59 | "one-line": [ 60 | true, 61 | "check-open-brace", 62 | "check-catch", 63 | "check-else", 64 | "check-whitespace" 65 | ], 66 | "prefer-const": true, 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "radix": true, 72 | "semicolon": [ 73 | "always" 74 | ], 75 | "triple-equals": [ 76 | true, 77 | "allow-null-check" 78 | ], 79 | "typedef-whitespace": [ 80 | true, 81 | { 82 | "call-signature": "nospace", 83 | "index-signature": "nospace", 84 | "parameter": "nospace", 85 | "property-declaration": "nospace", 86 | "variable-declaration": "nospace" 87 | } 88 | ], 89 | "typeof-compare": true, 90 | "unified-signatures": true, 91 | "variable-name": false, 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-decl", 96 | "check-operator", 97 | "check-separator", 98 | "check-type" 99 | ], 100 | 101 | "directive-selector": [true, "attribute", "app", "camelCase"], 102 | "component-selector": [true, "element", "app", "kebab-case"], 103 | "use-input-property-decorator": true, 104 | "use-output-property-decorator": true, 105 | "use-host-property-decorator": true, 106 | "no-input-rename": true, 107 | "no-output-rename": true, 108 | "use-life-cycle-interface": true, 109 | "use-pipe-transform-interface": true, 110 | "component-class-suffix": true, 111 | "directive-class-suffix": true, 112 | "no-access-missing-member": true, 113 | "templates-use-public": true, 114 | "invoke-injectable": true 115 | } 116 | } 117 | --------------------------------------------------------------------------------