├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── batch-server.ts ├── db-data.ts ├── e2e ├── app.e2e-spec.ts ├── app.po.ts ├── tsconfig.e2e.json └── tsconfig.json ├── package.json ├── populate-db.ts ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── course-detail │ │ ├── course-detail.component.css │ │ ├── course-detail.component.html │ │ ├── course-detail.component.spec.ts │ │ └── course-detail.component.ts │ ├── courses │ │ ├── courses.component.css │ │ ├── courses.component.html │ │ ├── courses.component.spec.ts │ │ └── courses.component.ts │ ├── edit-lesson │ │ ├── edit-lesson.component.css │ │ ├── edit-lesson.component.html │ │ ├── edit-lesson.component.spec.ts │ │ └── edit-lesson.component.ts │ ├── home │ │ ├── home.component.css │ │ ├── home.component.html │ │ ├── home.component.spec.ts │ │ └── home.component.ts │ ├── index.ts │ ├── lesson-detail │ │ ├── lesson-detail.component.css │ │ ├── lesson-detail.component.html │ │ ├── lesson-detail.component.spec.ts │ │ └── lesson-detail.component.ts │ ├── lesson-form │ │ ├── lesson-form.component.css │ │ ├── lesson-form.component.html │ │ ├── lesson-form.component.spec.ts │ │ └── lesson-form.component.ts │ ├── lessons-list │ │ ├── lessons-list.component.css │ │ ├── lessons-list.component.html │ │ ├── lessons-list.component.spec.ts │ │ └── lessons-list.component.ts │ ├── login │ │ ├── login.component.css │ │ ├── login.component.html │ │ ├── login.component.spec.ts │ │ └── login.component.ts │ ├── new-lesson │ │ ├── new-lesson.component.css │ │ ├── new-lesson.component.html │ │ └── new-lesson.component.ts │ ├── register │ │ ├── register.component.css │ │ ├── register.component.html │ │ ├── register.component.spec.ts │ │ └── register.component.ts │ ├── router.config.ts │ ├── shared │ │ ├── index.ts │ │ ├── model │ │ │ ├── course.ts │ │ │ ├── courses.service.spec.ts │ │ │ ├── courses.service.ts │ │ │ ├── lesson.resolver.ts │ │ │ ├── lesson.ts │ │ │ ├── lessons.service.spec.ts │ │ │ └── lessons.service.ts │ │ ├── security │ │ │ ├── auth-info.ts │ │ │ ├── auth.guard.ts │ │ │ ├── auth.service.spec.ts │ │ │ ├── auth.service.ts │ │ │ ├── safe-url.pipe.spec.ts │ │ │ └── safe-url.pipe.ts │ │ └── validators │ │ │ └── validateUrl.ts │ └── top-menu │ │ ├── top-menu.component.css │ │ ├── top-menu.component.html │ │ ├── top-menu.component.spec.ts │ │ └── top-menu.component.ts ├── assets │ ├── .gitkeep │ ├── .npmignore │ └── app.css ├── environments │ ├── environment.prod.ts │ ├── environment.ts │ └── firebase.config.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json ├── 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 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | typings 3 | tsd_typings/ 4 | npm-debug.log 5 | dist/ 6 | .idea 7 | .DS_Store 8 | tmp 9 | *.js 10 | *.map -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Angular University 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Important 2 | 3 | This course is now archived and has been replaced with the [Serverless Angular with Firebase & AngularFire Course](https://github.com/angular-university/firebase-course). 4 | 5 | 6 | 7 | # Repository contents 8 | 9 | This repository contains the full application of the course Angular and Firebase - Build a Web Application, so this contains the complete application like it will look like at the end of the course. 10 | 11 | This course repository is updated to Angular 6, there is a Yarn lock file available. 12 | 13 | [Angular and Firebase - Build a Web Application](https://angular-university.io/course/build-an-application-with-angular2) 14 | 15 | ![Angular firebase course](https://angular-academy.s3.amazonaws.com/thumbnails/angular_app-firebase-small.jpg) 16 | 17 | # Looking for the course starting repo? 18 | 19 | If you are looking for a clean starting point for coding along as you follow the course, please check this [repository](https://github.com/angular-university/angular-firebase-app-starter) instead. 20 | 21 | 22 | # Which Course are You Looking For? 23 | 24 | Below is a list of the Angular University courses: 25 | 26 | # Angular University Lessons Code 27 | Contains the code for all the Angular University courses. 28 | 29 | ![Angular for Beginners course](https://angular-academy.s3.amazonaws.com/thumbnails/angular2-for-beginners-small.png) 30 | 31 | If you are looking for the Complete Angular With Typescript Course, the repo can be found here: 32 | 33 | # Complete Typescript 2 Course - Build A REST API 34 | 35 | If you are looking for the Complete Typescript 2 Course - Build a REST API, the repo with the full code can be found here: 36 | 37 | [Complete Typescript 2 Course - Build A REST API](https://github.com/angular-university/complete-typescript-course) 38 | 39 | ![Complete Typescript Course](https://angular-academy.s3.amazonaws.com/thumbnails/typescript-2-small.png) 40 | 41 | 42 | # Angular Ngrx Reactive Extensions Architecture Course 43 | 44 | If you are looking for the Angular Ngrx Reactive Extensions Architecture Course code, the repo with the full code can be found here: 45 | 46 | [Angular Ngrx Reactive Extensions Architecture Course](https://github.com/angular-university/ngrx-course) 47 | 48 | ![Angular Ngrx Course](https://angular-academy.s3.amazonaws.com/thumbnails/ngrx-angular.png) 49 | 50 | 51 | # RxJs and Reactive Patterns Angular Architecture Course 52 | 53 | If you are looking for the RxJs and Reactive Patterns Angular Architecture Course code, the repo with the full code can be found here: 54 | 55 | [RxJs and Reactive Patterns Angular Architecture Course](https://angular-university.io/course/reactive-angular-architecture-course) 56 | 57 | ![RxJs and Reactive Patterns Angular Architecture Course](https://s3-us-west-1.amazonaws.com/angular-academy/blog/images/rxjs-reactive-patterns-small.png) 58 | 59 | 60 | # Installation pre-requisites 61 | 62 | For running this project we need and npm installed on our machine. These are some tutorials to install node in different operating systems: 63 | 64 | *Its important to install the latest version of Node* 65 | 66 | - [Install Node and NPM on Windows](https://www.youtube.com/watch?v=8ODS6RM6x7g) 67 | - [Install Node and NPM on Linux](https://www.youtube.com/watch?v=yUdHk-Dk_BY) 68 | - [Install Node and NPM on Mac](https://www.youtube.com/watch?v=Imj8PgG3bZU) 69 | 70 | 71 | # Installing the Angular CLI 72 | 73 | With the following command the angular-cli will be installed globally in your machine: 74 | 75 | npm install -g @angular/cli 76 | 77 | # Installing the code 78 | 79 | The code can be installed with the following command (needs to be run in the folder where package.json is): 80 | 81 | npm install 82 | 83 | If you prefer the Yarn package manager, instead of npm install you can also run: 84 | 85 | yarn 86 | 87 | Although npm install would also work, its recommended to use Yarn to install the course dependencies. Yarn has the big advantage that if you use it you will be 88 | installing the exact same dependencies than I installed in my machine, so you wont run into issues caused by semantic versioning updates. 89 | 90 | This should take a couple of minutes. If there are issues, please post the complete error message in the Questions section of the course. 91 | 92 | 93 | ## Running the code 94 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 95 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ng4-cli": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets", 21 | "src/favicon.ico" 22 | ], 23 | "styles": [ 24 | "src/styles.css" 25 | ], 26 | "scripts": [] 27 | }, 28 | "configurations": { 29 | "production": { 30 | "optimization": true, 31 | "outputHashing": "all", 32 | "sourceMap": false, 33 | "extractCss": true, 34 | "namedChunks": false, 35 | "aot": true, 36 | "extractLicenses": true, 37 | "vendorChunk": false, 38 | "buildOptimizer": true, 39 | "fileReplacements": [ 40 | { 41 | "replace": "src/environments/environment.ts", 42 | "with": "src/environments/environment.prod.ts" 43 | } 44 | ] 45 | } 46 | } 47 | }, 48 | "serve": { 49 | "builder": "@angular-devkit/build-angular:dev-server", 50 | "options": { 51 | "browserTarget": "ng4-cli:build" 52 | }, 53 | "configurations": { 54 | "production": { 55 | "browserTarget": "ng4-cli:build:production" 56 | } 57 | } 58 | }, 59 | "extract-i18n": { 60 | "builder": "@angular-devkit/build-angular:extract-i18n", 61 | "options": { 62 | "browserTarget": "ng4-cli:build" 63 | } 64 | }, 65 | "test": { 66 | "builder": "@angular-devkit/build-angular:karma", 67 | "options": { 68 | "main": "src/test.ts", 69 | "karmaConfig": "./karma.conf.js", 70 | "polyfills": "src/polyfills.ts", 71 | "tsConfig": "src/tsconfig.spec.json", 72 | "scripts": [], 73 | "styles": [ 74 | "src/styles.css" 75 | ], 76 | "assets": [ 77 | "src/assets", 78 | "src/favicon.ico" 79 | ] 80 | } 81 | }, 82 | "lint": { 83 | "builder": "@angular-devkit/build-angular:tslint", 84 | "options": { 85 | "tsConfig": [ 86 | "src/tsconfig.app.json", 87 | "src/tsconfig.spec.json" 88 | ], 89 | "exclude": [] 90 | } 91 | } 92 | } 93 | }, 94 | "ng4-cli-e2e": { 95 | "root": "", 96 | "sourceRoot": "", 97 | "projectType": "application", 98 | "architect": { 99 | "e2e": { 100 | "builder": "@angular-devkit/build-angular:protractor", 101 | "options": { 102 | "protractorConfig": "./protractor.conf.js", 103 | "devServerTarget": "ng4-cli:serve" 104 | } 105 | }, 106 | "lint": { 107 | "builder": "@angular-devkit/build-angular:tslint", 108 | "options": { 109 | "tsConfig": [ 110 | "e2e/tsconfig.e2e.json" 111 | ], 112 | "exclude": [] 113 | } 114 | } 115 | } 116 | } 117 | }, 118 | "defaultProject": "ng4-cli", 119 | "schematics": { 120 | "@schematics/angular:component": { 121 | "prefix": "app", 122 | "styleext": "css" 123 | }, 124 | "@schematics/angular:directive": { 125 | "prefix": "app" 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /batch-server.ts: -------------------------------------------------------------------------------- 1 | 2 | import {firebaseConfig} from "./src/environments/firebase.config"; 3 | import {initializeApp, auth,database} from 'firebase'; 4 | var Queue = require('firebase-queue'); 5 | 6 | 7 | console.log('Running batch server ...'); 8 | 9 | initializeApp(firebaseConfig); 10 | 11 | auth() 12 | .signInWithEmailAndPassword('admin@angular-university.io', 'test123') 13 | .then(runConsumer) 14 | .catch(onError); 15 | 16 | function onError(err) { 17 | console.error("Could not login", err); 18 | process.exit(); 19 | } 20 | 21 | 22 | function runConsumer() { 23 | 24 | console.log("Running consumer ..."); 25 | 26 | const lessonsRef = database().ref("lessons"); 27 | const lessonsPerCourseRef = database().ref("lessonsPerCourse"); 28 | 29 | const queueRef = database().ref('queue'); 30 | 31 | 32 | const queue = new Queue(queueRef, function(data, progress, resolve, reject) { 33 | 34 | console.log('received delete request ...',data); 35 | 36 | const deleteLessonPromise = lessonsRef.child(data.lessonId).remove(); 37 | 38 | const deleteLessonPerCoursePromise = 39 | lessonsPerCourseRef.child(`${data.courseId}/${data.lessonId}`).remove(); 40 | 41 | Promise.all([deleteLessonPromise, deleteLessonPerCoursePromise]) 42 | .then( 43 | () => { 44 | console.log("lesson deleted"); 45 | resolve(); 46 | } 47 | ) 48 | .catch(() => { 49 | console.log("lesson deletion in error"); 50 | reject(); 51 | }); 52 | 53 | 54 | }); 55 | 56 | 57 | } 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /db-data.ts: -------------------------------------------------------------------------------- 1 | export const dbData = { 2 | "courses": [ 3 | { 4 | "url": "getting-started-with-angular2", 5 | "description": "Angular Tutorial For Beginners", 6 | "iconUrl": "https://angular-academy.s3.amazonaws.com/thumbnails/angular2-for-beginners.jpg", 7 | "courseListIcon": "https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png", 8 | "longDescription": "Establish a solid layer of fundamentals, learn what's under the hood of Angular", 9 | "lessons": [ 10 | { 11 | "url": "angular2-hello-world-write-first-application", 12 | "description": "Angular Tutorial For Beginners - Build Your First App - Hello World Step By Step", 13 | "duration": "2:49", 14 | "tags": "BEGINNER", 15 | videoUrl: "https://www.youtube.com/embed/du6sKwEFrhQ", 16 | "longDescription": "This is step by step guide to create your first Angular application. Its aimed at beginners just starting out with the framework.This lesson will show how to create a component, and how to link the component to a given custom HTML tag. It will show how to give the component a given template." 17 | }, 18 | { 19 | "url": "angular2-build-your-first-component", 20 | "description": "Building Your First Angular Component - Component Composition", 21 | "duration": "2:07", 22 | "tags": "BEGINNER", 23 | videoUrl: "https://www.youtube.com/embed/VES1eTNxi1s", 24 | "longDescription": "In this lesson we are going to see how to include a component inside another component. We are going to create a simple search box component and include it in our main application." 25 | }, 26 | { 27 | "url": "angular2-passing-data-to-component-using-input", 28 | "description": "Component @Input - How To Pass Input Data To an Angular Component", 29 | "duration": "2:33", 30 | "tags": "BEGINNER", 31 | "videoUrl": "https://www.youtube.com/embed/Yfebo2mFrTU", 32 | "longDescription": "In this lesson we are going to learn how to use the Angular template syntax for properties, and learn how we can use it to pass input data to a component. We are going to see also a simplified input syntax for passing constant strings as component inputs." 33 | }, 34 | { 35 | "url": "angular2-component-events", 36 | "description": "Angular Component Events - Using @Output to create custom events", 37 | "duration": "4:44", 38 | "tags": "BEGINNER", 39 | videoUrl: "https://www.youtube.com/embed/dgyVrJ2XCq4", 40 | "longDescription": "In this lesson we are going to see how components can emit custom events via EventEmitter and the @Output decorator. We are going to see how we can subscribe to standard browser events, and how the syntax for that is exactly the same as in the case of custom component events. We will also learn how Typescript literals can be used to output variables inside template strings." 41 | }, 42 | { 43 | "url": "angular2-component-templates-internal-vs-external", 44 | "description": "Angular Component Templates - Inline Vs External", 45 | "duration": "2:55", 46 | "tags": "BEGINNER", 47 | "pro": true, 48 | "longDescription": "In this lesson we are going to learn how a component template can be defined both inline and in an external file. We are going to learn how to configure the component so that Angular can find the template at the correct location, using the module commonjs variable. We are going to learn also some best practices for component naming, from the official Angular Style Guide." 49 | }, 50 | { 51 | "url": "angular2-components-styling-component-isolation", 52 | "description": "Styling Angular Components - Learn About Component Style Isolation", 53 | "duration": "3:27", 54 | "tags": "BEGINNER", 55 | "pro": true, 56 | "longDescription": "In this lesson we are going to learn how components can be styled using both inline styles and an external css file. We will learn some more best practices on file naming. We will learn how the mechanism for style isolation works in Angular." 57 | }, 58 | { 59 | "url": "angular2-components-component-interaction", 60 | "description": "Angular Component Interaction - Extended Components Example", 61 | "duration": "9:22", 62 | "pro": true, 63 | "tags": "BEGINNER", 64 | "longDescription": "In this lesson we are going to put together all that we have learned so far about components to create a more complex example. We are going to create two components: a color picker and a color previewer and see how they can interact." 65 | }, 66 | { 67 | "url": "angular2-components-exercise", 68 | "description": "Angular Components Tutorial For Beginners - Components Exercise !", 69 | "duration": "1:26", 70 | "tags": "BEGINNER", 71 | "pro": true, 72 | "longDescription": "In this video we are going to present an exercise for putting in practice the several concepts that we have learned so far about components." 73 | }, 74 | { 75 | "url": "angular2-components-exercise-solution", 76 | "description": "Angular Components Tutorial For Beginners - Components Exercise Solution Inside", 77 | "duration": "2:08", 78 | "tags": "BEGINNER", 79 | "pro": true, 80 | "longDescription": "This video contains the solution for the introduction to components exercise." 81 | }, 82 | { 83 | "url": "angular2-directives-inputs-outputs-event-emitters", 84 | "description": "Angular Directives - Inputs, Output Event Emitters and How To Export Template References", 85 | "duration": "4:01", 86 | "tags": "BEGINNER", 87 | "pro": true, 88 | "longDescription": "Angular Components are actually simply just Directives. All the functionality that we have learned so far about Components also applies to Directives. In this lesson we are going to learn how to Directives can also have inputs and outputs, and how the use of the decorators @Input and @Output also applies to directives. We are also learn a new functionality for exporting a template reference for the directive itself into the template on which the directive is being used. " 89 | }, 90 | { 91 | "description": "Angular Core Directives - ngFor", 92 | "duration": "3:46", 93 | "url": "angular2-core-directives-ngfor", 94 | "tags": "BEGINNER", 95 | "pro": true, 96 | "longDescription": "This is an overview on the ngFor core directive, how it works and some common use cases on how it should be used to build templates. It demonstrates how ngFor can be used with any iterable and not only arrays, and how to use together with other mechanisms of the framework like @ContentChildren." 97 | }, 98 | { 99 | "description": "Angular Core Directives - ngClass and ngStyle", 100 | "duration": "3:15", 101 | "url": "angular2-core-directives-ngclass-ngstyle", 102 | "tags": "BEGINNER", 103 | "pro": true, 104 | "longDescription": "This lesson is an overview on how to use the ngClass and ngStyle Directives, when to use which, and alternative syntax in case we only to modify one property/style." 105 | }, 106 | { 107 | "description": "Angular Core Directives - ngIf", 108 | "duration": "3:56", 109 | "url": "angular2-core-directives-ngIf", 110 | "tags": "BEGINNER", 111 | "pro": true, 112 | "longDescription": "This lesson covers the use of the core directive ngIf, as well as two other alternative way of showing or hiding elements from the DOM: the hidden property and the visibility CSS property." 113 | }, 114 | { 115 | "description": "Directives Guided Tour - Learn Why Directives Might be a Better Choice Than Components", 116 | "url": "angular2-guided-tour-directives", 117 | "duration": "7:58", 118 | "tags": "BEGINNER", 119 | "pro": true, 120 | "longDescription": "This lesson is an extended tour on Angular directives. This is an extremely powerful feature of Angular that often remains underused. Its super powerful and and if used correctly can be used to create functionality that is much more reusable than components themselves." 121 | }, 122 | { 123 | "description": "Introduction to Angular Directives - Exercise - Improve the Collapsible Directive", 124 | "duration": "1:30", 125 | "url": "angular2-directives-exercise-improve-collapsible-directive", 126 | "tags": "BEGINNER", 127 | "pro": true, 128 | "longDescription": "In this video we are going to present the exercise for the Introduction To Directives section. The goal of the exercise is to take the learned functionality about how to build a custom directive and how to use the standard Angular Core directives to build an improved version of the collapse-on-click directive." 129 | }, 130 | { 131 | "description": "Introduction to Angular Directives - Exercise Solution", 132 | "duration": "2:40", 133 | "url": "angular2-directives-exercise-solution-improve-collapsible-directive", 134 | "tags": "BEGINNER", 135 | "pro": true, 136 | "longDescription": "In this video we are going to present the exercise solution for the Introduction To Directives section." 137 | } 138 | ] 139 | }, 140 | { 141 | "url": "angular2-http", 142 | "description": "Angular HTTP and Services", 143 | "longDescription": "

Build Services using Observables, learn to use the HTTP module effectively.", 144 | "iconUrl": "https://angular-academy.s3.amazonaws.com/thumbnails/services-and-http.jpg", 145 | "courseListIcon": "https://angular-academy.s3.amazonaws.com/course-logos/observables_rxjs.png", 146 | "lessons": [ 147 | { 148 | "description": "What is an Observable ? Introduction to Streams and RxJs Observables", 149 | "duration": "5:41", 150 | "url": "angular2-what-is-an-observable", 151 | "tags": "BEGINNER", 152 | "videoUrl": "https://www.youtube.com/embed/Sol2uLolmUM", 153 | "longDescription": "In this lesson we are going to present a couple of baseline concepts that are essential for being able to build service layers in Angular: we will introduce the notions of stream and Observable. We are going to understand that these are two different concepts: an Observable is not a stream. During the lesson we will write our first Observable and we will learn one of the main properties of Observables: that they are inherently inert, and that we need to subscribe to them in order for them to work. We are also going to introduce our first RxJs operator: the do operator which should only be used for debugging purposes as it introduces side effects." 154 | }, 155 | { 156 | "description": "Observables Error Handling and Completion - How do Observables handle Errors?", 157 | "duration": "5:28", 158 | "url": "angular2-observables-error-handling-and-completion-network-calls-as-observables", 159 | "tags": "BEGINNER", 160 | "videoUrl": "https://www.youtube.com/embed/ot_FrQbmEmU", 161 | "longDescription": "In this lesson we are going to present two other foundation concepts of Observables: error handling and completion. We are going to initially call our backend server using the browser Fetch API, which is promises based. We will then learn how to create an Observable from a promise, and see how and why an observable is a good way to model a network call. We will learn about some advantages of Observables vs Promises." 162 | }, 163 | { 164 | "description": "How does Angular HTTP use Observables ? The HTTP Response object", 165 | "duration": "4:32", 166 | "url": "how-does-angular2-use-observables-http-response-object", 167 | "tags": "BEGINNER", 168 | "longDescription": "In this lesson we are going to learn the relation between the Angular HTTP module and Observables, and how its essential to understand Observables in order to do even the most common backend-communication tasks using the HTTP module. We are going to learn how Angular HTTP models network responses using Observables, and how completion is handled. " 169 | }, 170 | { 171 | "description": "How to use Observables and HTTP to build a Service Layer", 172 | "duration": "4:32", 173 | "url": "angular2-how-to-use-observables-and-http-to-build-a-servicelayer", 174 | "tags": "BEGINNER", 175 | "videoUrl": "", 176 | "longDescription": "In this lesson we are going to learn how to use Angular HTTP to build the service layer of our application. We are going to learn how to build strongly typed services and we are going to learn how the service layer can be designed around the notion of Observables." 177 | }, 178 | { 179 | "description": "Introduction to Functional Reactive Programming - Using the Async Pipe - Pitfalls to Avoid", 180 | "duration": "4:36", 181 | "url": "angular2-how-to-use-the-async-pipe-to-pass-observables-into-a-template", 182 | "tags": "BEGINNER", 183 | "pro": true, 184 | "longDescription": "In this lesson we are going to do an introduction to Functional Reactive Programming, and we are going to see how an application can be built around the notion of Observables. We are going to see how programs can be build with very little state variables, and how data can be passed on from Observables directly to templates by using the Async Pipe. We are going to learn also why in some cases its not a good idea to call the service layer directly from a template expression - this is a pitfall to avoid." 185 | }, 186 | { 187 | "description": "The RxJs Map Operator - How to create an Observable from another Observable", 188 | "duration": "3:04", 189 | "url": "angular2-observable-map-operator-how-to-create-an-observable-from-another", 190 | "tags": "BEGINNER", 191 | "pro": true, 192 | "longDescription": "In this lesson we are going to learn one of the key concepts about Observables: we can easily derive new Observables from existing Observables using the many RxJs operators available to us. In this lesson we are going to create an Observable from another Observable by using the RxJs map operator." 193 | }, 194 | { 195 | "description": "Observable Composition - combine multiple Observables Using combineLatest", 196 | "duration": "5:59", 197 | "url": "angular2-observable-composition-combine-latests", 198 | "tags": "BEGINNER", 199 | "pro": true, 200 | "longDescription": "In this lesson we are going to learn that Observables can be combined with other Observables. In this case we are going to create an Observable of mouse moves that only emits if the mouse is bellow a certain region of the page. We are also going to create an Observable of mouse clicks, that emits if the user clicks anywhere on the page - both of these Observables will be created using fromEvent. We will then combine these two Observables to create third Observable using the RxJs combineLatests operator. " 201 | }, 202 | { 203 | "description": "Avoid the Biggest Pitfall of Angular HTTP - Learn the RxJs Cache Operator", 204 | "duration": "5:10", 205 | "url": "angular2-how-to--aAvoid-duplicate-http-requests-rxjs-cache-operator", 206 | "tags": "INTERMEDIATE", 207 | "pro": true, 208 | "longDescription": "In this lesson we are going to use the HTTP module to implement a modification operation: we are going to add a lesson to a lessons list via an HTTP POST call, and then reload the data from the server. While implementing this simple use case, we are going to come across something that might be surprising at first: its really simple to do duplicate network calls accidentally while using Angular HTTP. We are going to learn the reason why that is the case, and learn how we can avoid that using the RxJs Cache Operator." 209 | }, 210 | { 211 | "description": "How to do multiple HTTP requests using the RxJs Concat Operator", 212 | "duration": "4:19", 213 | "url": "angular2-how-to-do-multiple-http-requests-using-the-rxjs-concat-operator", 214 | "tags": "INTERMEDIATE", 215 | "pro": true, 216 | "longDescription": "In this lesson we are going to learn how we make multiple sequential requests to the server by using the RxJs Concat operator. This is another example of how from the point of view of the Angular HTTP module network requests are really just Observables that can be easily combined using the many RxJs operators available. We are going to implement the following concrete example: do a delete on the server, then a second delete and finally reload the new list from the server and display it on the screen." 217 | }, 218 | { 219 | "description": "How to do two HTTP Requests in Parallel using the RxJs combineLatest Operator", 220 | "duration": "3:58", 221 | "url": "angular2-how-to-do-two-http-requests-in-parallel-using-the-rxjs-combinelatest-operator", 222 | "tags": "INTERMEDIATE", 223 | "pro": true, 224 | "longDescription": "In this lesson we are going to learn how to do two HTTP requests in parallel, wait for each to return and produce a result that contains the combination of the two HTTP calls. For that we are going to use an operator that we presented before, the combineLatest Operator which will in this time be used in a completely different context. This is a good example of the power of the approach that the Angular HTTP module gives us, by modeling network calls as Observables; any RxJs operator can potentially by used to process network calls." 225 | }, 226 | { 227 | "description": "How to setup an HTTP request sequence using the RxJs switchMap Operator", 228 | "duration": "4:33", 229 | "url": "angular2-how-to-setup-an-http-request-sequence-using-the-rxjs-switchmap-operator", 230 | "tags": "INTERMEDIATE", 231 | "pro": true, 232 | "longDescription": "In this lesson we are going to learn how we can build a chain of HTTP requests, but now we will be able to take the result of one request and then use it to build the next request. For this we are going to introduce a new RxJs Operator for combining Observables, the switchMap Operator. This lesson will give us a first contact with the more general Switch strategy of combining Observables." 233 | }, 234 | { 235 | "description": "Retry HTTP requests in Error using the retry and retryWhen RxJs Operators", 236 | "duration": "3:42", 237 | "url": "angular2-retry-http-requests-in-error-using-the-retry-and-retrywhen-rxjs-operators", 238 | "tags": "INTERMEDIATE", 239 | "pro": true, 240 | "longDescription": "In this lesson we are going to learn how RxJs and Observables make it very simple to deal with certain uses cases that before might be challenging. For example, we are going to learn how to retry a network call in case of error. This is very useful in situations when the backend occasionally returns errors that are of an intermittent nature. In those scenarios a good strategy is to try to send the network call again a second time, usually after a certain delay has elapsed. In this lesson we are going to learn how we can use the RxJs Operators retry and retryWhen to implement service layers that are resilient to temporary errors." 241 | }, 242 | { 243 | "description": "How to Cancel an HTTP Request using an RxJs Subscription", 244 | "duration": "2:56", 245 | "url": "angular2-how-to-cancel-an-http-request-using-an-rxjs-subscription", 246 | "tags": "INTERMEDIATE", 247 | "pro": true, 248 | "longDescription": "In this lesson we are going to learn how to implement a use case using RxJs and Observables that was very hard to do previously: the cancellation of an ongoing HTTP request. We are going to learn about the RxJs subscription object and how to use it to implement cancellation." 249 | }, 250 | { 251 | "description": "Exercise - Improve a Search Service and Build a Typeahead", 252 | "duration": "3:15", 253 | "url": "angular2-exercise-improve-a-search-service-and-build-a-typeahead", 254 | "tags": "INTERMEDIATE", 255 | "pro": true, 256 | "longDescription": "This lesson is the setup for the exercise of the Services and HTTP series. We are going to implement a Typeahead that continuously retrieves from the backend new search results depending on what the use is typing. We are going to show how to use the Angular HTTP API to pass a GET parameter request to the backend." 257 | }, 258 | { 259 | "description": "Exercise Solution - Learn How to build a Typeahead that cancels obsolete search requests", 260 | "duration": "5:07", 261 | "url": "angular2-exercise-solution-learn-how-to-build-a-typeahead-that-cancels-obsolete-search-requests", 262 | "tags": "INTERMEDIATE", 263 | "pro": true, 264 | "longDescription": "This is the solution for the HTTP and Services exercise, where we will build a Typeahead. For that we are going to use a couple of RxJs Operators that we have previously presented in this course. We are going to see how results from a previous search can be implicitly canceled." 265 | } 266 | ] 267 | } 268 | ] 269 | }; 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Ng4CliPage } from './app.po'; 2 | 3 | describe('ng4-cli App', () => { 4 | let page: Ng4CliPage; 5 | 6 | beforeEach(() => { 7 | page = new Ng4CliPage(); 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 Ng4CliPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types":[ 8 | "jasmine", 9 | "node" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "lib": [ 8 | "es2016" 9 | ], 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "outDir": "../dist/out-tsc-e2e", 13 | "sourceMap": true, 14 | "target": "es6", 15 | "typeRoots": [ 16 | "../node_modules/@types" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-firebase-app", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e", 12 | "populate-db": "./node_modules/.bin/ts-node ./populate-db.ts", 13 | "batch-server": "./node_modules/.bin/ts-node ./batch-server.ts" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular/animations": "6.0.0", 18 | "@angular/common": "6.0.0", 19 | "@angular/compiler": "6.0.0", 20 | "@angular/core": "6.0.0", 21 | "@angular/forms": "6.0.0", 22 | "@angular/http": "6.0.0", 23 | "@angular/platform-browser": "6.0.0", 24 | "@angular/platform-browser-dynamic": "6.0.0", 25 | "@angular/router": "6.0.0", 26 | "@types/lodash": "^4.14.36", 27 | "@types/request": "0.0.30", 28 | "angularfire2": "^4.0.0-rc.0", 29 | "core-js": "^2.4.1", 30 | "firebase": "^3.7.4", 31 | "firebase-queue": "^1.5.0", 32 | "lodash": "^4.15.0", 33 | "rxjs": "6.1.0", 34 | "rxjs-compat": "^6.0.0-rc.0", 35 | "zone.js": "0.8.26" 36 | }, 37 | "devDependencies": { 38 | "@angular-devkit/build-angular": "~0.6.0", 39 | "@angular/cli": "^6.0.0", 40 | "@angular/compiler-cli": "6.0.0", 41 | "@angular/language-service": "6.0.0", 42 | "@types/jasmine": "~2.5.53", 43 | "@types/jasminewd2": "~2.0.2", 44 | "@types/node": "~6.0.60", 45 | "codelyzer": "~3.2.0", 46 | "jasmine-core": "~2.6.2", 47 | "jasmine-spec-reporter": "~4.1.0", 48 | "karma": "~1.7.0", 49 | "karma-chrome-launcher": "~2.1.1", 50 | "karma-cli": "~1.0.1", 51 | "karma-coverage-istanbul-reporter": "^1.2.1", 52 | "karma-jasmine": "~1.1.0", 53 | "karma-jasmine-html-reporter": "^0.2.2", 54 | "protractor": "~5.1.2", 55 | "ts-node": "~3.2.0", 56 | "tslint": "~5.7.0", 57 | "typescript": "2.7.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /populate-db.ts: -------------------------------------------------------------------------------- 1 | import {database, initializeApp} from "firebase"; 2 | import {firebaseConfig} from "./src/environments/firebase.config"; 3 | import {dbData} from "./db-data"; 4 | 5 | 6 | console.log('Initizalizing Firebase database ... '); 7 | 8 | initializeApp(firebaseConfig); 9 | 10 | 11 | const coursesRef = database().ref('courses'); 12 | const lessonsRef = database().ref('lessons'); 13 | 14 | 15 | 16 | dbData.courses.forEach( course => { 17 | 18 | console.log('adding course', course.url); 19 | 20 | const courseRef = coursesRef.push({ 21 | url: course.url, 22 | description: course.description, 23 | iconUrl: course.iconUrl, 24 | courseListIcon: course.courseListIcon, 25 | longDescription: course.longDescription 26 | }); 27 | 28 | let lessonKeysPerCourse = []; 29 | 30 | course.lessons.forEach((lesson:any) => { 31 | 32 | console.log('adding lesson ', lesson.url); 33 | 34 | lessonKeysPerCourse.push(lessonsRef.push({ 35 | description: lesson.description, 36 | duration: lesson.duration, 37 | url: lesson.url, 38 | tags: lesson.tags, 39 | videoUrl: lesson.videoUrl || null, 40 | longDescription: lesson.longDescription, 41 | courseId: courseRef.key 42 | }).key); 43 | 44 | }); 45 | 46 | 47 | const association = database().ref('lessonsPerCourse'); 48 | 49 | const lessonsPerCourse = association.child(courseRef.key); 50 | 51 | lessonKeysPerCourse.forEach(lessonKey => { 52 | console.log('adding lesson to course '); 53 | 54 | const lessonCourseAssociation = lessonsPerCourse.child(lessonKey); 55 | 56 | lessonCourseAssociation.set(true); 57 | }); 58 | 59 | 60 | }); 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/angular-firebase-app/c0dfacf8d96aecb42896763cd7ac6d7fc1d589c6/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | 6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /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('AppComponent', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | declarations: [ 10 | AppComponent 11 | ], 12 | }); 13 | TestBed.compileComponents(); 14 | }); 15 | 16 | it('should create the app', async(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app).toBeTruthy(); 20 | })); 21 | 22 | it(`should have as title 'app works!'`, async(() => { 23 | const fixture = TestBed.createComponent(AppComponent); 24 | const app = fixture.debugElement.componentInstance; 25 | expect(app.title).toEqual('app works!'); 26 | })); 27 | 28 | it('should render title in a h1 tag', async(() => { 29 | const fixture = TestBed.createComponent(AppComponent); 30 | fixture.detectChanges(); 31 | const compiled = fixture.debugElement.nativeElement; 32 | expect(compiled.querySelector('h1').textContent).toContain('app works!'); 33 | })); 34 | }); 35 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.css'] 8 | }) 9 | export class AppComponent { 10 | 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppComponent } from './app.component'; 5 | import {firebaseConfig} from "../environments/firebase.config"; 6 | import {AngularFireModule} from "angularfire2"; 7 | 8 | 9 | 10 | 11 | 12 | import { HomeComponent } from './home/home.component'; 13 | import {LessonsService} from "./shared/model/lessons.service"; 14 | import { LessonsListComponent } from './lessons-list/lessons-list.component'; 15 | import {RouterModule} from "@angular/router"; 16 | import {routerConfig} from "./router.config"; 17 | import { TopMenuComponent } from './top-menu/top-menu.component'; 18 | import { CoursesComponent } from './courses/courses.component'; 19 | import {CoursesService} from "./shared/model/courses.service"; 20 | import { CourseDetailComponent } from './course-detail/course-detail.component'; 21 | import { LessonDetailComponent } from './lesson-detail/lesson-detail.component'; 22 | import { SafeUrlPipe } from './shared/security/safe-url.pipe'; 23 | import {ReactiveFormsModule} from "@angular/forms"; 24 | import { NewLessonComponent } from './new-lesson/new-lesson.component'; 25 | import { LessonFormComponent } from './lesson-form/lesson-form.component'; 26 | import { EditLessonComponent } from './edit-lesson/edit-lesson.component'; 27 | import {LessonResolver} from "./shared/model/lesson.resolver"; 28 | import { LoginComponent } from './login/login.component'; 29 | import { RegisterComponent } from './register/register.component'; 30 | import {AuthService} from "./shared/security/auth.service"; 31 | import {AuthGuard} from "./shared/security/auth.guard"; 32 | import {HttpModule} from "@angular/http"; 33 | import {AngularFireDatabaseModule} from "angularfire2/database"; 34 | import {AngularFireAuthModule} from "angularfire2/auth"; 35 | 36 | @NgModule({ 37 | declarations: [ 38 | AppComponent, 39 | HomeComponent, 40 | LessonsListComponent, 41 | TopMenuComponent, 42 | CoursesComponent, 43 | CourseDetailComponent, 44 | LessonDetailComponent, 45 | SafeUrlPipe, 46 | NewLessonComponent, 47 | LessonFormComponent, 48 | EditLessonComponent, 49 | LoginComponent, 50 | RegisterComponent 51 | ], 52 | imports: [ 53 | BrowserModule, 54 | AngularFireModule.initializeApp(firebaseConfig), 55 | AngularFireDatabaseModule, 56 | AngularFireAuthModule, 57 | RouterModule.forRoot(routerConfig), 58 | ReactiveFormsModule, 59 | HttpModule 60 | ], 61 | providers: [LessonsService, CoursesService, LessonResolver, AuthService, AuthGuard], 62 | bootstrap: [AppComponent] 63 | }) 64 | export class AppModule { } 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/app/course-detail/course-detail.component.css: -------------------------------------------------------------------------------- 1 | 2 | .nav-bar { 3 | margin-bottom: 20px; 4 | background: #FAFAFA; 5 | } 6 | 7 | .nav-bar button { 8 | margin-right: 5px; 9 | } 10 | 11 | 12 | .lessons-list-container { 13 | background: #FAFAFA; 14 | } 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/app/course-detail/course-detail.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

{{ (course$ | async)?.description }}

4 | 5 | 6 |
7 | 8 | 12 | 13 | 15 | 16 |
17 | 20 |
21 | 22 |
-------------------------------------------------------------------------------- /src/app/course-detail/course-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { CourseDetailComponent } from './course-detail.component'; 5 | 6 | describe('Component: CourseDetail', () => { 7 | it('should create an instance', () => { 8 | let component = new CourseDetailComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/course-detail/course-detail.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {CoursesService} from "../shared/model/courses.service"; 3 | import {Lesson} from "../shared/model/lesson"; 4 | import {Observable} from "rxjs/Rx"; 5 | import {ActivatedRoute, Router} from "@angular/router"; 6 | import {Course} from "../shared/model/course"; 7 | 8 | 9 | @Component({ 10 | selector: 'app-course-detail', 11 | templateUrl: './course-detail.component.html', 12 | styleUrls: ['./course-detail.component.css'] 13 | }) 14 | export class CourseDetailComponent implements OnInit { 15 | 16 | course$:Observable; 17 | lessons:Lesson[]; 18 | 19 | courseUrl:string; 20 | 21 | constructor( 22 | private router: Router, 23 | private route:ActivatedRoute, 24 | private coursesService:CoursesService) { 25 | 26 | 27 | } 28 | 29 | ngOnInit() { 30 | 31 | this.courseUrl = this.route.snapshot.params['id']; 32 | 33 | this.course$ = this.coursesService.findCourseByUrl(this.courseUrl); 34 | 35 | const lessons$ = this.coursesService.loadFirstLessonsPage(this.courseUrl, 3); 36 | 37 | lessons$.subscribe(lessons => this.lessons = lessons); 38 | 39 | } 40 | 41 | next() { 42 | 43 | this.coursesService.loadNextPage( 44 | this.courseUrl, 45 | this.lessons[this.lessons.length - 1].$key, 46 | 3 47 | ) 48 | .subscribe(lessons => this.lessons = lessons); 49 | 50 | 51 | } 52 | 53 | 54 | previous() { 55 | 56 | this.coursesService.loadPreviousPage( 57 | this.courseUrl, 58 | this.lessons[0].$key, 59 | 3 60 | ) 61 | .subscribe(lessons => this.lessons = lessons); 62 | 63 | } 64 | 65 | navigateToLesson(lesson:Lesson) { 66 | 67 | this.router.navigate(['lessons', lesson.url]); 68 | 69 | } 70 | 71 | 72 | 73 | 74 | } 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/app/courses/courses.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/angular-firebase-app/c0dfacf8d96aecb42896763cd7ac6d7fc1d589c6/src/app/courses/courses.component.css -------------------------------------------------------------------------------- /src/app/courses/courses.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 11 | 14 | 15 |
5 | 7 | 9 | {{course.description}} 10 | 12 | 13 |
16 | 17 | -------------------------------------------------------------------------------- /src/app/courses/courses.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { CoursesComponent } from './courses.component'; 5 | 6 | describe('Component: Courses', () => { 7 | it('should create an instance', () => { 8 | let component = new CoursesComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/courses/courses.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {CoursesService} from "../shared/model/courses.service"; 3 | import {Observable} from "rxjs/Rx"; 4 | import {Course} from "../shared/model/course"; 5 | 6 | @Component({ 7 | selector: 'app-courses', 8 | templateUrl: './courses.component.html', 9 | styleUrls: ['./courses.component.css'] 10 | }) 11 | export class CoursesComponent implements OnInit { 12 | 13 | courses$: Observable; 14 | 15 | constructor(private coursesService: CoursesService) { 16 | 17 | } 18 | 19 | ngOnInit() { 20 | 21 | this.courses$ = this.coursesService.findAllCourses(); 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/app/edit-lesson/edit-lesson.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/angular-firebase-app/c0dfacf8d96aecb42896763cd7ac6d7fc1d589c6/src/app/edit-lesson/edit-lesson.component.css -------------------------------------------------------------------------------- /src/app/edit-lesson/edit-lesson.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/edit-lesson/edit-lesson.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { EditLessonComponent } from './edit-lesson.component'; 5 | 6 | describe('Component: EditLesson', () => { 7 | it('should create an instance', () => { 8 | let component = new EditLessonComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/edit-lesson/edit-lesson.component.ts: -------------------------------------------------------------------------------- 1 | 2 | import {tap} from 'rxjs/operators'; 3 | import { Component, OnInit } from '@angular/core'; 4 | import {ActivatedRoute} from "@angular/router"; 5 | import {Lesson} from "../shared/model/lesson"; 6 | import {LessonsService} from "../shared/model/lessons.service"; 7 | 8 | @Component({ 9 | selector: 'app-edit-lesson', 10 | templateUrl: './edit-lesson.component.html', 11 | styleUrls: ['./edit-lesson.component.css'] 12 | }) 13 | export class EditLessonComponent implements OnInit { 14 | 15 | lesson:Lesson; 16 | 17 | constructor(private route: ActivatedRoute, 18 | private lessonsService: LessonsService) { 19 | 20 | route.data.pipe( 21 | tap(console.log)) 22 | .subscribe( 23 | data => this.lesson = data['lesson'] 24 | ); 25 | 26 | } 27 | 28 | ngOnInit() { 29 | } 30 | 31 | 32 | save(lesson) { 33 | 34 | this.lessonsService.saveLesson(this.lesson.$key, lesson) 35 | .subscribe( 36 | () => { 37 | alert("lesson saved succesfully."); 38 | }, 39 | err => alert(`error saving lesson ${err}`) 40 | ); 41 | 42 | } 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/app/home/home.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/angular-firebase-app/c0dfacf8d96aecb42896763cd7ac6d7fc1d589c6/src/app/home/home.component.css -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |

All Lessons

2 | 3 |

Total Lessons: {{lessons?.length}}

4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { HomeComponent } from './home.component'; 5 | 6 | describe('Component: Home', () => { 7 | it('should create an instance', () => { 8 | let component = new HomeComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | 2 | import {tap} from 'rxjs/operators'; 3 | import { Component, OnInit } from '@angular/core'; 4 | import {LessonsService} from "../shared/model/lessons.service"; 5 | import {Lesson} from "../shared/model/lesson"; 6 | 7 | @Component({ 8 | selector: 'app-home', 9 | templateUrl: './home.component.html', 10 | styleUrls: ['./home.component.css'] 11 | }) 12 | export class HomeComponent implements OnInit { 13 | 14 | allLessons: Lesson[]; 15 | filtered: Lesson[]; 16 | 17 | constructor(private lessonsService: LessonsService) { 18 | 19 | 20 | } 21 | 22 | ngOnInit() { 23 | this.lessonsService.findAllLessons().pipe( 24 | tap(console.log)) 25 | .subscribe( 26 | lessons => this.allLessons = this.filtered = lessons 27 | ); 28 | 29 | } 30 | 31 | search(search:string) { 32 | 33 | this.filtered = this.allLessons.filter(lesson => lesson.description.includes(search) ); 34 | 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.component'; 2 | export * from './app.module'; 3 | -------------------------------------------------------------------------------- /src/app/lesson-detail/lesson-detail.component.css: -------------------------------------------------------------------------------- 1 | 2 | .nav-bar button, tools-bar button { 3 | margin-right: 5px; 4 | } -------------------------------------------------------------------------------- /src/app/lesson-detail/lesson-detail.component.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |

{{ lesson?.description }}

7 |
Duration: {{ lesson?.duration }}
8 | 9 | 10 | 15 | 16 | 17 |

Description

18 |

{{ lesson?.longDescription }}

19 | 20 |
21 | 22 | 23 |
24 | -------------------------------------------------------------------------------- /src/app/lesson-detail/lesson-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { LessonDetailComponent } from './lesson-detail.component'; 5 | 6 | describe('Component: LessonDetail', () => { 7 | it('should create an instance', () => { 8 | let component = new LessonDetailComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/lesson-detail/lesson-detail.component.ts: -------------------------------------------------------------------------------- 1 | 2 | import {switchMap} from 'rxjs/operators'; 3 | import {Component, OnInit} from '@angular/core'; 4 | import {ActivatedRoute, Router} from "@angular/router"; 5 | import {LessonsService} from "../shared/model/lessons.service"; 6 | import {Lesson} from "../shared/model/lesson"; 7 | import * as _ from 'lodash'; 8 | 9 | 10 | @Component({ 11 | selector: 'app-lesson-detail', 12 | templateUrl: './lesson-detail.component.html', 13 | styleUrls: ['./lesson-detail.component.css'] 14 | }) 15 | export class LessonDetailComponent implements OnInit { 16 | 17 | 18 | lesson:Lesson; 19 | 20 | constructor(private route:ActivatedRoute, 21 | private router:Router, 22 | private lessonsService:LessonsService) { 23 | 24 | console.log('lesson detail created'); 25 | 26 | 27 | } 28 | 29 | 30 | ngOnInit() { 31 | 32 | this.route.params.pipe(switchMap(params => { 33 | 34 | const lessonUrl = params['id']; 35 | 36 | return this.lessonsService.findLessonByUrl(lessonUrl); 37 | })) 38 | .subscribe(lesson => this.lesson = lesson); 39 | 40 | 41 | 42 | } 43 | 44 | next() { 45 | this.lessonsService.loadNextLesson(this.lesson.courseId,this.lesson.$key) 46 | .subscribe(this.navigateToLesson.bind(this)); 47 | } 48 | 49 | previous() { 50 | this.lessonsService.loadPreviousLesson(this.lesson.courseId,this.lesson.$key) 51 | .subscribe(this.navigateToLesson.bind(this)); 52 | } 53 | 54 | 55 | navigateToLesson(lesson:Lesson) { 56 | this.router.navigate(['lessons', lesson.url]); 57 | } 58 | 59 | 60 | delete() { 61 | this.lessonsService.deleteLesson(this.lesson.$key) 62 | .subscribe( 63 | () => alert('Lesson deleted'), 64 | console.error 65 | ); 66 | } 67 | 68 | 69 | requestLessonDeletion() { 70 | this.lessonsService.requestLessonDeletion(this.lesson.$key, this.lesson.courseId); 71 | } 72 | 73 | 74 | 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/app/lesson-form/lesson-form.component.css: -------------------------------------------------------------------------------- 1 | 2 | .lesson-form { 3 | width: 350px; 4 | } 5 | 6 | 7 | .field-error-message { 8 | padding-right: 20px; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/app/lesson-form/lesson-form.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | Lesson 6 |
7 | 8 | 9 |
field is mandatory
11 |
12 |
13 | 14 | 15 |
field is mandatory
16 |
17 |
18 | 19 | 20 |
field is mandatory
21 |
not a valid url
22 |
23 |
24 | 25 | 26 |
field is mandatory
27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 |
-------------------------------------------------------------------------------- /src/app/lesson-form/lesson-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { LessonFormComponent } from './lesson-form.component'; 5 | 6 | describe('Component: LessonForm', () => { 7 | it('should create an instance', () => { 8 | let component = new LessonFormComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/lesson-form/lesson-form.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, Input, OnChanges, SimpleChanges} from '@angular/core'; 2 | import {FormBuilder, FormGroup, Validators} from "@angular/forms"; 3 | import {validateUrl} from "../shared/validators/validateUrl"; 4 | @Component({ 5 | selector: 'lesson-form', 6 | templateUrl: './lesson-form.component.html', 7 | styleUrls: ['./lesson-form.component.css'] 8 | }) 9 | export class LessonFormComponent implements OnInit, OnChanges { 10 | 11 | 12 | form:FormGroup; 13 | 14 | @Input() 15 | initialValue:any; 16 | 17 | constructor(private fb:FormBuilder) { 18 | 19 | this.form = this.fb.group({ 20 | description: ['',Validators.required], 21 | url: ['',Validators.required], 22 | videoUrl: ['',[Validators.required, validateUrl]], 23 | tags: ['',Validators.required], 24 | longDescription: [''] 25 | }); 26 | 27 | 28 | } 29 | 30 | 31 | ngOnChanges(changes:SimpleChanges) { 32 | if (changes['initialValue']) { 33 | this.form.patchValue(changes['initialValue'].currentValue); 34 | } 35 | } 36 | 37 | ngOnInit() { 38 | 39 | } 40 | 41 | isErrorVisible(field:string, error:string) { 42 | 43 | return this.form.controls[field].dirty 44 | && this.form.controls[field].errors && 45 | this.form.controls[field].errors[error]; 46 | 47 | } 48 | 49 | 50 | reset() { 51 | this.form.reset(); 52 | } 53 | 54 | 55 | get valid() { 56 | return this.form.valid; 57 | } 58 | 59 | get value() { 60 | return this.form.value; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/app/lessons-list/lessons-list.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/angular-firebase-app/c0dfacf8d96aecb42896763cd7ac6d7fc1d589c6/src/app/lessons-list/lessons-list.component.css -------------------------------------------------------------------------------- /src/app/lessons-list/lessons-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 |
{{lesson.description}} 7 | access_time 8 | {{lesson.duration}} 9 |
-------------------------------------------------------------------------------- /src/app/lessons-list/lessons-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { LessonsListComponent } from './lessons-list.component'; 5 | 6 | describe('Component: LessonsList', () => { 7 | it('should create an instance', () => { 8 | let component = new LessonsListComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/lessons-list/lessons-list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, Input, EventEmitter, Output} from '@angular/core'; 2 | import {Lesson} from "../shared/model/lesson"; 3 | 4 | @Component({ 5 | selector: 'lessons-list', 6 | templateUrl: './lessons-list.component.html', 7 | styleUrls: ['./lessons-list.component.css'] 8 | }) 9 | export class LessonsListComponent implements OnInit { 10 | 11 | @Input() 12 | lessons: Lesson[]; 13 | 14 | @Output('lesson') 15 | lessonEmitter = new EventEmitter(); 16 | 17 | constructor() { } 18 | 19 | ngOnInit() { 20 | 21 | } 22 | 23 | selectLesson(lesson:Lesson) { 24 | this.lessonEmitter.emit(lesson); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/app/login/login.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/angular-firebase-app/c0dfacf8d96aecb42896763cd7ac6d7fc1d589c6/src/app/login/login.component.css -------------------------------------------------------------------------------- /src/app/login/login.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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 | it('should create an instance', () => { 8 | let component = new LoginComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {Validators, FormGroup, FormBuilder} from "@angular/forms"; 3 | import {AuthService} from "../shared/security/auth.service"; 4 | import {Router} from "@angular/router"; 5 | 6 | @Component({ 7 | selector: 'app-login', 8 | templateUrl: './login.component.html', 9 | styleUrls: ['./login.component.css'] 10 | }) 11 | export class LoginComponent implements OnInit { 12 | 13 | form:FormGroup; 14 | 15 | constructor(private fb:FormBuilder, private authService: AuthService, 16 | private router:Router) { 17 | 18 | this.form = this.fb.group({ 19 | email: ['',Validators.required], 20 | password: ['',Validators.required] 21 | }); 22 | 23 | 24 | } 25 | 26 | ngOnInit() { 27 | } 28 | 29 | 30 | login() { 31 | 32 | const formValue = this.form.value; 33 | 34 | this.authService.login(formValue.email, formValue.password) 35 | .subscribe( 36 | () => this.router.navigate(['/home']), 37 | alert 38 | ); 39 | 40 | 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/app/new-lesson/new-lesson.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/angular-firebase-app/c0dfacf8d96aecb42896763cd7ac6d7fc1d589c6/src/app/new-lesson/new-lesson.component.css -------------------------------------------------------------------------------- /src/app/new-lesson/new-lesson.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/new-lesson/new-lesson.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {ActivatedRoute} from "@angular/router"; 3 | import {LessonsService} from "../shared/model/lessons.service"; 4 | 5 | 6 | @Component({ 7 | selector: 'app-new-lesson', 8 | templateUrl: './new-lesson.component.html', 9 | styleUrls: ['./new-lesson.component.css'] 10 | }) 11 | export class NewLessonComponent implements OnInit { 12 | 13 | courseId:string; 14 | 15 | constructor(private route:ActivatedRoute, private lessonsService: LessonsService) { } 16 | 17 | 18 | ngOnInit() { 19 | 20 | this.courseId = this.route.snapshot.queryParams['courseId']; 21 | console.log("course", this.courseId); 22 | } 23 | 24 | save(form) { 25 | this.lessonsService.createNewLesson(this.courseId, form.value) 26 | .subscribe( 27 | () => { 28 | alert("lesson created succesfully. Create another lesson ?"); 29 | form.reset(); 30 | }, 31 | err => alert(`error creating lesson ${err}`) 32 | ); 33 | 34 | } 35 | 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/app/register/register.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/angular-firebase-app/c0dfacf8d96aecb42896763cd7ac6d7fc1d589c6/src/app/register/register.component.css -------------------------------------------------------------------------------- /src/app/register/register.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 25 | -------------------------------------------------------------------------------- /src/app/register/register.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { RegisterComponent } from './register.component'; 5 | 6 | describe('Component: Register', () => { 7 | it('should create an instance', () => { 8 | let component = new RegisterComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {FormGroup, FormBuilder, Validators} from "@angular/forms"; 3 | import {AuthService} from "../shared/security/auth.service"; 4 | import {Router} from "@angular/router"; 5 | 6 | @Component({ 7 | selector: 'app-register', 8 | templateUrl: './register.component.html', 9 | styleUrls: ['./register.component.css'] 10 | }) 11 | export class RegisterComponent { 12 | 13 | form:FormGroup; 14 | 15 | constructor(private fb: FormBuilder, 16 | private authService: AuthService, 17 | private router: Router) { 18 | 19 | this.form = this.fb.group({ 20 | email: ['',Validators.required], 21 | password: ['',Validators.required], 22 | confirm: ['',Validators.required] 23 | }); 24 | 25 | 26 | } 27 | 28 | isPasswordMatch() { 29 | const val = this.form.value; 30 | return val && val.password && val.password == val.confirm; 31 | } 32 | 33 | signUp() { 34 | const val = this.form.value; 35 | 36 | this.authService.signUp(val.email, val.password) 37 | .subscribe( 38 | () => { 39 | alert('User created successfully !'); 40 | this.router.navigateByUrl('/home'); 41 | }, 42 | err => alert(err) 43 | ); 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/app/router.config.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import {Route} from "@angular/router"; 4 | import {HomeComponent} from "./home/home.component"; 5 | import {CoursesComponent} from "./courses/courses.component"; 6 | import {CourseDetailComponent} from "./course-detail/course-detail.component"; 7 | import {LessonDetailComponent} from "./lesson-detail/lesson-detail.component"; 8 | import {NewLessonComponent} from "./new-lesson/new-lesson.component"; 9 | import {EditLessonComponent} from "./edit-lesson/edit-lesson.component"; 10 | import {LessonResolver} from "./shared/model/lesson.resolver"; 11 | import {LoginComponent} from "./login/login.component"; 12 | import {RegisterComponent} from "./register/register.component"; 13 | import {AuthGuard} from "./shared/security/auth.guard"; 14 | 15 | export const routerConfig : Route[] = [ 16 | { 17 | path:'home', 18 | component: HomeComponent 19 | }, 20 | { 21 | path: 'courses', 22 | children: [ 23 | { 24 | path: ':id', 25 | children: [ 26 | { 27 | path: '', 28 | component: CourseDetailComponent 29 | }, 30 | { 31 | path: 'new', 32 | component: NewLessonComponent 33 | } 34 | ] 35 | }, 36 | { 37 | path: '', 38 | component: CoursesComponent 39 | } 40 | ] 41 | }, 42 | { 43 | path: 'lessons/:id', 44 | children: [ 45 | { 46 | path: 'edit', 47 | component: EditLessonComponent, 48 | resolve: { 49 | lesson: LessonResolver 50 | } 51 | }, 52 | { 53 | path: '', 54 | component: LessonDetailComponent, 55 | canActivate: [AuthGuard] 56 | } 57 | ] 58 | }, 59 | { 60 | 'path': 'login', 61 | component: LoginComponent 62 | }, 63 | { 64 | path: 'register', 65 | component: RegisterComponent 66 | }, 67 | { 68 | path: '', 69 | redirectTo: 'home', 70 | pathMatch: 'full' 71 | }, 72 | { 73 | path: '**', 74 | redirectTo: 'home' 75 | } 76 | ]; 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/app/shared/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/angular-firebase-app/c0dfacf8d96aecb42896763cd7ac6d7fc1d589c6/src/app/shared/index.ts -------------------------------------------------------------------------------- /src/app/shared/model/course.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import {Lesson} from "./lesson"; 5 | import {Observable} from "rxjs/Rx"; 6 | 7 | 8 | 9 | 10 | export class Course { 11 | 12 | constructor( 13 | public $key:string, 14 | public url:string, 15 | public description:string, 16 | public iconUrl: string, 17 | public courseListIcon:string, 18 | public longDescription: string) { 19 | 20 | } 21 | 22 | static fromJson({$key, url, description, iconUrl, courseListIcon, longDescription}) { 23 | return new Course($key, url, description, iconUrl, courseListIcon, longDescription); 24 | } 25 | 26 | static fromJsonArray(json : any[]) : Course[] { 27 | return json.map(Course.fromJson); 28 | } 29 | 30 | 31 | } 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/app/shared/model/courses.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async, inject } from '@angular/core/testing'; 4 | import { CoursesService } from './courses.service'; 5 | 6 | describe('Service: Courses', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [CoursesService] 10 | }); 11 | }); 12 | 13 | it('should ...', inject([CoursesService], (service: CoursesService) => { 14 | expect(service).toBeTruthy(); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/shared/model/courses.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import {combineLatest as observableCombineLatest} from 'rxjs'; 3 | 4 | import {mergeMap, filter, map, switchMap, tap} from 'rxjs/operators'; 5 | import {Injectable} from '@angular/core'; 6 | import {AngularFireDatabase} from "angularfire2/database"; 7 | import {Observable} from "rxjs/Rx"; 8 | import {Course} from "./course"; 9 | import {Lesson} from "./lesson"; 10 | import {FirebaseListFactoryOpts} from "angularfire2/interfaces"; 11 | 12 | @Injectable() 13 | export class CoursesService { 14 | 15 | 16 | constructor(private db:AngularFireDatabase) { 17 | } 18 | 19 | findAllCourses():Observable { 20 | return this.db.list('courses').pipe(map(Course.fromJsonArray)); 21 | } 22 | 23 | 24 | findCourseByUrl(courseUrl:string): Observable { 25 | return this.db.list('courses', { 26 | query: { 27 | orderByChild: 'url', 28 | equalTo: courseUrl 29 | } 30 | }).pipe( 31 | map(results => results[0])); 32 | } 33 | 34 | 35 | findLessonKeysPerCourseUrl(courseUrl:string, 36 | query: FirebaseListFactoryOpts = {}): Observable { 37 | return this.findCourseByUrl(courseUrl).pipe( 38 | tap(val => console.log("course",val)), 39 | filter(course => !!course), 40 | switchMap(course => this.db.list(`lessonsPerCourse/${course.$key}`,query)), 41 | map( lspc => lspc.map(lpc => lpc.$key) ),); 42 | } 43 | 44 | 45 | findLessonsForLessonKeys(lessonKeys$: Observable) :Observable { 46 | return lessonKeys$.pipe( 47 | map(lspc => lspc.map(lessonKey => this.db.object('lessons/' + lessonKey)) ), 48 | mergeMap(fbojs => observableCombineLatest(fbojs) ),) 49 | 50 | } 51 | 52 | 53 | findAllLessonsForCourse(courseUrl:string):Observable { 54 | return this.findLessonsForLessonKeys(this.findLessonKeysPerCourseUrl(courseUrl)); 55 | } 56 | 57 | 58 | loadFirstLessonsPage(courseUrl:string, pageSize:number): Observable { 59 | 60 | const firstPageLessonKeys$ = this.findLessonKeysPerCourseUrl(courseUrl, 61 | { 62 | query: { 63 | limitToFirst:pageSize 64 | } 65 | }); 66 | 67 | return this.findLessonsForLessonKeys(firstPageLessonKeys$); 68 | } 69 | 70 | 71 | 72 | 73 | loadNextPage(courseUrl:string, 74 | lessonKey:string, pageSize:number): Observable { 75 | 76 | const lessonKeys$ = this.findLessonKeysPerCourseUrl(courseUrl, 77 | { 78 | query: { 79 | orderByKey: true, 80 | startAt: lessonKey, 81 | limitToFirst:pageSize + 1 82 | } 83 | }); 84 | 85 | return this.findLessonsForLessonKeys(lessonKeys$).pipe( 86 | map(lessons => lessons.slice(1, lessons.length))); 87 | 88 | 89 | } 90 | 91 | loadPreviousPage(courseUrl:string, 92 | lessonKey:string, pageSize:number): Observable { 93 | 94 | 95 | const lessonKeys$ = this.findLessonKeysPerCourseUrl(courseUrl, 96 | { 97 | query: { 98 | orderByKey: true, 99 | endAt: lessonKey, 100 | limitToLast:pageSize + 1 101 | } 102 | }); 103 | 104 | return this.findLessonsForLessonKeys(lessonKeys$).pipe( 105 | map(lessons => lessons.slice(0, lessons.length - 1))); 106 | 107 | } 108 | 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/app/shared/model/lesson.resolver.ts: -------------------------------------------------------------------------------- 1 | 2 | import {first} from 'rxjs/operators'; 3 | 4 | 5 | 6 | import {Resolve, ActivatedRouteSnapshot, RouterStateSnapshot} from "@angular/router"; 7 | import {Lesson} from "./lesson"; 8 | import {Observable} from "rxjs/Rx"; 9 | import {Injectable} from "@angular/core"; 10 | import {LessonsService} from "./lessons.service"; 11 | 12 | 13 | @Injectable() 14 | export class LessonResolver implements Resolve { 15 | 16 | 17 | constructor(private lessonsService: LessonsService) { 18 | 19 | } 20 | 21 | resolve(route:ActivatedRouteSnapshot, 22 | state:RouterStateSnapshot):Observable { 23 | 24 | return this.lessonsService 25 | .findLessonByUrl(route.params['id']).pipe( 26 | first()); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/app/shared/model/lesson.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export class Lesson { 5 | 6 | 7 | constructor( 8 | public $key:string, 9 | public description: string, 10 | public duration: string, 11 | public url: string, 12 | public tags: string, 13 | public pro: boolean, 14 | public longDescription: string, 15 | public courseId:string, 16 | public videoUrl:string) { 17 | 18 | } 19 | 20 | 21 | get isBeginner() { 22 | return this.tags && this.tags.includes('BEGINNER'); 23 | } 24 | 25 | 26 | static fromJsonList(array): Lesson[] { 27 | return array.map(Lesson.fromJson); 28 | } 29 | 30 | static fromJson({$key, description, duration, 31 | url,tags,pro,longDescription, courseId,videoUrl}):Lesson { 32 | return new Lesson( 33 | $key, 34 | description, 35 | duration, 36 | url, 37 | tags, 38 | pro, 39 | longDescription, 40 | courseId, 41 | videoUrl); 42 | } 43 | 44 | 45 | } 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/app/shared/model/lessons.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async, inject } from '@angular/core/testing'; 4 | import { LessonsService } from './lessons.service'; 5 | 6 | describe('Service: Lessons', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [LessonsService] 10 | }); 11 | }); 12 | 13 | it('should ...', inject([LessonsService], (service: LessonsService) => { 14 | expect(service).toBeTruthy(); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/shared/model/lessons.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import {switchMap, filter, map, tap} from 'rxjs/operators'; 3 | import {Injectable, Inject} from '@angular/core'; 4 | import {Observable, Subject} from "rxjs/Rx"; 5 | import {Lesson} from "./lesson"; 6 | import {AngularFireDatabase} from "angularfire2/database"; 7 | import {FirebaseApp} from 'angularfire2'; 8 | import {Http} from "@angular/http"; 9 | import {firebaseConfig} from "../../../environments/firebase.config"; 10 | 11 | 12 | @Injectable() 13 | export class LessonsService { 14 | 15 | sdkDb:any; 16 | 17 | constructor(private db:AngularFireDatabase, @Inject(FirebaseApp) fb: FirebaseApp, 18 | private http:Http) { 19 | 20 | this.sdkDb = fb.database(); 21 | 22 | } 23 | 24 | 25 | findAllLessons():Observable { 26 | 27 | return this.db.list('lessons').pipe( 28 | tap(console.log), 29 | map(Lesson.fromJsonList),); 30 | 31 | } 32 | 33 | findLessonByUrl(url:string):Observable { 34 | return this.db.list('lessons', { 35 | query: { 36 | orderByChild: 'url', 37 | equalTo: url 38 | } 39 | }).pipe( 40 | filter(results => results && results.length > 0), 41 | map(results => Lesson.fromJson(results[0])), 42 | tap(console.log),); 43 | } 44 | 45 | 46 | loadNextLesson(courseId:string, lessonId:string):Observable { 47 | return this.db.list(`lessonsPerCourse/${courseId}`, { 48 | query: { 49 | orderByKey:true, 50 | startAt: lessonId, 51 | limitToFirst: 2 52 | } 53 | }).pipe( 54 | filter(results => results && results.length > 0), 55 | map(results => results[1].$key), 56 | switchMap(lessonId => this.db.object(`lessons/${lessonId}`)), 57 | map(Lesson.fromJson),); 58 | } 59 | 60 | 61 | loadPreviousLesson(courseId:string, lessonId:string):Observable { 62 | return this.db.list(`lessonsPerCourse/${courseId}`, { 63 | query: { 64 | orderByKey:true, 65 | endAt: lessonId, 66 | limitToLast: 2 67 | } 68 | }).pipe( 69 | filter(results => results && results.length > 0), 70 | map(results => results[0].$key), 71 | switchMap(lessonId => this.db.object(`lessons/${lessonId}`)), 72 | map(Lesson.fromJson),); 73 | 74 | } 75 | 76 | createNewLesson(courseId:string, lesson:any): Observable { 77 | 78 | const lessonToSave = Object.assign({}, lesson, {courseId}); 79 | 80 | const newLessonKey = this.sdkDb.child('lessons').push().key; 81 | 82 | let dataToSave = {}; 83 | 84 | dataToSave["lessons/" + newLessonKey] = lessonToSave; 85 | dataToSave[`lessonsPerCourse/${courseId}/${newLessonKey}`] = true; 86 | 87 | 88 | return this.firebaseUpdate(dataToSave); 89 | } 90 | 91 | firebaseUpdate(dataToSave) { 92 | const subject = new Subject(); 93 | 94 | this.sdkDb.update(dataToSave) 95 | .then( 96 | val => { 97 | subject.next(val); 98 | subject.complete(); 99 | 100 | }, 101 | err => { 102 | subject.error(err); 103 | subject.complete(); 104 | } 105 | ); 106 | 107 | return subject.asObservable(); 108 | } 109 | 110 | 111 | saveLesson(lessonId:string, lesson): Observable { 112 | 113 | const lessonToSave = Object.assign({}, lesson); 114 | delete(lessonToSave.$key); 115 | 116 | let dataToSave = {}; 117 | dataToSave[`lessons/${lessonId}`] = lessonToSave; 118 | 119 | return this.firebaseUpdate(dataToSave); 120 | 121 | 122 | } 123 | 124 | 125 | deleteLesson(lessonId:string): Observable { 126 | 127 | const url = firebaseConfig.databaseURL + '/lessons/' + lessonId + '.json'; 128 | 129 | return this.http.delete(url); 130 | } 131 | 132 | 133 | requestLessonDeletion(lessonId:string, courseId:string) { 134 | this.sdkDb.child('queue/tasks').push({lessonId,courseId}) 135 | .then( 136 | () => alert('lesson deletion requested !') 137 | ); 138 | } 139 | 140 | 141 | 142 | } 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /src/app/shared/security/auth-info.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export class AuthInfo { 5 | 6 | constructor( 7 | public $uid:string 8 | ) { 9 | 10 | } 11 | 12 | 13 | isLoggedIn() { 14 | return !!this.$uid; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/shared/security/auth.guard.ts: -------------------------------------------------------------------------------- 1 | 2 | import {tap, take, map} from 'rxjs/operators'; 3 | 4 | 5 | import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router} from "@angular/router"; 6 | import {Observable} from "rxjs/Rx"; 7 | import {Injectable} from "@angular/core"; 8 | import {AuthService} from "./auth.service"; 9 | 10 | @Injectable() 11 | export class AuthGuard implements CanActivate { 12 | 13 | 14 | constructor(private authService:AuthService, private router:Router) { 15 | 16 | } 17 | 18 | canActivate(route:ActivatedRouteSnapshot, 19 | state:RouterStateSnapshot):Observable { 20 | 21 | 22 | return this.authService.authInfo$.pipe( 23 | map(authInfo => authInfo.isLoggedIn()), 24 | take(1), 25 | tap(allowed => { 26 | if(!allowed) { 27 | this.router.navigate(['/login']); 28 | } 29 | }),); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/app/shared/security/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async, inject } from '@angular/core/testing'; 4 | import { AuthService } from './auth.service'; 5 | 6 | describe('Service: Auth', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [AuthService] 10 | }); 11 | }); 12 | 13 | it('should ...', inject([AuthService], (service: AuthService) => { 14 | expect(service).toBeTruthy(); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/shared/security/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import {Observable, Subject, BehaviorSubject} from "rxjs/Rx"; 3 | import {AngularFireAuth } from "angularfire2/auth"; 4 | import {AuthInfo} from "./auth-info"; 5 | import {Router} from "@angular/router"; 6 | import * as firebase from 'firebase/app'; 7 | 8 | 9 | @Injectable() 10 | export class AuthService { 11 | 12 | static UNKNOWN_USER = new AuthInfo(null); 13 | 14 | authInfo$: BehaviorSubject = new BehaviorSubject(AuthService.UNKNOWN_USER); 15 | 16 | 17 | constructor(private afAuth: AngularFireAuth, private router:Router) { 18 | 19 | } 20 | 21 | 22 | 23 | 24 | login(email, password):Observable { 25 | return this.fromFirebaseAuthPromise(this.afAuth.auth.signInWithEmailAndPassword(email, password)); 26 | } 27 | 28 | 29 | signUp(email, password) { 30 | return this.fromFirebaseAuthPromise(this.afAuth.auth.createUserWithEmailAndPassword(email, password)); 31 | } 32 | 33 | /* 34 | * 35 | * This is a demo on how we can 'Observify' any asynchronous interaction 36 | * 37 | * 38 | * */ 39 | 40 | fromFirebaseAuthPromise(promise):Observable { 41 | 42 | const subject = new Subject(); 43 | 44 | promise 45 | .then(res => { 46 | const authInfo = new AuthInfo(this.afAuth.auth.currentUser.uid); 47 | this.authInfo$.next(authInfo); 48 | subject.next(res); 49 | subject.complete(); 50 | }, 51 | err => { 52 | this.authInfo$.error(err); 53 | subject.error(err); 54 | subject.complete(); 55 | }); 56 | 57 | return subject.asObservable(); 58 | } 59 | 60 | 61 | logout() { 62 | this.afAuth.auth.signOut(); 63 | this.authInfo$.next(AuthService.UNKNOWN_USER); 64 | this.router.navigate(['/home']); 65 | 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/app/shared/security/safe-url.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { SafeUrlPipe } from './safe-url.pipe'; 5 | 6 | describe('Pipe: SafeUrl', () => { 7 | it('create an instance', () => { 8 | let pipe = new SafeUrlPipe(); 9 | expect(pipe).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/shared/security/safe-url.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import {DomSanitizer} from "@angular/platform-browser"; 3 | 4 | @Pipe({ 5 | name: 'safeUrl' 6 | }) 7 | export class SafeUrlPipe implements PipeTransform { 8 | 9 | constructor(private sanitizer: DomSanitizer) { 10 | 11 | } 12 | 13 | transform(url) { 14 | return this.sanitizer.bypassSecurityTrustResourceUrl(url); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/shared/validators/validateUrl.ts: -------------------------------------------------------------------------------- 1 | import {FormControl} from "@angular/forms"; 2 | 3 | 4 | export function validateUrl(ctrl:FormControl) { 5 | 6 | const urlValue = ctrl.value; 7 | 8 | const valid = /^(ftp|http|https):\/\/[^ "]+$/.test(urlValue); 9 | 10 | return valid ? null: { 11 | validUrl: { 12 | valid: false 13 | } 14 | } 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/top-menu/top-menu.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .top-menu li:first-child { 4 | margin-left: 40px; 5 | } 6 | 7 | header { 8 | height:55px; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/top-menu/top-menu.component.html: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /src/app/top-menu/top-menu.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { TopMenuComponent } from './top-menu.component'; 5 | 6 | describe('Component: TopMenu', () => { 7 | it('should create an instance', () => { 8 | let component = new TopMenuComponent(); 9 | expect(component).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/top-menu/top-menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {AuthService} from "../shared/security/auth.service"; 3 | import {AuthInfo} from "../shared/security/auth-info"; 4 | 5 | @Component({ 6 | selector: 'top-menu', 7 | templateUrl: './top-menu.component.html', 8 | styleUrls: ['./top-menu.component.css'] 9 | }) 10 | export class TopMenuComponent implements OnInit { 11 | 12 | authInfo: AuthInfo; 13 | 14 | constructor(private authService:AuthService) { 15 | 16 | 17 | 18 | } 19 | 20 | ngOnInit() { 21 | 22 | 23 | this.authService.authInfo$.subscribe(authInfo => this.authInfo = authInfo); 24 | 25 | 26 | } 27 | 28 | 29 | logout() { 30 | this.authService.logout(); 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/angular-firebase-app/c0dfacf8d96aecb42896763cd7ac6d7fc1d589c6/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/angular-firebase-app/c0dfacf8d96aecb42896763cd7ac6d7fc1d589c6/src/assets/.npmignore -------------------------------------------------------------------------------- /src/assets/app.css: -------------------------------------------------------------------------------- 1 | 2 | .lesson { 3 | margin: 25px auto 0 auto; 4 | max-width: 350px; 5 | } 6 | 7 | .lessons-list { 8 | padding: 10px 10px 0 10px; 9 | display: table-cell; 10 | margin-bottom: 15px; 11 | } 12 | 13 | .lessons-list tr { 14 | border-bottom: 1px solid darkgray; 15 | cursor: pointer; 16 | } 17 | 18 | .lessons-list td { 19 | padding-bottom: 5px; 20 | } 21 | 22 | .lesson-logo { 23 | height: 20px; 24 | margin-right: 10px; 25 | } 26 | 27 | .add-lesson { 28 | width: 350px; 29 | margin-bottom: 15px; 30 | } 31 | 32 | .md-icon { 33 | font-family: 'Material Icons'; 34 | text-rendering: optimizeLegibility; 35 | font-feature-settings: "liga" 1; 36 | font-style: normal; 37 | text-transform: none; 38 | line-height: 1; 39 | width: 24px; 40 | height: 24px; 41 | display: inline-block; 42 | overflow: hidden; 43 | -webkit-font-smoothing: antialiased; 44 | -moz-osx-font-smoothing: grayscale; 45 | } 46 | 47 | .collapsible-indicator { 48 | font-size: 30px; 49 | line-height:30px; 50 | } 51 | 52 | 53 | .collapsible-section { 54 | padding: 0 20px 20px 20px; 55 | } 56 | 57 | .collapsed .collapsible-section { 58 | display: none; 59 | } 60 | 61 | 62 | .disable-text-selection { 63 | -webkit-touch-callout: none; 64 | -webkit-user-select: none; 65 | -moz-user-select: none; 66 | -ms-user-select: none; 67 | user-select: none; 68 | } 69 | 70 | 71 | .course-logo { 72 | height: 75px; 73 | margin: 0 auto; 74 | background-color: #FAFAFA; 75 | background-image: url(https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png); 76 | background-repeat: no-repeat; 77 | background-position-x: center; 78 | background-size: 65px; 79 | background-position-y: center; 80 | } 81 | 82 | form label { 83 | width: 100px; 84 | display: inline-block; 85 | text-align: right; 86 | vertical-align: top; 87 | margin-right: 5px; 88 | } 89 | 90 | .field-error-message { 91 | text-align: right; 92 | padding-right: 68px; 93 | font-size: 16px; 94 | color: #a10000; 95 | } 96 | 97 | form fieldset { 98 | margin-bottom: 20px; 99 | } 100 | 101 | form textarea { 102 | width: 170px; 103 | } 104 | 105 | form input[type='radio'] { 106 | box-shadow: none; 107 | width: 20px; 108 | vertical-align: bottom; 109 | margin-left: 10px; 110 | } 111 | 112 | form button[type='submit'] { 113 | float: right; 114 | margin-right: 75px; 115 | } 116 | 117 | .debug { 118 | clear: both; 119 | font-size: 14px; 120 | } 121 | 122 | .debug h3 { 123 | margin-top: 20px; 124 | margin-bottom: 5px; 125 | } 126 | 127 | .form-field { 128 | margin-bottom: 15px; 129 | } 130 | 131 | .lesson-button { 132 | background: #1976d2; 133 | color: white; 134 | font-weight: bold; 135 | } 136 | 137 | .lesson-button[disabled] { 138 | background: grey; 139 | color: white; 140 | cursor: not-allowed; 141 | } 142 | 143 | button[disabled] { 144 | cursor: not-allowed; 145 | opacity: 0.5; 146 | } 147 | 148 | 149 | .top-menu { 150 | margin-bottom: 30px; 151 | } 152 | 153 | 154 | 155 | 156 | .ng-dirty.ng-invalid { 157 | border: 2px solid #ff2118; 158 | } 159 | 160 | .ng-touched.ng-invalid { 161 | border: 1px solid #cccccc !important; 162 | } 163 | 164 | 165 | form.ng-dirty.ng-invalid { 166 | border:none; 167 | } 168 | 169 | form.ng-touched.ng-invalid { 170 | border:none !important; 171 | } 172 | 173 | .l-header img { 174 | cursor: pointer; 175 | } 176 | 177 | .youtube-logo { 178 | max-height: 100px; 179 | border-radius: 4px; 180 | } 181 | 182 | .lessons-list { 183 | text-align: left; 184 | } 185 | 186 | .home-screen { 187 | margin-top: 50px; 188 | } 189 | 190 | .courses-list { 191 | padding: 5px 10px; 192 | text-align: left; 193 | } 194 | 195 | 196 | table.courses-list tr { 197 | border-bottom: 1px solid darkgray; 198 | } 199 | 200 | table.courses-list tr td { 201 | padding: 5px 0; 202 | } 203 | 204 | table.courses-list tr td.description { 205 | padding-right: 15px; 206 | } 207 | 208 | table.courses-list tr td:first-child { 209 | padding-left: 15px; 210 | } 211 | 212 | table.courses-list tr td:last-child { 213 | padding-right: 15px; 214 | } 215 | 216 | ul.top-menu > li > a.menu-active { 217 | color: #ee1c1b; 218 | font-weight: bold; 219 | text-decoration: underline; 220 | } 221 | 222 | .nav-button { 223 | margin-bottom: 20px; 224 | } 225 | 226 | .playlist { 227 | float: right; 228 | } 229 | 230 | .main-container .list { 231 | display: inline-block; 232 | max-width: 270px; 233 | vertical-align: top; 234 | margin-right: 50px; 235 | } 236 | 237 | 238 | .nav-fields { 239 | margin-bottom: 40px; 240 | } 241 | 242 | .course-summary { 243 | cursor: pointer; 244 | } 245 | 246 | .chat { 247 | 248 | } 249 | 250 | img.dashboard-section { 251 | display: block; 252 | max-height: 300px; 253 | margin: 0 auto 50px auto; 254 | 255 | } 256 | 257 | .toggle-buttons { 258 | margin-bottom: 15px; 259 | } 260 | 261 | .graph-toggle { 262 | width: 30px; 263 | box-shadow: none; 264 | vertical-align: bottom; 265 | } 266 | 267 | 268 | 269 | .l-header { 270 | height: 58px; 271 | } 272 | 273 | .top-menu { 274 | margin-left: 50px; 275 | } 276 | 277 | .top-menu li { 278 | box-shadow: none !important; 279 | } 280 | 281 | .l-sample-app { 282 | text-align: center; 283 | } 284 | 285 | .tools-bar { 286 | text-align: right; 287 | margin-top: 30px; 288 | } 289 | 290 | .login-form { 291 | width: 350px; 292 | } 293 | 294 | .search-bar { 295 | margin-bottom: 30px; 296 | width: 250px; 297 | } 298 | 299 | 300 | -------------------------------------------------------------------------------- /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/environments/firebase.config.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | export const firebaseConfig = { 6 | apiKey: "AIzaSyA0BcUcu4V8aHT_gM-32BhRcmqji4z-lts", 7 | authDomain: "final-project-recording.firebaseapp.com", 8 | databaseURL: "https://final-project-recording.firebaseio.com", 9 | storageBucket: "final-project-recording.appspot.com", 10 | messagingSenderId: "290354329688" 11 | }; 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/angular-firebase-app/c0dfacf8d96aecb42896763cd7ac6d7fc1d589c6/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FinalProject 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | Loading... 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /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 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | import 'core-js/es6/reflect'; 45 | import 'core-js/es7/reflect'; 46 | 47 | 48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | 70 | 71 | // Add global to window, assigning the value of window itself. 72 | (window as any).global = window; -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /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.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "baseUrl": "", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts", 15 | "polyfills.ts" 16 | ], 17 | "include": [ 18 | "**/*.spec.ts", 19 | "**/*.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "baseUrl": "src", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2016", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /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], 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 | true, 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, "ignore-params"], 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 | --------------------------------------------------------------------------------