├── .browserslistrc ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── resources ├── challenges.md └── slides.pdf ├── server └── db.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── courses │ │ ├── course-details │ │ │ ├── course-details.component.html │ │ │ ├── course-details.component.scss │ │ │ ├── course-details.component.spec.ts │ │ │ └── course-details.component.ts │ │ ├── courses-list │ │ │ ├── courses-list.component.html │ │ │ ├── courses-list.component.scss │ │ │ ├── courses-list.component.spec.ts │ │ │ └── courses-list.component.ts │ │ ├── courses.component.html │ │ ├── courses.component.scss │ │ ├── courses.component.spec.ts │ │ └── courses.component.ts │ ├── examples │ │ ├── child-routes │ │ │ ├── admin-payroll │ │ │ │ ├── admin-payroll.component.html │ │ │ │ ├── admin-payroll.component.scss │ │ │ │ ├── admin-payroll.component.spec.ts │ │ │ │ └── admin-payroll.component.ts │ │ │ ├── admin-vacation │ │ │ │ ├── admin-vacation.component.html │ │ │ │ ├── admin-vacation.component.scss │ │ │ │ ├── admin-vacation.component.spec.ts │ │ │ │ └── admin-vacation.component.ts │ │ │ ├── admin.component.html │ │ │ ├── admin.component.scss │ │ │ ├── admin.component.spec.ts │ │ │ └── admin.component.ts │ │ ├── custom-input │ │ │ ├── custom-control │ │ │ │ ├── custom-control.component.html │ │ │ │ ├── custom-control.component.scss │ │ │ │ ├── custom-control.component.spec.ts │ │ │ │ └── custom-control.component.ts │ │ │ ├── custom-input.component.html │ │ │ ├── custom-input.component.scss │ │ │ ├── custom-input.component.spec.ts │ │ │ └── custom-input.component.ts │ │ ├── dynamic-component │ │ │ ├── circle │ │ │ │ └── circle.component.ts │ │ │ ├── dynamic-component.component.html │ │ │ ├── dynamic-component.component.scss │ │ │ ├── dynamic-component.component.spec.ts │ │ │ ├── dynamic-component.component.ts │ │ │ ├── square │ │ │ │ └── square.component.ts │ │ │ └── triangle │ │ │ │ └── triangle.component.ts │ │ ├── examples.component.html │ │ ├── examples.component.scss │ │ ├── examples.component.spec.ts │ │ ├── examples.component.ts │ │ ├── lazy-module │ │ │ ├── lazy-module.component.html │ │ │ ├── lazy-module.component.scss │ │ │ ├── lazy-module.component.spec.ts │ │ │ ├── lazy-module.component.ts │ │ │ ├── lazy-module.module.ts │ │ │ └── lazy-routing.module.ts │ │ ├── protected │ │ │ ├── protected.component.html │ │ │ ├── protected.component.scss │ │ │ ├── protected.component.spec.ts │ │ │ └── protected.component.ts │ │ └── route-params │ │ │ ├── route-params.component.html │ │ │ ├── route-params.component.scss │ │ │ ├── route-params.component.spec.ts │ │ │ └── route-params.component.ts │ ├── home │ │ ├── home.component.html │ │ ├── home.component.scss │ │ ├── home.component.spec.ts │ │ └── home.component.ts │ ├── login │ │ ├── login.component.html │ │ ├── login.component.scss │ │ ├── login.component.spec.ts │ │ └── login.component.ts │ ├── material.module.ts │ └── shared │ │ ├── guards │ │ ├── auth.guard.spec.ts │ │ └── auth.guard.ts │ │ ├── interfaces │ │ ├── auth.interface.ts │ │ └── course.interface.ts │ │ └── services │ │ ├── auth │ │ ├── auth.service.spec.ts │ │ └── auth.service.ts │ │ ├── courses │ │ ├── courses.service.spec.ts │ │ └── courses.service.ts │ │ ├── interceptors │ │ ├── http-interceptor.service.spec.ts │ │ └── http-interceptor.service.ts │ │ └── notifications │ │ ├── notification.service.spec.ts │ │ └── notification.service.ts ├── assets │ ├── .gitkeep │ ├── logo.png │ └── screenshots │ │ └── demo.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://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 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .history/* 32 | 33 | # misc 34 | /.angular/cache 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular 13 Fundamentals Workshop 2 | 3 | ![Angular 13 Fundamentals Workshop App](src/assets/screenshots/demo.png) 4 | 5 | This is the sample project for the Angular 13 Fundamentals Workshop. 6 | 7 | This sample project includes an Angular web application generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.0.3. It also contains a mock RESTful API using `json-server`. 8 | 9 | ## Prerequisites 10 | - [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 11 | - Node.js and NPM – we recommend using [NVM (Linux/Mac)](https://github.com/creationix/nvm) or [NVM-Windows (Windows)](https://github.com/coreybutler/nvm-windows) 12 | - Install Angular CLI via `npm i -g @angular/cli` 13 | 14 | ## Web: Getting Started 15 | 16 | ``` 17 | git clone https://github.com/onehungrymind/angular13-fundamentals-workshop 18 | cd angular13-fundamentals-workshop 19 | npm i 20 | npm start 21 | ``` 22 | 23 | The `npm start` calls the `serve:all` command which is a convenience method that runs the `serve:api` and `serve:web` commands concurrently. You can run each command separately if you need to. 24 | 25 | ``` 26 | "start": "npm run serve:all", 27 | "serve:web": "ng serve --port 4300 --open", 28 | "serve:api": "json-server server/db.json", 29 | "serve:all": "concurrently \"npm run serve:api\" \"npm run serve:web\"", 30 | ``` 31 | 32 | The web application will open to [http://localhost:4200](http://localhost:4200) in your browser. 33 | 34 | You can see the API by navigating to [http://localhost:3000/](http://localhost:3000/) in your browser. 35 | 36 | > Note: the above terminal commands are for Mac. Remember to substitute the appropriate commands for your OS. 37 | 38 | ## Development server 39 | 40 | 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. 41 | 42 | ## Code scaffolding 43 | 44 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 45 | 46 | ## Build 47 | 48 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 49 | 50 | ## Running unit tests 51 | 52 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 53 | 54 | ## Running end-to-end tests 55 | 56 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 57 | 58 | ## Further help 59 | 60 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 61 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular13-fundamentals-workshop": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist/angular13-fundamentals-workshop", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "inlineStyleLanguage": "scss", 29 | "assets": [ 30 | "src/favicon.ico", 31 | "src/assets" 32 | ], 33 | "styles": [ 34 | "src/styles.scss" 35 | ], 36 | "scripts": [] 37 | }, 38 | "configurations": { 39 | "production": { 40 | "budgets": [ 41 | { 42 | "type": "initial", 43 | "maximumWarning": "500kb", 44 | "maximumError": "1mb" 45 | }, 46 | { 47 | "type": "anyComponentStyle", 48 | "maximumWarning": "2kb", 49 | "maximumError": "4kb" 50 | } 51 | ], 52 | "fileReplacements": [ 53 | { 54 | "replace": "src/environments/environment.ts", 55 | "with": "src/environments/environment.prod.ts" 56 | } 57 | ], 58 | "outputHashing": "all" 59 | }, 60 | "development": { 61 | "buildOptimizer": false, 62 | "optimization": false, 63 | "vendorChunk": true, 64 | "extractLicenses": false, 65 | "sourceMap": true, 66 | "namedChunks": true 67 | } 68 | }, 69 | "defaultConfiguration": "production" 70 | }, 71 | "serve": { 72 | "builder": "@angular-devkit/build-angular:dev-server", 73 | "configurations": { 74 | "production": { 75 | "browserTarget": "angular13-fundamentals-workshop:build:production" 76 | }, 77 | "development": { 78 | "browserTarget": "angular13-fundamentals-workshop:build:development" 79 | } 80 | }, 81 | "defaultConfiguration": "development" 82 | }, 83 | "extract-i18n": { 84 | "builder": "@angular-devkit/build-angular:extract-i18n", 85 | "options": { 86 | "browserTarget": "angular13-fundamentals-workshop:build" 87 | } 88 | }, 89 | "test": { 90 | "builder": "@angular-devkit/build-angular:karma", 91 | "options": { 92 | "main": "src/test.ts", 93 | "polyfills": "src/polyfills.ts", 94 | "tsConfig": "tsconfig.spec.json", 95 | "karmaConfig": "karma.conf.js", 96 | "inlineStyleLanguage": "scss", 97 | "assets": [ 98 | "src/favicon.ico", 99 | "src/assets" 100 | ], 101 | "styles": [ 102 | "src/styles.scss" 103 | ], 104 | "scripts": [] 105 | } 106 | } 107 | } 108 | } 109 | }, 110 | "defaultProject": "angular13-fundamentals-workshop" 111 | } 112 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/angular13-fundamentals-workshop'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular13-fundamentals-workshop", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "npm run serve:all", 7 | "serve:web": "ng serve --port 4300 --open", 8 | "serve:api": "json-server server/db.json", 9 | "serve:all": "concurrently \"npm run serve:api\" \"npm run serve:web\"", 10 | "build": "ng build", 11 | "watch": "ng build --watch --configuration development", 12 | "test": "ng test" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "~13.0.0", 17 | "@angular/cdk": "^13.0.2", 18 | "@angular/common": "~13.0.0", 19 | "@angular/compiler": "~13.0.0", 20 | "@angular/core": "~13.0.0", 21 | "@angular/forms": "~13.0.0", 22 | "@angular/material": "^13.0.2", 23 | "@angular/platform-browser": "~13.0.0", 24 | "@angular/platform-browser-dynamic": "~13.0.0", 25 | "@angular/router": "~13.0.0", 26 | "concurrently": "^6.4.0", 27 | "json-server": "^0.17.0", 28 | "rxjs": "~7.4.0", 29 | "tslib": "^2.3.0", 30 | "zone.js": "~0.11.4" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "~13.0.3", 34 | "@angular/cli": "~13.0.3", 35 | "@angular/compiler-cli": "~13.0.0", 36 | "@types/jasmine": "~3.10.0", 37 | "@types/node": "^12.11.1", 38 | "jasmine-core": "~3.10.0", 39 | "karma": "~6.3.0", 40 | "karma-chrome-launcher": "~3.1.0", 41 | "karma-coverage": "~2.0.3", 42 | "karma-jasmine": "~4.0.0", 43 | "karma-jasmine-html-reporter": "~1.7.0", 44 | "typescript": "~4.4.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /resources/challenges.md: -------------------------------------------------------------------------------- 1 | 2 | # Module 01: Component Challenges 3 | In the `src/app/courses/courses.component.ts` file, complete the following challenges. 4 | - Display courses using `ngFor` 5 | - Add event handler to select course 6 | - Display raw JSON of selected course 7 | 8 | # Module 02: Form Challenges 9 | In the `src/app/courses/courses.component.ts` file, complete the following challenges. 10 | - Update the form to show `percentComplete` 11 | - Update the form to show `favorite` 12 | 13 | # Module 03: Services Challenges 14 | Complete the following challenges. 15 | - Generate a `LessonsService` service 16 | - Inject `LessonsService` into `HomeComponent` 17 | - Move `lessons` to the `LessonsService` and consume in the `HomeComponent` 18 | 19 | **HINT:** `ng g s shared/services/lessons -d` 20 | 21 | # Module 04: Server Communication Challenges 22 | Complete the following challenges. 23 | - Complete remote update call functionality in the `CoursesService` 24 | - Complete remote delete call functionality in the `CoursesService` 25 | - Update the `CoursesComponent` to display remote data 26 | 27 | # Module 05: Component Driven Architecture Challenges 28 | Complete the following challenges. 29 | - Generate a `LessonsListComponent` component 30 | - Create the appropriate inputs and outputs 31 | - Render `LessonsList` component 32 | 33 | **HINT:** `ng g c lessons/lessons -m app.module.ts -d` 34 | 35 | # Module 06: Routing Challenges 36 | Complete the following challenges. 37 | - Generate a `UsersComponent` component 38 | - Create a new `users` route 39 | - Update the `AppComponent` to create link to the `users` route in the `SideNav` 40 | -------------------------------------------------------------------------------- /resources/slides.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/resources/slides.pdf -------------------------------------------------------------------------------- /server/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "courses": [ 3 | { 4 | "id": 1, 5 | "title": "Angular 13 Fundamentals", 6 | "description": "Learn the fundamentals of Angular 13", 7 | "percentComplete": 26, 8 | "favorite": true 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { AdminPayrollComponent } from './examples/child-routes/admin-payroll/admin-payroll.component'; 4 | import { AdminVacationComponent } from './examples/child-routes/admin-vacation/admin-vacation.component'; 5 | import { AdminComponent } from './examples/child-routes/admin.component'; 6 | import { CoursesComponent } from './courses/courses.component'; 7 | import { DynamicComponent } from './examples/dynamic-component/dynamic-component.component'; 8 | import { CustomInputComponent } from './examples/custom-input/custom-input.component'; 9 | import { ExamplesComponent } from './examples/examples.component'; 10 | import { HomeComponent } from './home/home.component'; 11 | import { LoginComponent } from './login/login.component'; 12 | import { RouteParamsComponent } from './examples/route-params/route-params.component'; 13 | import { AuthGuard } from './shared/guards/auth.guard'; 14 | import { ProtectedComponent } from './examples/protected/protected.component'; 15 | 16 | const routes: Routes = [ 17 | { path: 'home', component: HomeComponent }, 18 | { path: 'courses', component: CoursesComponent }, 19 | { path: 'login', component: LoginComponent }, 20 | { 21 | path: 'examples', 22 | component: ExamplesComponent, 23 | children: [ 24 | { path: 'create', component: DynamicComponent }, 25 | { path: 'input', component: CustomInputComponent }, 26 | { 27 | path: 'child', 28 | component: AdminComponent, 29 | children: [ 30 | { path: 'payroll', component: AdminPayrollComponent }, 31 | { path: 'vacation', component: AdminVacationComponent }, 32 | ], 33 | }, 34 | { 35 | path: 'lazy', 36 | loadChildren: () => 37 | import('./examples/lazy-module/lazy-module.module').then( 38 | (m) => m.LazyModule 39 | ), 40 | }, 41 | { path: 'params', component: RouteParamsComponent }, 42 | { path: 'params/:id', component: RouteParamsComponent }, 43 | { 44 | path: 'protected', 45 | component: ProtectedComponent, 46 | canActivate: [AuthGuard], 47 | }, 48 | ], 49 | }, 50 | { path: '**', redirectTo: '/home' }, 51 | ]; 52 | 53 | @NgModule({ 54 | imports: [RouterModule.forRoot(routes)], 55 | exports: [RouterModule], 56 | }) 57 | export class AppRoutingModule {} 58 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 13 | {{title}} 14 | 15 | 16 | 24 | 25 | 26 | 27 | 33 | 47 | 48 |

Examples

49 | 63 |
64 | 65 |
66 | 67 |
68 |
69 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { DebugElement } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { of } from 'rxjs'; 6 | import { AppComponent } from './app.component'; 7 | import { AuthService } from './shared/services/auth/auth.service'; 8 | 9 | const noop = () => {}; 10 | 11 | const mockAuthService = { 12 | isAuthenticated$: of(true), 13 | logout: noop, 14 | } 15 | 16 | describe('AppComponent', () => { 17 | let component: AppComponent; 18 | let service: AuthService; 19 | let fixture: ComponentFixture; 20 | let de: DebugElement; 21 | 22 | beforeEach(async(() => { 23 | TestBed.configureTestingModule({ 24 | imports: [ 25 | RouterTestingModule 26 | ], 27 | declarations: [ 28 | AppComponent 29 | ], 30 | providers: [ 31 | { 32 | provide: AuthService, 33 | useValue: mockAuthService 34 | } 35 | ] 36 | }).compileComponents(); 37 | })); 38 | 39 | beforeEach(() => { 40 | fixture = TestBed.createComponent(AppComponent); 41 | component = fixture.componentInstance; 42 | service = TestBed.inject(AuthService); 43 | de = fixture.debugElement; 44 | fixture.detectChanges(); 45 | }); 46 | 47 | it('should create the app', () => { 48 | expect(component).toBeTruthy(); 49 | }); 50 | 51 | it(`should have the correct title'`, () => { 52 | const title = 'Angular 13 Fundamentals'; 53 | expect(component.title).toEqual(title); 54 | }); 55 | 56 | it(`should render an updated title`, () => { 57 | const newTitle = 'Angular 20 Fundamentals'; 58 | const titleElement = de.query(By.css('.title')); 59 | component.title = newTitle; 60 | fixture.detectChanges(); 61 | expect(titleElement.nativeElement.innerText).toBe(newTitle); 62 | }) 63 | 64 | it(`should properly delegate logout responsibility`, () => { 65 | spyOn(service, 'logout').and.callThrough(); 66 | component.logout(); 67 | expect(service.logout).toHaveBeenCalled(); 68 | }) 69 | }); 70 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { shareReplay } from 'rxjs/operators'; 3 | 4 | import { AuthService } from './shared/services/auth/auth.service'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.scss'], 10 | }) 11 | export class AppComponent { 12 | title = 'Angular 13 Fundamentals'; 13 | links = [ 14 | { path: '/home', icon: 'home', title: 'Home' }, 15 | { path: '/courses', icon: 'view_list', title: 'Courses' }, 16 | ]; 17 | 18 | examples = [ 19 | { path: '/examples/params', icon: 'system_update_alt', title: 'Route Params' }, 20 | { path: '/examples/child', icon: 'face', title: 'Child Routes' }, 21 | { path: '/examples/protected', icon: 'vpn_key', title: 'Protected Routes' }, 22 | { path: '/examples/lazy', icon: 'swap_vertical_circle', title: 'Lazy Module' }, 23 | { path: '/examples/create', icon: 'add_box', title: 'Dynamic Components' }, 24 | { path: '/examples/input', icon: 'dashboard_customize', title: 'Custom Input' }, 25 | ] 26 | 27 | isAuthenticated$ = this.authService.isAuthenticated$.pipe(shareReplay(1)); 28 | 29 | constructor(private authService: AuthService) {} 30 | 31 | logout() { 32 | this.authService.logout(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | 7 | import { AdminPayrollComponent } from './examples/child-routes/admin-payroll/admin-payroll.component'; 8 | import { AdminVacationComponent } from './examples/child-routes/admin-vacation/admin-vacation.component'; 9 | import { AdminComponent } from './examples/child-routes/admin.component'; 10 | import { AppRoutingModule } from './app-routing.module'; 11 | import { AppComponent } from './app.component'; 12 | import { CourseDetailsComponent } from './courses/course-details/course-details.component'; 13 | import { CoursesListComponent } from './courses/courses-list/courses-list.component'; 14 | import { CoursesComponent } from './courses/courses.component'; 15 | import { DynamicComponent } from './examples/dynamic-component/dynamic-component.component'; 16 | import { ExamplesComponent } from './examples/examples.component'; 17 | import { HomeComponent } from './home/home.component'; 18 | import { LoginComponent } from './login/login.component'; 19 | import { MaterialModule } from './material.module'; 20 | import { AuthService } from './shared/services/auth/auth.service'; 21 | import { NotificationService } from './shared/services/notifications/notification.service'; 22 | import { CircleComponent } from './examples/dynamic-component/circle/circle.component'; 23 | import { SquareComponent } from './examples/dynamic-component/square/square.component'; 24 | import { TriangleComponent } from './examples/dynamic-component/triangle/triangle.component'; 25 | import { CustomInputComponent } from './examples/custom-input/custom-input.component'; 26 | import { CustomControlComponent } from './examples/custom-input/custom-control/custom-control.component'; 27 | import { RouteParamsComponent } from './examples/route-params/route-params.component'; 28 | import { ProtectedComponent } from './examples/protected/protected.component'; 29 | 30 | @NgModule({ 31 | imports: [ 32 | BrowserModule, 33 | AppRoutingModule, 34 | BrowserAnimationsModule, 35 | FormsModule, 36 | MaterialModule, 37 | HttpClientModule, 38 | ], 39 | declarations: [ 40 | AppComponent, 41 | HomeComponent, 42 | CoursesComponent, 43 | CourseDetailsComponent, 44 | CoursesListComponent, 45 | LoginComponent, 46 | AdminComponent, 47 | AdminPayrollComponent, 48 | AdminVacationComponent, 49 | ExamplesComponent, 50 | DynamicComponent, 51 | CircleComponent, 52 | SquareComponent, 53 | TriangleComponent, 54 | CustomInputComponent, 55 | CustomControlComponent, 56 | RouteParamsComponent, 57 | ProtectedComponent, 58 | ], 59 | providers: [AuthService, NotificationService], 60 | bootstrap: [AppComponent], 61 | }) 62 | export class AppModule {} 63 | -------------------------------------------------------------------------------- /src/app/courses/course-details/course-details.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ originalTitle | titlecase }} 5 | 6 | Select Course 7 | 8 |
9 | 10 | 11 | 19 | 20 | 21 | 29 | 30 |
31 |

{{ currentCourse.percentComplete }}% Complete

32 | 40 | 41 |
42 |
43 | 44 | Favorite 45 | 46 |
47 |
48 | 49 | 57 | 58 | 59 |
60 |
61 | -------------------------------------------------------------------------------- /src/app/courses/course-details/course-details.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/courses/course-details/course-details.component.scss -------------------------------------------------------------------------------- /src/app/courses/course-details/course-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { FormsModule } from '@angular/forms'; 3 | 4 | import { CourseDetailsComponent } from './course-details.component'; 5 | 6 | describe('CourseDetailsComponent', () => { 7 | let component: CourseDetailsComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [FormsModule], 13 | declarations: [ CourseDetailsComponent ] 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(CourseDetailsComponent); 20 | component = fixture.componentInstance; 21 | component.selectedCourse = {} as any; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/app/courses/course-details/course-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; 2 | import { Course } from 'src/app/shared/interfaces/course.interface'; 3 | 4 | @Component({ 5 | selector: 'app-course-details', 6 | templateUrl: './course-details.component.html', 7 | styleUrls: ['./course-details.component.scss'], 8 | }) 9 | export class CourseDetailsComponent implements OnInit { 10 | @Input() set selectedCourse(value: Course) { 11 | if (value?.title) { 12 | this.originalTitle = value.title; 13 | } 14 | this.currentCourse = Object.assign({}, value); 15 | } 16 | @Output() courseSaved = new EventEmitter(); 17 | @Output() courseCancelled = new EventEmitter(); 18 | 19 | originalTitle: string; 20 | currentCourse: Course; 21 | 22 | constructor() {} 23 | 24 | ngOnInit(): void {} 25 | 26 | saveCourse(course: Course) { 27 | this.courseSaved.emit(course); 28 | } 29 | 30 | cancelCourse() { 31 | this.courseCancelled.emit(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/courses/courses-list/courses-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Course List 4 | 5 | 6 | 7 | 11 |

{{ course.title | titlecase }}

12 |

13 | {{ course.description }} 14 |

15 | 22 |
23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /src/app/courses/courses-list/courses-list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/courses/courses-list/courses-list.component.scss -------------------------------------------------------------------------------- /src/app/courses/courses-list/courses-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CoursesListComponent } from './courses-list.component'; 4 | 5 | describe('CoursesListComponent', () => { 6 | let component: CoursesListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CoursesListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CoursesListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/courses/courses-list/courses-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; 2 | import { Course } from 'src/app/shared/interfaces/course.interface'; 3 | 4 | @Component({ 5 | selector: 'app-courses-list', 6 | templateUrl: './courses-list.component.html', 7 | styleUrls: ['./courses-list.component.scss'], 8 | }) 9 | export class CoursesListComponent implements OnInit { 10 | @Input() courses: Course[] = []; 11 | @Output() courseSelected = new EventEmitter(); 12 | @Output() courseDeleted = new EventEmitter(); 13 | 14 | constructor() {} 15 | 16 | ngOnInit(): void {} 17 | 18 | selectCourse(course: Course) { 19 | this.courseSelected.emit(course); 20 | } 21 | 22 | deleteCourse(id: number) { 23 | this.courseDeleted.emit(id); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/courses/courses.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 8 |
9 |
10 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /src/app/courses/courses.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/courses/courses.component.scss -------------------------------------------------------------------------------- /src/app/courses/courses.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { CoursesService } from '../shared/services/courses/courses.service'; 3 | 4 | import { CoursesComponent } from './courses.component'; 5 | 6 | describe('CoursesComponent', () => { 7 | let component: CoursesComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ CoursesComponent ], 13 | providers: [ 14 | { 15 | provide: CoursesService, 16 | useValue: jasmine.createSpyObj('CoursesService', ['getAllCourses', 'updateCourse', 'createCourse', 'deleteCourse']) 17 | } 18 | ] 19 | }) 20 | .compileComponents(); 21 | })); 22 | 23 | beforeEach(() => { 24 | fixture = TestBed.createComponent(CoursesComponent); 25 | component = fixture.componentInstance; 26 | fixture.detectChanges(); 27 | }); 28 | 29 | it('should create', () => { 30 | expect(component).toBeTruthy(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/courses/courses.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { tap } from 'rxjs/operators'; 4 | 5 | import { Course } from '../shared/interfaces/course.interface'; 6 | import { CoursesService } from '../shared/services/courses/courses.service'; 7 | 8 | @Component({ 9 | selector: 'app-courses', 10 | templateUrl: './courses.component.html', 11 | styleUrls: ['./courses.component.scss'] 12 | }) 13 | export class CoursesComponent implements OnInit { 14 | courses$: Observable; 15 | selectedCourse: Course; 16 | 17 | constructor(private coursesService: CoursesService) { } 18 | 19 | ngOnInit(): void { 20 | this.loadCourses(true); 21 | this.resetCourse(); 22 | } 23 | 24 | selectCourse(course: Course) { 25 | this.selectedCourse = course; 26 | } 27 | 28 | loadCourses(displayNotification: boolean) { 29 | this.courses$ = this.coursesService.getAllCourses(displayNotification); 30 | } 31 | 32 | saveCourse(course: Course) { 33 | if (course.id) { 34 | this.updateCourse(course); 35 | } else { 36 | this.createCourse(course); 37 | } 38 | this.resetCourse(); 39 | } 40 | 41 | updateCourse(course: Course) { 42 | this.coursesService.updateCourse(course).pipe( 43 | tap(() => this.loadCourses(false)) 44 | ).subscribe(); 45 | } 46 | 47 | createCourse(course: Course) { 48 | this.coursesService.createCourse(course).pipe( 49 | tap(() => this.loadCourses(false)) 50 | ).subscribe(); 51 | } 52 | 53 | deleteCourse(id: number) { 54 | this.coursesService.deleteCourse(id).pipe( 55 | tap(() => this.loadCourses(false)) 56 | ).subscribe(); 57 | } 58 | 59 | resetCourse() { 60 | const emptyCourse: Course = { 61 | id: null, 62 | title: '', 63 | description: '', 64 | percentComplete: 0, 65 | favorite: false 66 | }; 67 | 68 | this.selectCourse(emptyCourse); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/examples/child-routes/admin-payroll/admin-payroll.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Payroll 4 | 5 | 6 | This is a child route. 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/examples/child-routes/admin-payroll/admin-payroll.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/examples/child-routes/admin-payroll/admin-payroll.component.scss -------------------------------------------------------------------------------- /src/app/examples/child-routes/admin-payroll/admin-payroll.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AdminPayrollComponent } from './admin-payroll.component'; 4 | 5 | describe('AdminPayrollComponent', () => { 6 | let component: AdminPayrollComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ AdminPayrollComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AdminPayrollComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/examples/child-routes/admin-payroll/admin-payroll.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-admin-payroll', 5 | templateUrl: './admin-payroll.component.html', 6 | styleUrls: ['./admin-payroll.component.scss'] 7 | }) 8 | export class AdminPayrollComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/examples/child-routes/admin-vacation/admin-vacation.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Vacation 4 | 5 | 6 | This is a child route. 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/examples/child-routes/admin-vacation/admin-vacation.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/examples/child-routes/admin-vacation/admin-vacation.component.scss -------------------------------------------------------------------------------- /src/app/examples/child-routes/admin-vacation/admin-vacation.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AdminVacationComponent } from './admin-vacation.component'; 4 | 5 | describe('AdminVacationComponent', () => { 6 | let component: AdminVacationComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ AdminVacationComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AdminVacationComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/examples/child-routes/admin-vacation/admin-vacation.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-admin-vacation', 5 | templateUrl: './admin-vacation.component.html', 6 | styleUrls: ['./admin-vacation.component.scss'] 7 | }) 8 | export class AdminVacationComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/examples/child-routes/admin.component.html: -------------------------------------------------------------------------------- 1 | 2 | Child Routes Example 3 | 4 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | Payroll 12 | 13 | 14 | Vacation 15 | 16 | 17 | 18 | 19 |
20 |
21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /src/app/examples/child-routes/admin.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/examples/child-routes/admin.component.scss -------------------------------------------------------------------------------- /src/app/examples/child-routes/admin.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AdminComponent } from './admin.component'; 4 | 5 | describe('AdminComponent', () => { 6 | let component: AdminComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ AdminComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AdminComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/examples/child-routes/admin.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-admin', 5 | templateUrl: './admin.component.html', 6 | styleUrls: ['./admin.component.scss'] 7 | }) 8 | export class AdminComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/examples/custom-input/custom-control/custom-control.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Secure Key

4 |
5 | 6 | 7 | 12 | 13 | 20 | 21 |
22 | -------------------------------------------------------------------------------- /src/app/examples/custom-input/custom-control/custom-control.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/examples/custom-input/custom-control/custom-control.component.scss -------------------------------------------------------------------------------- /src/app/examples/custom-input/custom-control/custom-control.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CustomControlComponent } from './custom-control.component'; 4 | 5 | describe('CustomControlComponent', () => { 6 | let component: CustomControlComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ CustomControlComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CustomControlComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/examples/custom-input/custom-control/custom-control.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, forwardRef, OnDestroy, OnInit } from '@angular/core'; 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 3 | import { timer } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'app-custom-control', 7 | templateUrl: './custom-control.component.html', 8 | styleUrls: ['./custom-control.component.scss'], 9 | providers: [ 10 | { 11 | provide: NG_VALUE_ACCESSOR, 12 | useExisting: forwardRef(() => CustomControlComponent), 13 | multi: true, 14 | }, 15 | ], 16 | }) 17 | export class CustomControlComponent 18 | implements ControlValueAccessor, OnInit, OnDestroy 19 | { 20 | key: string; 21 | source$ = timer(1000, 5000); 22 | sub; 23 | 24 | ngOnInit(): void { 25 | this.sub = this.source$.subscribe((_) => this.generateKey()); 26 | } 27 | 28 | ngOnDestroy(): void { 29 | this.sub.unsubscribe(); 30 | } 31 | 32 | generate() { 33 | return Math.random().toString(36).substr(2, 8); 34 | } 35 | 36 | generateKey() { 37 | this.key = this.generate(); 38 | this.onChange(this.key); 39 | } 40 | 41 | writeValue(key: string) { 42 | this.key = key; 43 | } 44 | 45 | onChange: any = () => {}; 46 | onTouch: any = () => {}; 47 | registerOnChange(fn: any): void { 48 | this.onChange = fn; 49 | } 50 | registerOnTouched(fn: any): void { 51 | this.onTouch = fn; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/examples/custom-input/custom-input.component.html: -------------------------------------------------------------------------------- 1 | 2 | Control Value Accessor 3 | 4 | 5 |
6 | 7 |
8 |
9 |
{{form.value | json}}
10 | -------------------------------------------------------------------------------- /src/app/examples/custom-input/custom-input.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/examples/custom-input/custom-input.component.scss -------------------------------------------------------------------------------- /src/app/examples/custom-input/custom-input.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { FormsModule } from '@angular/forms'; 3 | 4 | import { CustomInputComponent } from './custom-input.component'; 5 | 6 | describe('CustomInputComponent', () => { 7 | let component: CustomInputComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | declarations: [ CustomInputComponent ], 13 | imports: [FormsModule] 14 | }) 15 | .compileComponents(); 16 | }); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(CustomInputComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/examples/custom-input/custom-input.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-custom-input', 5 | templateUrl: './custom-input.component.html', 6 | styleUrls: ['./custom-input.component.scss'] 7 | }) 8 | export class CustomInputComponent implements OnInit { 9 | currentKey = 'currentkey'; 10 | 11 | constructor() { } 12 | 13 | ngOnInit(): void { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/app/examples/dynamic-component/circle/circle.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-circle', 5 | template: ` 6 | 7 | 8 | Circle 9 | 10 | 11 |
12 |
13 |
14 | `, 15 | styles: [ 16 | ` 17 | .circle { 18 | width: 100px; 19 | height: 100px; 20 | background: #ff1a00; 21 | border-radius: 50%; 22 | margin: 0 auto; 23 | } 24 | `, 25 | ], 26 | }) 27 | export class CircleComponent {} 28 | -------------------------------------------------------------------------------- /src/app/examples/dynamic-component/dynamic-component.component.html: -------------------------------------------------------------------------------- 1 | 2 | Create Component Example 3 | 4 | -------------------------------------------------------------------------------- /src/app/examples/dynamic-component/dynamic-component.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/examples/dynamic-component/dynamic-component.component.scss -------------------------------------------------------------------------------- /src/app/examples/dynamic-component/dynamic-component.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DynamicComponent } from './dynamic-component.component'; 4 | 5 | describe('DynamicComponent', () => { 6 | let component: DynamicComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ DynamicComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DynamicComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/examples/dynamic-component/dynamic-component.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewContainerRef } from '@angular/core'; 2 | import { CircleComponent } from './circle/circle.component'; 3 | import { SquareComponent } from './square/square.component'; 4 | import { TriangleComponent } from './triangle/triangle.component'; 5 | 6 | const Components = { 7 | square: SquareComponent, 8 | triangle: TriangleComponent, 9 | circle: CircleComponent, 10 | }; 11 | 12 | @Component({ 13 | selector: 'app-dynamic-component', 14 | templateUrl: './dynamic-component.component.html', 15 | styleUrls: ['./dynamic-component.component.scss'], 16 | }) 17 | export class DynamicComponent implements OnInit { 18 | shapes = [ 19 | { type: 'square' }, 20 | { type: 'square' }, 21 | { type: 'triangle' }, 22 | { type: 'circle' }, 23 | { type: 'square' }, 24 | { type: 'triangle' }, 25 | ]; 26 | 27 | constructor(private viewContainerRef: ViewContainerRef) {} 28 | 29 | ngOnInit(): void { 30 | this.shapes.forEach((shape) => 31 | this.viewContainerRef.createComponent(Components[shape.type]) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/examples/dynamic-component/square/square.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-circle', 5 | template: ` 6 | 7 | 8 | Square 9 | 10 | 11 |
12 |
13 |
14 | `, 15 | styles: [ 16 | ` 17 | .square { 18 | width: 100px; 19 | height: 100px; 20 | background: #4096ee; 21 | margin: 0 auto; 22 | } 23 | `, 24 | ], 25 | }) 26 | export class SquareComponent {} 27 | -------------------------------------------------------------------------------- /src/app/examples/dynamic-component/triangle/triangle.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-circle', 5 | template: ` 6 | 7 | 8 | Triangle 9 | 10 | 11 |
12 |
13 |
14 | `, 15 | styles: [ 16 | ` 17 | .triangle { 18 | width: 0; 19 | height: 0; 20 | border-left: 50px solid transparent; 21 | border-right: 50px solid transparent; 22 | border-bottom: 100px solid #008c00; 23 | margin: 0 auto; 24 | } 25 | `, 26 | ], 27 | }) 28 | export class TriangleComponent {} 29 | -------------------------------------------------------------------------------- /src/app/examples/examples.component.html: -------------------------------------------------------------------------------- 1 | 2 | Examples 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/examples/examples.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/examples/examples.component.scss -------------------------------------------------------------------------------- /src/app/examples/examples.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ExamplesComponent } from './examples.component'; 4 | 5 | describe('ExamplesComponent', () => { 6 | let component: ExamplesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ExamplesComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ExamplesComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/examples/examples.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-examples', 5 | templateUrl: './examples.component.html', 6 | styleUrls: ['./examples.component.scss'] 7 | }) 8 | export class ExamplesComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/examples/lazy-module/lazy-module.component.html: -------------------------------------------------------------------------------- 1 | 2 | This is a lazy module. 3 | 4 | -------------------------------------------------------------------------------- /src/app/examples/lazy-module/lazy-module.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/examples/lazy-module/lazy-module.component.scss -------------------------------------------------------------------------------- /src/app/examples/lazy-module/lazy-module.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LazyModuleComponent } from './lazy-module.component'; 4 | 5 | describe('LazyModuleComponent', () => { 6 | let component: LazyModuleComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ LazyModuleComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LazyModuleComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/examples/lazy-module/lazy-module.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-lazy-module', 5 | templateUrl: './lazy-module.component.html', 6 | styleUrls: ['./lazy-module.component.scss'] 7 | }) 8 | export class LazyModuleComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/examples/lazy-module/lazy-module.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { LazyModuleComponent } from './lazy-module.component'; 4 | import { LazyRoutingModule } from './lazy-routing.module'; 5 | import { MaterialModule } from 'src/app/material.module'; 6 | 7 | @NgModule({ 8 | declarations: [LazyModuleComponent], 9 | imports: [CommonModule, MaterialModule, LazyRoutingModule], 10 | }) 11 | export class LazyModule {} 12 | -------------------------------------------------------------------------------- /src/app/examples/lazy-module/lazy-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { LazyModuleComponent } from './lazy-module.component'; 4 | 5 | const routes: Routes = [ 6 | { path: '', component: LazyModuleComponent } 7 | ]; 8 | 9 | @NgModule({ 10 | imports: [RouterModule.forChild(routes)], 11 | exports: [RouterModule] 12 | }) 13 | export class LazyRoutingModule { } 14 | -------------------------------------------------------------------------------- /src/app/examples/protected/protected.component.html: -------------------------------------------------------------------------------- 1 | 2 | Protected Route Example 3 | 4 | 5 | 6 | 7 |

If you can see this, click the user icon in the corner to log out and then try go to this route again.

8 |

You can log back in by just submitting the empty login form.

9 |
10 |
11 | 12 | -------------------------------------------------------------------------------- /src/app/examples/protected/protected.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/examples/protected/protected.component.scss -------------------------------------------------------------------------------- /src/app/examples/protected/protected.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProtectedComponent } from './protected.component'; 4 | 5 | describe('ProtectedComponent', () => { 6 | let component: ProtectedComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ProtectedComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProtectedComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/examples/protected/protected.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-protected', 5 | templateUrl: './protected.component.html', 6 | styleUrls: ['./protected.component.scss'] 7 | }) 8 | export class ProtectedComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/examples/route-params/route-params.component.html: -------------------------------------------------------------------------------- 1 | 2 | Route Params Example 3 | 4 | 5 |
6 |
7 | 8 | 9 | 10 | Course Outline 11 | 12 | 13 | 14 | 15 | 18 |

{{ lesson.title | titlecase }}

19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | Current Lesson 28 | 29 | 30 | 31 | 32 |

{{ currentLesson.title | titlecase }}

33 |
34 |
35 | Select a lesson 36 |
37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /src/app/examples/route-params/route-params.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/examples/route-params/route-params.component.scss -------------------------------------------------------------------------------- /src/app/examples/route-params/route-params.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | import { RouteParamsComponent } from './route-params.component'; 5 | 6 | describe('RouteParamsComponent', () => { 7 | let component: RouteParamsComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | declarations: [RouteParamsComponent], 13 | imports: [RouterTestingModule], 14 | }).compileComponents(); 15 | }); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(RouteParamsComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/examples/route-params/route-params.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-route-params', 6 | templateUrl: './route-params.component.html', 7 | styleUrls: ['./route-params.component.scss'], 8 | }) 9 | export class RouteParamsComponent implements OnInit { 10 | currentId = null; 11 | currentLesson = null; 12 | 13 | lessons = [ 14 | { id: '1', title: 'Hello Angular' }, 15 | { id: '2', title: 'Component Fundamentals' }, 16 | { id: '3', title: 'Template Driven Forms' }, 17 | { id: '4', title: 'Angular Services' }, 18 | { id: '5', title: 'Server Communication' }, 19 | { id: '6', title: 'Component Driven Architecture' }, 20 | { id: '7', title: 'Angular Routing' }, 21 | { id: '8', title: 'Unit Testing Fundamentals' }, 22 | ]; 23 | 24 | constructor(private route: ActivatedRoute, private router: Router) {} 25 | 26 | ngOnInit() { 27 | this.route.paramMap.subscribe(params => { 28 | this.currentId = params.get('id'); 29 | if(this.currentId) { 30 | this.setCurrentLesson(this.currentId); 31 | } 32 | }) 33 | } 34 | 35 | setCurrentLesson(id) { 36 | this.currentLesson = this.lessons.find(lesson => lesson.id === id); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Course Outline 5 | 6 | 7 | 8 | 9 | 10 |

{{ lesson.title | titlecase }}

11 |
12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /src/app/home/home.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/home/home.component.scss -------------------------------------------------------------------------------- /src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ HomeComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HomeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home', 5 | templateUrl: './home.component.html', 6 | styleUrls: ['./home.component.scss'] 7 | }) 8 | export class HomeComponent implements OnInit { 9 | currentCourseId = '1'; 10 | courseLessons = { 11 | '1': [ 12 | { title: 'Hello Angular' }, 13 | { title: 'Component Fundamentals' }, 14 | { title: 'Template Driven Forms' }, 15 | { title: 'Angular Services' }, 16 | { title: 'Server Communication' }, 17 | { title: 'Component Driven Architecture' }, 18 | { title: 'Angular Routing' }, 19 | { title: 'Unit Testing Fundamentals' }, 20 | ] 21 | }; 22 | 23 | constructor() { } 24 | 25 | ngOnInit(): void { 26 | } 27 | 28 | getCourseLessons(courseId: string) { 29 | return this.courseLessons[courseId]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/login/login.component.html: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /src/app/login/login.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/app/login/login.component.scss -------------------------------------------------------------------------------- /src/app/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { AuthService } from '../shared/services/auth/auth.service'; 3 | 4 | import { LoginComponent } from './login.component'; 5 | 6 | describe('LoginComponent', () => { 7 | let component: LoginComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ LoginComponent ], 13 | providers: [ 14 | { 15 | provide: AuthService, 16 | useValue: jasmine.createSpyObj('AuthService', ['login']) 17 | } 18 | ] 19 | }) 20 | .compileComponents(); 21 | })); 22 | 23 | beforeEach(() => { 24 | fixture = TestBed.createComponent(LoginComponent); 25 | component = fixture.componentInstance; 26 | fixture.detectChanges(); 27 | }); 28 | 29 | it('should create', () => { 30 | expect(component).toBeTruthy(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { UserInfo } from '../shared/interfaces/auth.interface'; 4 | import { AuthService } from '../shared/services/auth/auth.service'; 5 | 6 | @Component({ 7 | selector: 'app-login', 8 | templateUrl: './login.component.html', 9 | styleUrls: ['./login.component.scss'], 10 | }) 11 | export class LoginComponent implements OnInit { 12 | userInfo: UserInfo = { 13 | email: '', 14 | password: '', 15 | }; 16 | 17 | constructor(private authService: AuthService) {} 18 | 19 | ngOnInit(): void {} 20 | 21 | login(userInfo: UserInfo) { 22 | this.authService.login(userInfo); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { MatButtonModule } from '@angular/material/button'; 3 | import { MatButtonToggleModule } from '@angular/material/button-toggle'; 4 | import { MatCardModule } from '@angular/material/card'; 5 | import { MatCheckboxModule } from '@angular/material/checkbox'; 6 | import { MatFormFieldModule } from '@angular/material/form-field'; 7 | import { MatGridListModule } from '@angular/material/grid-list'; 8 | import { MatIconModule } from '@angular/material/icon'; 9 | import { MatInputModule } from '@angular/material/input'; 10 | import { MatListModule } from '@angular/material/list'; 11 | import { MatMenuModule } from '@angular/material/menu'; 12 | import { MatSelectModule } from '@angular/material/select'; 13 | import { MatSidenavModule } from '@angular/material/sidenav'; 14 | import { MatSliderModule } from '@angular/material/slider'; 15 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 16 | import { MatTableModule } from '@angular/material/table'; 17 | import { MatToolbarModule } from '@angular/material/toolbar'; 18 | 19 | @NgModule({ 20 | exports: [ 21 | MatButtonModule, 22 | MatCardModule, 23 | MatCheckboxModule, 24 | MatFormFieldModule, 25 | MatGridListModule, 26 | MatIconModule, 27 | MatInputModule, 28 | MatListModule, 29 | MatMenuModule, 30 | MatSelectModule, 31 | MatSidenavModule, 32 | MatSliderModule, 33 | MatSnackBarModule, 34 | MatTableModule, 35 | MatToolbarModule, 36 | MatButtonToggleModule, 37 | ], 38 | }) 39 | export class MaterialModule {} 40 | -------------------------------------------------------------------------------- /src/app/shared/guards/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { Router } from '@angular/router'; 3 | import { of } from 'rxjs'; 4 | 5 | import { AuthService } from '../services/auth/auth.service'; 6 | import { AuthGuard } from './auth.guard'; 7 | 8 | const mockAuthService = { 9 | isAuthenticated$: of(true) 10 | }; 11 | 12 | describe('AuthGuard', () => { 13 | let guard: AuthGuard; 14 | 15 | beforeEach(() => { 16 | TestBed.configureTestingModule({ 17 | providers: [ 18 | { 19 | provide: AuthService, 20 | useValue: mockAuthService 21 | }, 22 | { 23 | provide: Router, 24 | useValue: jasmine.createSpyObj('Router', ['navigateByUrl']) 25 | } 26 | ] 27 | }); 28 | guard = TestBed.inject(AuthGuard); 29 | }); 30 | 31 | it('should be created', () => { 32 | expect(guard).toBeTruthy(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/app/shared/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivate, 5 | Router, 6 | RouterStateSnapshot, 7 | UrlTree, 8 | } from '@angular/router'; 9 | import { Observable } from 'rxjs'; 10 | import { tap } from 'rxjs/operators'; 11 | 12 | import { AuthService } from '../services/auth/auth.service'; 13 | 14 | @Injectable({ 15 | providedIn: 'root', 16 | }) 17 | export class AuthGuard implements CanActivate { 18 | constructor(private authService: AuthService, private router: Router) {} 19 | 20 | canActivate( 21 | next: ActivatedRouteSnapshot, 22 | state: RouterStateSnapshot 23 | ): 24 | | Observable 25 | | Promise 26 | | boolean 27 | | UrlTree { 28 | return this.authService.isAuthenticated$.pipe( 29 | tap((isAuthenticated) => { 30 | if (!isAuthenticated) { 31 | this.router.navigateByUrl('/login'); 32 | } 33 | 34 | return isAuthenticated; 35 | }) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/auth.interface.ts: -------------------------------------------------------------------------------- 1 | export interface UserInfo { 2 | email: string; 3 | password: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/course.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Course { 2 | id: number; 3 | title: string; 4 | description: string; 5 | percentComplete: number; 6 | favorite: boolean; 7 | } -------------------------------------------------------------------------------- /src/app/shared/services/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { AuthService } from './auth.service'; 5 | 6 | describe('AuthService', () => { 7 | let service: AuthService; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | providers: [ 12 | { 13 | provide: Router, 14 | useValue: jasmine.createSpyObj('Router', ['navigateByUrl']) 15 | } 16 | ] 17 | }); 18 | service = TestBed.inject(AuthService); 19 | }); 20 | 21 | it('should be created', () => { 22 | expect(service).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/shared/services/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { BehaviorSubject } from 'rxjs'; 4 | 5 | import { UserInfo } from '../../interfaces/auth.interface'; 6 | 7 | const AUTHENTICATION_KEY = 'workshop:authenticated'; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class AuthService { 13 | private isAuthenticated = new BehaviorSubject(this.getIsAuthenticated() || false); 14 | isAuthenticated$ = this.isAuthenticated.asObservable(); 15 | 16 | constructor(private router: Router) { } 17 | 18 | login(userInfo: UserInfo) { 19 | this.setIsAuthenticated(true); 20 | this.isAuthenticated.next(true); 21 | this.router.navigateByUrl('/courses'); 22 | } 23 | 24 | logout() { 25 | this.setIsAuthenticated(false); 26 | this.isAuthenticated.next(false); 27 | this.router.navigateByUrl('/login'); 28 | } 29 | 30 | private getIsAuthenticated(): boolean { 31 | return JSON.parse(localStorage.getItem(AUTHENTICATION_KEY)); 32 | } 33 | 34 | private setIsAuthenticated(isAuthenticated: boolean) { 35 | localStorage.setItem(AUTHENTICATION_KEY, JSON.stringify(isAuthenticated)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/shared/services/courses/courses.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { TestBed } from '@angular/core/testing'; 3 | 4 | import { CoursesService } from './courses.service'; 5 | import { NotificationService } from '../notifications/notification.service'; 6 | 7 | describe('CoursesService', () => { 8 | let service: CoursesService; 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({ 12 | providers: [ 13 | { 14 | provide: HttpClient, 15 | useValue: jasmine.createSpyObj('HttpClient', ['get', 'post', 'put', 'delete']) 16 | }, 17 | { 18 | provide: NotificationService, 19 | useValue: jasmine.createSpyObj('NotificationService', ['notify']) 20 | } 21 | ] 22 | }); 23 | service = TestBed.inject(CoursesService); 24 | }); 25 | 26 | it('should be created', () => { 27 | expect(service).toBeTruthy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/app/shared/services/courses/courses.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | import { Course } from '../../interfaces/course.interface'; 5 | import { NotificationService } from '../notifications/notification.service'; 6 | 7 | const BASE_URL = 'http://localhost:3000'; 8 | 9 | @Injectable({ 10 | providedIn: 'root', 11 | }) 12 | export class CoursesService { 13 | model = 'courses'; 14 | 15 | constructor( 16 | private http: HttpClient, 17 | private notificationService: NotificationService 18 | ) {} 19 | 20 | getAllCourses(displayNotification: boolean) { 21 | if (displayNotification) { 22 | this.notificationService.notify('Get All Courses HTTP Call'); 23 | } 24 | return this.http.get(this.getUrl()); 25 | } 26 | 27 | createCourse(course: Course) { 28 | this.notificationService.notify('Create Course HTTP Call'); 29 | return this.http.post(this.getUrl(), course); 30 | } 31 | 32 | updateCourse(course: Course) { 33 | this.notificationService.notify('Update Course HTTP Call'); 34 | return this.http.put(this.getUrlWithID(course.id), course); 35 | } 36 | 37 | deleteCourse(id: number) { 38 | this.notificationService.notify('Delete Course HTTP Call'); 39 | return this.http.delete(this.getUrlWithID(id)); 40 | } 41 | 42 | private getUrl() { 43 | return `${BASE_URL}/${this.model}`; 44 | } 45 | 46 | private getUrlWithID(id) { 47 | return `${this.getUrl()}/${id}`; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/shared/services/interceptors/http-interceptor.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { HttpInterceptorService } from './http-interceptor.service'; 4 | 5 | describe('HttpInterceptorService', () => { 6 | let service: HttpInterceptorService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(HttpInterceptorService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/shared/services/interceptors/http-interceptor.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpEvent, 3 | HttpHandler, 4 | HttpInterceptor, 5 | HttpRequest, 6 | } from '@angular/common/http'; 7 | import { Injectable } from '@angular/core'; 8 | import { Observable } from 'rxjs'; 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class HttpInterceptorService implements HttpInterceptor { 14 | constructor() {} 15 | 16 | intercept( 17 | req: HttpRequest, 18 | next: HttpHandler 19 | ): Observable> { 20 | console.log('REQUEST INTERCEPTED', req); 21 | return next.handle(req); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/shared/services/notifications/notification.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { MatSnackBar } from '@angular/material/snack-bar'; 3 | 4 | import { NotificationService } from './notification.service'; 5 | 6 | describe('NotificationService', () => { 7 | let service: NotificationService; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | providers: [ 12 | { 13 | provide: MatSnackBar, 14 | useValue: jasmine.createSpyObj('MatSnackBar', ['open']) 15 | } 16 | ] 17 | }); 18 | service = TestBed.inject(NotificationService); 19 | }); 20 | 21 | it('should be created', () => { 22 | expect(service).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/shared/services/notifications/notification.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MatSnackBar } from '@angular/material/snack-bar'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class NotificationService { 8 | constructor(private snackBar: MatSnackBar) {} 9 | 10 | notify(message: string) { 11 | this.snackBar.open(message, 'CLOSE', { 12 | duration: 3000, 13 | horizontalPosition: 'right', 14 | verticalPosition: 'top', 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/screenshots/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/assets/screenshots/demo.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular13-fundamentals-workshop/29f023dbba3be621e8a3b3fc14ae7618d2ee6406/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Fundamentals Workshop 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 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 recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | // Custom Theming for Angular Material 2 | // For more information: https://material.angular.io/guide/theming 3 | @use "@angular/material" as mat; 4 | // Plus imports for other components in your app. 5 | 6 | // Include the common styles for Angular Material. We include this here so that you only 7 | // have to load a single css file for Angular Material in your app. 8 | // Be sure that you only ever include this mixin once! 9 | @include mat.core(); 10 | 11 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 12 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 13 | // hue. Available color palettes: https://material.io/design/color/ 14 | $angular13-fundamentals-workshop-primary: mat.define-palette( 15 | mat.$indigo-palette 16 | ); 17 | $angular13-fundamentals-workshop-accent: mat.define-palette( 18 | mat.$pink-palette, 19 | A200, 20 | A100, 21 | A400 22 | ); 23 | 24 | // The warn palette is optional (defaults to red). 25 | $angular13-fundamentals-workshop-warn: mat.define-palette(mat.$red-palette); 26 | 27 | // Create the theme object. A theme consists of configurations for individual 28 | // theming systems such as "color" or "typography". 29 | $angular13-fundamentals-workshop-theme: mat.define-light-theme( 30 | ( 31 | color: ( 32 | primary: $angular13-fundamentals-workshop-primary, 33 | accent: $angular13-fundamentals-workshop-accent, 34 | warn: $angular13-fundamentals-workshop-warn, 35 | ), 36 | ) 37 | ); 38 | 39 | // Include theme styles for core and each component used in your app. 40 | // Alternatively, you can import and @include the theme mixins for each component 41 | // that you are using. 42 | @include mat.all-component-themes($angular13-fundamentals-workshop-theme); 43 | 44 | html { 45 | height: 100%; 46 | } 47 | 48 | body { 49 | margin: 0; 50 | font-family: Roboto, sans-serif; 51 | height: 100%; 52 | display: flex; 53 | } 54 | 55 | p { 56 | margin: 16px; 57 | } 58 | 59 | .icon-20 { 60 | font-size: 20px; 61 | } 62 | 63 | * { 64 | -webkit-font-smoothing: antialiased; 65 | -moz-osx-font-smoothing: grayscale; 66 | } 67 | 68 | .container { 69 | margin: 10px; 70 | flex-wrap: wrap; 71 | 72 | .mat-card-header-text { 73 | margin-left: 0; 74 | margin-bottom: 10px; 75 | border-bottom: 1px solid #ffd740; 76 | } 77 | 78 | .mat-toolbar { 79 | margin-bottom: 10px; 80 | } 81 | 82 | .mat-card.shape { 83 | width: 200px; 84 | } 85 | } 86 | 87 | .container [class*="col"] { 88 | padding: 10px; 89 | flex: 1; 90 | } 91 | 92 | 93 | .header { 94 | padding-bottom: 10px; 95 | color: #673ab7; 96 | border-bottom: 2px solid #673ab7; 97 | } 98 | 99 | app-root { 100 | display: flex; 101 | flex-direction: column; 102 | flex: 1; 103 | } 104 | 105 | mat-toolbar { 106 | box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 107 | 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); 108 | z-index: 1; 109 | } 110 | 111 | mat-sidenav { 112 | box-shadow: 3px 0 6px rgba(0, 0, 0, 0.24); 113 | width: 200px; 114 | padding-top: 10px; 115 | } 116 | 117 | mat-card:not(.dashboard-card) { 118 | margin-bottom: 20px; 119 | } 120 | 121 | .mat-sidenav-container { 122 | background: #f5f5f5; 123 | flex: 1; 124 | 125 | .mat-divider { 126 | margin-top: 10px; 127 | margin-bottom: 10px; 128 | } 129 | 130 | .mat-subheader { 131 | margin: 0px; 132 | padding-top: 5px; 133 | padding-bottom: 5px; 134 | } 135 | } 136 | 137 | .fill-remaining-space { 138 | flex: 1 1 auto; 139 | } 140 | 141 | .full-width { 142 | width: 100%; 143 | } 144 | 145 | .container-component { 146 | display: flex; 147 | justify-content: space-around; 148 | 149 | .list { 150 | width: 49%; 151 | } 152 | 153 | .details { 154 | width: 49%; 155 | } 156 | } 157 | 158 | .courses-container { 159 | display: flex; 160 | justify-content: space-around; 161 | 162 | .courses-list { 163 | width: 49%; 164 | } 165 | 166 | .course-details { 167 | width: 49%; 168 | } 169 | } 170 | 171 | .login-container { 172 | display: flex; 173 | align-items: center; 174 | justify-content: center; 175 | height: calc(100vh - 64px); 176 | 177 | mat-card { 178 | width: 400px; 179 | } 180 | } 181 | 182 | .dashboard-card { 183 | position: absolute !important; 184 | top: 15px; 185 | left: 15px; 186 | right: 15px; 187 | bottom: 15px; 188 | 189 | mat-card-header { 190 | justify-content: center; 191 | } 192 | } 193 | 194 | .dashboard-card-content { 195 | text-align: center; 196 | } 197 | 198 | .more-button { 199 | position: absolute; 200 | top: 5px; 201 | right: 10px; 202 | } 203 | 204 | .grid-container { 205 | margin: 20px; 206 | } 207 | 208 | .list-container { 209 | margin: 20px; 210 | 211 | .full-width-table { 212 | width: 100%; 213 | } 214 | 215 | tr { 216 | -webkit-transition: background-color 300ms; 217 | -moz-transition: background-color 300ms; 218 | -ms-transition: background-color 300ms; 219 | -o-transition: background-color 300ms; 220 | transition: background-color 300ms; 221 | } 222 | 223 | tr:not(.mat-header-row):hover { 224 | background: whitesmoke; 225 | cursor: pointer; 226 | } 227 | } 228 | 229 | .details-container { 230 | mat-form-field { 231 | width: 100%; 232 | } 233 | 234 | mat-card-header { 235 | display: flex; 236 | justify-content: space-between; 237 | align-items: center; 238 | } 239 | } 240 | 241 | mat-list-item:not(:first-of-type) { 242 | border-top: 1px solid #efefef; 243 | } 244 | 245 | mat-form-field { 246 | width: 100%; 247 | } 248 | 249 | mat-list-item:hover { 250 | cursor: pointer; 251 | background: whitesmoke; 252 | } 253 | 254 | mat-list-item:not(:first-of-type) { 255 | border-top: 1px solid #efefef; 256 | } 257 | 258 | .symbol { 259 | color: #777; 260 | } 261 | 262 | mat-card-actions { 263 | margin-bottom: 0; 264 | } 265 | 266 | .full-width { 267 | width: 100%; 268 | } 269 | 270 | .nav-link { 271 | color: rgba(0, 0, 0, 0.54); 272 | display: flex !important; 273 | align-items: center; 274 | padding-top: 5px; 275 | padding-bottom: 5px; 276 | } 277 | 278 | // UI LOGIN 279 | .login { 280 | height: 100%; 281 | 282 | .background { 283 | position: fixed; 284 | left: 0; 285 | right: 0; 286 | z-index: 1; 287 | display: block; 288 | // background: url(assets/background.jpg) no-repeat center center fixed; 289 | // background-size: cover; 290 | height: 100%; 291 | overflow: hidden; 292 | } 293 | 294 | .container { 295 | position: fixed; 296 | left: 0; 297 | right: 0; 298 | top: 0; 299 | bottom: 0; 300 | z-index: 100; 301 | 302 | .card-wrapper { 303 | margin: auto; 304 | display: flex; 305 | justify-content: center; 306 | } 307 | } 308 | 309 | mat-card { 310 | box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 311 | 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); 312 | 313 | mat-card-header { 314 | margin-bottom: 10px; 315 | } 316 | 317 | mat-form-field { 318 | width: 100%; 319 | } 320 | } 321 | 322 | .ig-xpro2 { 323 | -webkit-filter: contrast(1.3) brightness(0.8) sepia(0.3) saturate(1.5) 324 | hue-rotate(-20deg); 325 | filter: contrast(1.3) brightness(0.8) sepia(0.3) saturate(1.5) 326 | hue-rotate(-20deg); 327 | } 328 | 329 | .ig-willow { 330 | -webkit-filter: saturate(0.02) contrast(0.85) brightness(1.2) sepia(0.02); 331 | filter: saturate(0.02) contrast(0.85) brightness(1.2) sepia(0.02); 332 | } 333 | 334 | .ig-walden { 335 | -webkit-filter: sepia(0.35) contrast(0.9) brightness(1.1) hue-rotate(-10deg) 336 | saturate(1.5); 337 | filter: sepia(0.35) contrast(0.9) brightness(1.1) hue-rotate(-10deg) 338 | saturate(1.5); 339 | } 340 | 341 | .ig-valencia { 342 | -webkit-filter: sepia(0.15) saturate(1.5) contrast(0.9); 343 | filter: sepia(0.15) saturate(1.5) contrast(0.9); 344 | } 345 | 346 | .ig-toaster { 347 | -webkit-filter: sepia(0.4) saturate(2.5) hue-rotate(-30deg) contrast(0.67); 348 | -filter: sepia(0.4) saturate(2.5) hue-rotate(-30deg) contrast(0.67); 349 | } 350 | 351 | .ig-sutro { 352 | -webkit-filter: brightness(0.75) contrast(1.3) sepia(0.5) hue-rotate(-25deg); 353 | filter: brightness(0.75) contrast(1.3) sepia(0.5) hue-rotate(-25deg); 354 | } 355 | 356 | .ig-sierra { 357 | -webkit-filter: contrast(0.8) saturate(1.2) sepia(0.15); 358 | filter: contrast(0.8) saturate(1.2) sepia(0.15); 359 | } 360 | 361 | .ig-rise { 362 | -webkit-filter: saturate(1.4) sepia(0.25) hue-rotate(-15deg) contrast(0.8) 363 | brightness(1.1); 364 | filter: saturate(1.4) sepia(0.25) hue-rotate(-15deg) contrast(0.8) 365 | brightness(1.1); 366 | } 367 | 368 | .ig-nashville { 369 | -webkit-filter: sepia(0.4) saturate(1.5) contrast(0.9) brightness(1.1) 370 | hue-rotate(-15deg); 371 | filter: sepia(0.4) saturate(1.5) contrast(0.9) brightness(1.1) 372 | hue-rotate(-15deg); 373 | } 374 | 375 | .ig-mayfair { 376 | -webkit-filter: saturate(1.4) contrast(1.1); 377 | filter: saturate(1.4) contrast(1.1); 378 | } 379 | 380 | .ig-lofi { 381 | filter: contrast(1.4) brightness(0.9) sepia(0.05); 382 | -webkit-filter: contrast(1.4) brightness(0.9) sepia(0.05); 383 | } 384 | 385 | .ig-kelvin { 386 | filter: sepia(0.4) saturate(2.4) brightness(1.3) contrast(1); 387 | -webkit-filter: sepia(0.4) saturate(2.4) brightness(1.3) contrast(1); 388 | } 389 | 390 | .ig-inkwell { 391 | -webkit-filter: grayscale(1) brightness(1.2) contrast(1.05); 392 | filter: grayscale(1) brightness(1.2) contrast(1.05); 393 | } 394 | 395 | .ig-hudson { 396 | -webkit-filter: contrast(1.2) brightness(0.9) hue-rotate(-10deg); 397 | filter: contrast(1.2) brightness(0.9) hue-rotate(-10deg); 398 | } 399 | 400 | .hefe { 401 | -webkit-filter: contrast(1.3) sepia(0.3) saturate(1.3) hue-rotate(-10deg) 402 | brightness(0.95); 403 | filter: contrast(1.3) sepia(0.3) saturate(1.3) hue-rotate(-10deg) 404 | brightness(0.95); 405 | } 406 | 407 | .ig-earlybird { 408 | -webkit-filter: sepia(0.4) saturate(1.6) contrast(1.1) brightness(0.9) 409 | hue-rotate(-10deg); 410 | filter: sepia(0.4) saturate(1.6) contrast(1.1) brightness(0.9) 411 | hue-rotate(-10deg); 412 | } 413 | 414 | .ig-brannan { 415 | -webkit-filter: sepia(0.5) contrast(1.4); 416 | filter: sepia(0.5) contrast(1.4); 417 | } 418 | 419 | .ig-amaro { 420 | -webkit-filter: hue-rotate(-10deg) contrast(0.9) brightness(1.1) 421 | saturate(1.5); 422 | filter: hue-rotate(-10deg) contrast(0.9) brightness(1.1) saturate(1.5); 423 | } 424 | 425 | .ig-1977 { 426 | -webkit-filter: sepia(0.5) hue-rotate(-30deg) saturate(1.2) contrast(0.8); 427 | filter: sepia(0.5) hue-rotate(-30deg) saturate(1.2) contrast(0.8); 428 | } 429 | } 430 | 431 | // UI TOOLBAR 432 | mat-toolbar { 433 | box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 434 | 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); 435 | z-index: 900; 436 | 437 | > .mat-mini-fab { 438 | margin-right: 10px; 439 | } 440 | 441 | .spacer { 442 | flex: 1 1 auto; 443 | } 444 | 445 | .title { 446 | vertical-align: middle; 447 | } 448 | 449 | .logo { 450 | margin-left: 20px; 451 | 452 | img { 453 | vertical-align: middle; 454 | width: 100px; 455 | height: auto; 456 | } 457 | } 458 | } 459 | 460 | html, 461 | body { 462 | height: 100%; 463 | } 464 | body { 465 | margin: 0; 466 | font-family: Roboto, "Helvetica Neue", sans-serif; 467 | } 468 | -------------------------------------------------------------------------------- /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/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting(), 21 | { teardown: { destroyAfterEach: true } }, 22 | ); 23 | 24 | // Then we find all the tests. 25 | const context = require.context('./', true, /\.spec\.ts$/); 26 | // And load the modules. 27 | context.keys().map(context); 28 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": false, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "es2017", 20 | "module": "es2020", 21 | "lib": [ 22 | "es2020", 23 | "dom" 24 | ] 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true, 31 | "strictPropertyInitialization": false 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------