├── src ├── assets │ ├── .gitkeep │ └── angular.jpg ├── app │ ├── app.component.html │ ├── modules │ │ ├── core │ │ │ ├── loading │ │ │ │ ├── loading.component.html │ │ │ │ ├── loading.component.scss │ │ │ │ ├── loading.component.ts │ │ │ │ └── loading.component.spec.ts │ │ │ ├── models │ │ │ │ └── group-interface.ts │ │ │ ├── page-not-found │ │ │ │ ├── page-not-found.component.scss │ │ │ │ ├── page-not-found.component.html │ │ │ │ ├── page-not-found.component.ts │ │ │ │ └── page-not-found.component.spec.ts │ │ │ ├── filter-actives.pipe.spec.ts │ │ │ ├── unless │ │ │ │ ├── unless.directive.spec.ts │ │ │ │ └── unless.directive.ts │ │ │ ├── index.ts │ │ │ ├── filter-actives.pipe.ts │ │ │ ├── validators.ts │ │ │ ├── highlight │ │ │ │ ├── highlight.directive.ts │ │ │ │ └── highlight.directive.spec.ts │ │ │ └── core.module.ts │ │ ├── index.ts │ │ └── login │ │ │ ├── index.ts │ │ │ ├── login-form.model.ts │ │ │ ├── resolver.service.spec.ts │ │ │ ├── resolver.service.ts │ │ │ ├── auth.guard.spec.ts │ │ │ ├── group.service.ts │ │ │ ├── login.component.scss │ │ │ ├── auth.guard.ts │ │ │ ├── login.service.ts │ │ │ ├── login.module.ts │ │ │ ├── group.service.spec.ts │ │ │ ├── login.component.ts │ │ │ ├── login.component.html │ │ │ ├── login.component.spec.ts │ │ │ └── login.service.spec.ts │ ├── pages │ │ ├── home │ │ │ ├── pages │ │ │ │ ├── list │ │ │ │ │ ├── list.component.scss │ │ │ │ │ ├── list.component.spec.ts │ │ │ │ │ ├── list.component.html │ │ │ │ │ └── list.component.ts │ │ │ │ └── dash │ │ │ │ │ ├── dash.component.ts │ │ │ │ │ ├── dash.component.spec.ts │ │ │ │ │ ├── dash.component.scss │ │ │ │ │ └── dash.component.html │ │ │ ├── home-routing.module.ts │ │ │ ├── home.component.scss │ │ │ ├── home.component.spec.ts │ │ │ ├── home.component.ts │ │ │ ├── home.module.ts │ │ │ └── home.component.html │ │ ├── admin │ │ │ ├── admin.service.spec.ts │ │ │ ├── admin-routing.module.ts │ │ │ ├── admin.component.spec.ts │ │ │ ├── admin.service.ts │ │ │ ├── admin.module.ts │ │ │ ├── admin.component.scss │ │ │ ├── admin.component.ts │ │ │ └── admin.component.html │ │ └── register │ │ │ ├── register.service.spec.ts │ │ │ ├── register-routing.module.ts │ │ │ ├── register.service.ts │ │ │ ├── register.component.spec.ts │ │ │ ├── register.component.scss │ │ │ ├── register.module.ts │ │ │ ├── register.component.ts │ │ │ └── register.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.component.spec.ts │ ├── app.module.ts │ └── app-routing.module.ts ├── favicon.ico ├── styles.scss ├── tsconfig.app.json ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── tsconfig.spec.json ├── tslint.json ├── browserslist ├── main.ts ├── index.html ├── test.ts ├── karma.conf.js └── polyfills.ts ├── ci ├── unit-tests ├── release └── deploy ├── stubs ├── user.json ├── users.json ├── groups.json └── config.yaml ├── docker ├── Dockerfile └── package.json ├── README.md ├── e2e ├── tsconfig.e2e.json ├── protractor-ci.conf.js ├── src │ ├── app.po.ts │ ├── login.e2e-spec.ts │ ├── login.po.ts │ └── app.e2e-spec.ts └── protractor.conf.js ├── .editorconfig ├── tsconfig.json ├── .gitignore ├── .travis.yml ├── server └── server.js ├── docs └── Cuestionario1.md ├── package.json ├── tslint.json └── angular.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ci/unit-tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm run test -- --watch=false 4 | 5 | -------------------------------------------------------------------------------- /src/app/modules/core/loading/loading.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export * from './login'; 3 | -------------------------------------------------------------------------------- /stubs/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "fullName": "Administrator", 4 | "email": "admin@app.com" 5 | } -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YerkoPalma/escalando-aplicaciones-con-angular/master/src/favicon.ico -------------------------------------------------------------------------------- /src/assets/angular.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YerkoPalma/escalando-aplicaciones-con-angular/master/src/assets/angular.jpg -------------------------------------------------------------------------------- /src/app/modules/core/models/group-interface.ts: -------------------------------------------------------------------------------- 1 | export interface Group { 2 | id: string; 3 | value: string; 4 | active: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/modules/core/page-not-found/page-not-found.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | align-items: center; 3 | justify-content: center; 4 | display: flex; 5 | flex-grow: 1; 6 | } -------------------------------------------------------------------------------- /src/app/pages/home/pages/list/list.component.scss: -------------------------------------------------------------------------------- 1 | .mat-card-avatar { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | table { 8 | width: 100%; 9 | } -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, body { height: 100%; } 4 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 5 | -------------------------------------------------------------------------------- /src/app/modules/core/page-not-found/page-not-found.component.html: -------------------------------------------------------------------------------- 1 | 2 | 404 3 | 4 |

Page not found!

5 |
6 |
-------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/theming'; 2 | 3 | $primary: mat-palette($mat-deep-purple); 4 | 5 | :host { 6 | background-color: mat-color($primary, lighter); 7 | display: flex; 8 | height: 100%; 9 | } -------------------------------------------------------------------------------- /src/app/modules/login/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthGuard } from './auth.guard'; 2 | export { LoginService } from './login.service'; 3 | 4 | export { LoginComponent } from './login.component'; 5 | export { LoginModule } from './login.module'; 6 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | 3 | ARG app_version 4 | ENV APP_VERSION=$app_version 5 | 6 | COPY . . 7 | 8 | RUN npm install --save express 9 | 10 | RUN npm version ${APP_VERSION} --no-git-tag-version 11 | 12 | CMD node server.js -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'web-app'; 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Escalando Aplicaciones con Angular 2 | 3 | #Subir a Producción 4 | 5 | 6 | travis.org 7 | 8 | $HEROKU_KEY 9 | $HEROKU_APP_NAME 10 | $HEROKU_OWNER_EMAIL 11 | 12 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.0.6. 13 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /src/app/modules/core/filter-actives.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { FilterActivesPipe } from './filter-actives.pipe'; 2 | 3 | describe('FilterActivesPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new FilterActivesPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /docker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "escalando-aplicaciones-con-angular", 3 | "version": "0.0.0", 4 | "author": { 5 | "name": "Gonzalo Pincheira", 6 | "email": "g.pincheira.a@gmail.com" 7 | }, 8 | "dependencies":{ 9 | "express": "^4.16.4" 10 | } 11 | } -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | endpoint: { 4 | auth: '/auth-service/v1/login', 5 | logout: '/auth-service/v1/logout', 6 | register: '/auth-service/v1/register', 7 | groups: '/auth-service/v1/groups' 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /stubs/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "id": 1, 5 | "fullName": "Administrator", 6 | "email": "admin@app.com" 7 | }, 8 | { 9 | "id": 2, 10 | "fullName": "Juan", 11 | "email": "juan@app.com", 12 | "group": "A" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.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 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/app/modules/core/loading/loading.component.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/theming'; 2 | 3 | :host { 4 | display: flex; 5 | position: absolute; 6 | width: 100%; 7 | height: 100%; 8 | background-color: $cdk-overlay-dark-backdrop-background; 9 | justify-content: center; 10 | align-items: center; 11 | } -------------------------------------------------------------------------------- /src/app/modules/core/unless/unless.directive.spec.ts: -------------------------------------------------------------------------------- 1 | // import { UnlessDirective } from './unless.directive'; 2 | 3 | // describe('UnlessDirective', () => { 4 | // it('should create an instance', () => { 5 | // const directive = new UnlessDirective(); 6 | // expect(directive).toBeTruthy(); 7 | // }); 8 | // }); 9 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/app/pages/home/pages/dash/dash.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-dash', 5 | templateUrl: './dash.component.html', 6 | styleUrls: ['./dash.component.scss'] 7 | }) 8 | export class DashComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/modules/core/loading/loading.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-loading', 5 | templateUrl: './loading.component.html', 6 | styleUrls: ['./loading.component.scss'] 7 | }) 8 | export class LoadingComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/modules/login/login-form.model.ts: -------------------------------------------------------------------------------- 1 | export class LoginFormModel { 2 | email: string; 3 | password: string; 4 | group: string; 5 | rememberMe: boolean; 6 | 7 | constructor(values: { [key: string]: any } = {}) { 8 | this.email = values.email; 9 | this.password = values.password; 10 | this.group = values.group; 11 | this.rememberMe = values.rememberMe; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/modules/core/index.ts: -------------------------------------------------------------------------------- 1 | export { CoreModule } from './core.module'; 2 | 3 | export { LoadingComponent } from './loading/loading.component'; 4 | export { PageNotFoundComponent } from './page-not-found/page-not-found.component'; 5 | export { HighlightDirective } from './highlight/highlight.directive'; 6 | export { UnlessDirective } from './unless/unless.directive'; 7 | 8 | export { Validators } from './validators'; 9 | -------------------------------------------------------------------------------- /e2e/protractor-ci.conf.js: -------------------------------------------------------------------------------- 1 | const config = require('./protractor.conf').config; 2 | 3 | config.capabilities = { 4 | browserName: 'chrome', 5 | chromeOptions: { 6 | args: ['--headless', '--no-sandbox'] 7 | }, 8 | allScriptsTimeout: 30000, 9 | jasmineNodeOpts: { 10 | showColors: true, 11 | defaultTimeoutInterval: 50000, 12 | print: function() {} 13 | }, 14 | }; 15 | 16 | exports.config = config; -------------------------------------------------------------------------------- /src/app/pages/admin/admin.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AdminService } from './admin.service'; 4 | 5 | describe('AdminService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: AdminService = TestBed.get(AdminService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/modules/core/page-not-found/page-not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-page-not-found', 5 | templateUrl: './page-not-found.component.html', 6 | styleUrls: ['./page-not-found.component.scss'] 7 | }) 8 | export class PageNotFoundComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /stubs/groups.json: -------------------------------------------------------------------------------- 1 | { 2 | "list": [ 3 | { "id": "A", "value": "Grupo A", "active": true }, 4 | { "id": "B", "value": "Grupo B", "active": true }, 5 | { "id": "D", "value": "Grupo D", "active": true }, 6 | { "id": "F", "value": "Grupo F", "active": true }, 7 | { "id": "E", "value": "Grupo E", "active": true }, 8 | { "id": "C", "value": "Grupo C", "active": false } 9 | ] 10 | } -------------------------------------------------------------------------------- /ci/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | imageId=$(docker inspect registry.heroku.com/$HEROKU_APP_NAME/web --format={{.Id}}) 3 | payload='{"updates":[{"type":"web","docker_image":"'"$imageId"'"}]}' 4 | 5 | curl -n -X PATCH https://api.heroku.com/apps/$HEROKU_APP_NAME/formation \ 6 | -d "$payload" \ 7 | -H "Content-Type: application/json" \ 8 | -H "Accept: application/vnd.heroku+json; version=3.docker-releases" \ 9 | -H "Authorization: Bearer $HEROKU_KEY" -------------------------------------------------------------------------------- /src/app/modules/login/resolver.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { ResolverService } from './resolver.service'; 3 | 4 | describe('ResolverService', () => { 5 | beforeEach(() => TestBed.configureTestingModule({})); 6 | 7 | it('should be created', () => { 8 | const service: ResolverService = TestBed.get(ResolverService); 9 | expect(service).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/pages/register/register.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { RegisterService } from './register.service'; 4 | 5 | describe('RegisterService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: RegisterService = TestBed.get(RegisterService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import 'hammerjs'; 2 | import { enableProdMode } from '@angular/core'; 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | 5 | import { AppModule } from './app/app.module'; 6 | import { environment } from './environments/environment'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule) 13 | .catch(err => console.error(err)); 14 | -------------------------------------------------------------------------------- /src/app/pages/admin/admin-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { AdminComponent } from './admin.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: AdminComponent 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class AdminRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/pages/register/register-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { RegisterComponent } from './register.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: RegisterComponent, 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class RegisterRoutingModule { } 17 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | selectors = { 5 | 'title' : 'app-root h1', 6 | 'card-titles': 'mat-card-title' 7 | }; 8 | 9 | navigateTo() { 10 | return browser.get('/'); 11 | } 12 | 13 | getTitleText() { 14 | return element(by.css(this.selectors['title'])).getText(); 15 | } 16 | 17 | getcardTitles() { 18 | return element.all(by.css(this.selectors['card-titles'])); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2018", 18 | "dom" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/modules/core/filter-actives.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { Group } from './models/group-interface'; 3 | 4 | @Pipe({ 5 | name: 'filterActives' 6 | }) 7 | export class FilterActivesPipe implements PipeTransform { 8 | 9 | transform(groups: Group[]): Group[] { 10 | return groups 11 | .filter(group => group.active) 12 | .sort((groupA, groupB) => { 13 | if (groupA.id === groupB.id) return 0; 14 | if (groupA.id > groupB.id) return 1; 15 | return -1; 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Escalando Aplicaciones con Angular 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/app/modules/core/validators.ts: -------------------------------------------------------------------------------- 1 | import { Validators as BaseValidators, ValidationErrors, AbstractControl, AsyncValidatorFn } from '@angular/forms'; 2 | import { Observable, of } from 'rxjs'; 3 | 4 | export class Validators extends BaseValidators { 5 | static equalsTo(formControlName: string): AsyncValidatorFn { 6 | return (control: AbstractControl): Promise | Observable => { 7 | return control.parent.get(formControlName).value === control.value 8 | ? of(null) 9 | : of({ equalsTo: true }); 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/pages/register/register.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { environment } from 'src/environments/environment'; 4 | import { Observable } from 'rxjs'; 5 | import { retry } from 'rxjs/operators'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class RegisterService { 11 | 12 | constructor( 13 | private http: HttpClient 14 | ) { } 15 | 16 | register(user): Observable { 17 | return this.http 18 | .post(environment.endpoint.register, user) 19 | .pipe( 20 | retry(2) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/modules/core/highlight/highlight.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, Input, HostListener } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appHighlight]' 5 | }) 6 | export class HighlightDirective { 7 | 8 | @Input('appHighlight') highlightColor: string; 9 | 10 | constructor(private el: ElementRef) { } 11 | 12 | @HostListener('mouseenter') onMouseEnter() { 13 | this.highlight(this.highlightColor || 'red'); 14 | } 15 | 16 | @HostListener('mouseleave') onMouseLeave() { 17 | this.highlight(null); 18 | } 19 | 20 | private highlight(color: string) { 21 | this.el.nativeElement.style.backgroundColor = color; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/app/modules/login/resolver.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { GroupService } from './group.service'; 5 | import { Group } from '../core/models/group-interface'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class ResolverService implements Resolve { 11 | constructor( 12 | private groupService: GroupService 13 | ) { } 14 | 15 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 16 | return this.groupService.getGroups(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.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 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | 5 | import { AppComponent } from './app.component'; 6 | 7 | describe('AppComponent', () => { 8 | beforeEach(async(() => { 9 | TestBed.configureTestingModule({ 10 | imports: [ 11 | RouterTestingModule 12 | ], 13 | declarations: [ 14 | AppComponent 15 | ], 16 | }).compileComponents(); 17 | })); 18 | 19 | it('should create the app', () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-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: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/app/modules/core/unless/unless.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, TemplateRef, ViewContainerRef, Input } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appUnless]' 5 | }) 6 | export class UnlessDirective { 7 | 8 | private hasView = false; 9 | 10 | constructor( 11 | private templateRef: TemplateRef, 12 | private viewContainer: ViewContainerRef 13 | ) { } 14 | 15 | @Input() set appUnless(condition: boolean) { 16 | if (!condition && !this.hasView) { 17 | this.viewContainer.createEmbeddedView(this.templateRef); 18 | this.hasView = true; 19 | } else if (condition && this.hasView) { 20 | this.viewContainer.clear(); 21 | this.hasView = false; 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/app/pages/admin/admin.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, 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 | 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 | -------------------------------------------------------------------------------- /e2e/src/login.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { browser, protractor } from 'protractor'; 2 | import { LoginPage } from './login.po'; 3 | 4 | describe('workspace-project Login', () => { 5 | let page: LoginPage; 6 | 7 | beforeEach(() => { 8 | page = new LoginPage(); 9 | }); 10 | 11 | it('Should login when fill correctly', () => { 12 | const EC = protractor.ExpectedConditions; 13 | const expectedUrl = 'http://localhost:4200/'; 14 | 15 | page.makeLogIn({ 16 | email: 'admin', 17 | password: 'admin' 18 | }); 19 | 20 | // http://www.protractortest.org/#/api?view=ProtractorExpectedConditions.prototype.urlContains 21 | browser.wait(EC.urlIs(expectedUrl)) 22 | .then(() => expect(browser.getCurrentUrl()).toEqual(expectedUrl)); 23 | 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 5 | 6 | import { AppRoutingModule } from './app-routing.module'; 7 | import { CoreModule } from './modules/core/core.module'; 8 | 9 | import { AppComponent } from './app.component'; 10 | 11 | @NgModule({ 12 | declarations: [ 13 | AppComponent 14 | ], 15 | imports: [ 16 | BrowserModule, 17 | AppRoutingModule, 18 | BrowserAnimationsModule, 19 | MatSnackBarModule, 20 | CoreModule 21 | ], 22 | providers: [], 23 | bootstrap: [AppComponent] 24 | }) 25 | export class AppModule { } 26 | -------------------------------------------------------------------------------- /src/app/modules/login/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 4 | 5 | import { AuthGuard } from './auth.guard'; 6 | import { LoginService } from './login.service'; 7 | 8 | describe('AuthGuard', () => { 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [ 12 | HttpClientTestingModule, 13 | RouterTestingModule 14 | ], 15 | providers: [ 16 | LoginService, 17 | AuthGuard, 18 | ] 19 | }); 20 | }); 21 | 22 | it('should ...', inject([AuthGuard], (guard: AuthGuard) => { 23 | expect(guard).toBeTruthy(); 24 | })); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/register/register.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RegisterComponent } from './register.component'; 4 | 5 | describe('RegisterComponent', () => { 6 | let component: RegisterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ RegisterComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RegisterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: node_js 3 | services: 4 | - docker 5 | sudo: false 6 | node_js: 7 | - "node" 8 | addons: 9 | apt: 10 | sources: 11 | - google-chrome 12 | packages: 13 | - google-chrome-stable 14 | cache: 15 | directories: 16 | - ./node_modules 17 | install: 18 | - npm install 19 | stages: 20 | - name: linter 21 | - name: unit-tests 22 | # - name: e2e 23 | - name: deploy 24 | if: branch = master 25 | jobs: 26 | include: 27 | - stage: linter 28 | script: npm run lint 29 | - stage: unit-tests 30 | script: bash ./ci/unit-tests 31 | # - stage: e2e 32 | # script: npm run e2e -- --protractor-config=e2e/protractor-ci.conf.js 33 | - stage: deploy 34 | script: bash ./ci/deploy && bash ./ci/release 35 | -------------------------------------------------------------------------------- /src/app/pages/home/home-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { HomeComponent } from './home.component'; 5 | import { DashComponent } from './pages/dash/dash.component'; 6 | import { ListComponent } from './pages/list/list.component'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: '', 11 | component: HomeComponent, 12 | children: [ 13 | { 14 | path: '', 15 | component: DashComponent, 16 | }, 17 | { 18 | path: 'list', 19 | component: ListComponent, 20 | } 21 | ] 22 | } 23 | ]; 24 | 25 | @NgModule({ 26 | imports: [RouterModule.forChild(routes)], 27 | exports: [RouterModule] 28 | }) 29 | export class HomeRoutingModule { } 30 | -------------------------------------------------------------------------------- /src/app/modules/login/group.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { environment } from 'src/environments/environment'; 4 | import { Observable } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | import { Group } from '../core/models/group-interface'; 8 | 9 | @Injectable() 10 | export class GroupService { 11 | groups: Group[] = []; 12 | 13 | constructor( 14 | private http: HttpClient 15 | ) { } 16 | 17 | setGroups (list: Array) { 18 | this.groups = list; 19 | } 20 | 21 | getStoredGroups () { 22 | return this.groups; 23 | } 24 | 25 | getGroups(): Observable { 26 | return this.http 27 | .get(environment.endpoint.groups) 28 | .pipe( 29 | map(response => response.list as Group[]) 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/modules/login/login.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | align-items: center; 3 | justify-content: center; 4 | display: flex; 5 | flex-grow: 1; 6 | 7 | form { 8 | width: 26.5em; 9 | 10 | @media (max-width: 26.5em) { 11 | width: 100%; 12 | padding: 1em; 13 | } 14 | 15 | .mat-card { 16 | 17 | .mat-card-image { 18 | height: 9em; 19 | background-image: url('/assets/angular.jpg'); 20 | background-size: 100%; 21 | background-position: center; 22 | 23 | @media (max-width: 26.5em) { 24 | background-size: 150%; 25 | } 26 | } 27 | 28 | .mat-card-content { 29 | display: flex; 30 | flex-direction: column; 31 | 32 | .mat-form-field { 33 | width: 100%; 34 | margin-bottom: .5rem; 35 | } 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/app/pages/admin/admin.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 2 | import { User } from './../../modules/login/login.service'; 3 | import { Injectable } from '@angular/core'; 4 | import { Observable } from 'rxjs'; 5 | import { environment } from 'src/environments/environment'; 6 | 7 | @Injectable() 8 | export class AdminService { 9 | 10 | users = []; 11 | 12 | constructor( 13 | private http: HttpClient 14 | ) {} 15 | 16 | createUser(user: User) { 17 | const httpOptions = { 18 | headers: new HttpHeaders({ 19 | 'Content-Type': 'application/json' 20 | }) 21 | }; 22 | return this.http 23 | .post(environment.endpoint.user, user, httpOptions); 24 | } 25 | 26 | listUsers(): Observable { 27 | return this.http 28 | .get(environment.endpoint.user); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /src/app/modules/core/loading/loading.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 3 | 4 | import { LoadingComponent } from './loading.component'; 5 | 6 | describe('LoadingComponent', () => { 7 | let component: LoadingComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ LoadingComponent ], 13 | schemas: [ CUSTOM_ELEMENTS_SCHEMA ] 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(LoadingComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/modules/login/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanLoad, Route, UrlSegment } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { LoginService } from './login.service'; 6 | 7 | @Injectable() 8 | export class AuthGuard implements CanLoad { 9 | 10 | constructor( 11 | private loginService: LoginService, 12 | private router: Router, 13 | ) { } 14 | 15 | canLoad( 16 | route: Route, 17 | segments: UrlSegment[] 18 | ): Observable | Promise | boolean { 19 | const url = `/${route.path}`; 20 | return this.checkLogin(url); 21 | } 22 | 23 | private checkLogin(url: string): boolean { 24 | if (this.loginService.isLoggedIn) { 25 | return true; 26 | } 27 | this.loginService.fallbackUrl = url; 28 | this.router.navigateByUrl('/login'); 29 | return false; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/app/pages/home/pages/dash/dash.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | // import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 3 | // import { DashComponent } from './dash.component'; 4 | 5 | // describe('DashComponent', () => { 6 | // let component: DashComponent; 7 | // let fixture: ComponentFixture; 8 | 9 | // beforeEach(async(() => { 10 | // TestBed.configureTestingModule({ 11 | // declarations: [ DashComponent ], 12 | // schemas: [ CUSTOM_ELEMENTS_SCHEMA ] 13 | // }) 14 | // .compileComponents(); 15 | // })); 16 | 17 | // beforeEach(() => { 18 | // fixture = TestBed.createComponent(DashComponent); 19 | // component = fixture.componentInstance; 20 | // fixture.detectChanges(); 21 | // }); 22 | 23 | // it('should create', () => { 24 | // expect(component).toBeTruthy(); 25 | // }); 26 | // }); 27 | -------------------------------------------------------------------------------- /src/app/pages/home/pages/list/list.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | // import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 3 | // import { ListComponent } from './list.component'; 4 | 5 | // describe('ListComponent', () => { 6 | // let component: ListComponent; 7 | // let fixture: ComponentFixture; 8 | 9 | // beforeEach(async(() => { 10 | // TestBed.configureTestingModule({ 11 | // declarations: [ ListComponent ], 12 | // schemas: [CUSTOM_ELEMENTS_SCHEMA] 13 | // }) 14 | // .compileComponents(); 15 | // })); 16 | 17 | // beforeEach(() => { 18 | // fixture = TestBed.createComponent(ListComponent); 19 | // component = fixture.componentInstance; 20 | // fixture.detectChanges(); 21 | // }); 22 | 23 | // it('should create', () => { 24 | // expect(component).toBeTruthy(); 25 | // }); 26 | // }); 27 | -------------------------------------------------------------------------------- /src/app/modules/core/page-not-found/page-not-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 3 | import { PageNotFoundComponent } from './page-not-found.component'; 4 | 5 | describe('PageNotFoundComponent', () => { 6 | let component: PageNotFoundComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ PageNotFoundComponent ], 12 | schemas: [ CUSTOM_ELEMENTS_SCHEMA ] 13 | }) 14 | .compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(PageNotFoundComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { version } = require('./package.json'); 3 | 4 | const app = express(); 5 | 6 | const port = process.env.PORT || 8080; 7 | 8 | // make express look in the public directory for assets (css/js/img) 9 | app.use(express.static(`${__dirname}/public`)); 10 | 11 | // show docker container health 12 | app.get('/health', (req, res) => { 13 | res.setHeader('Content-Type', 'application/json'); 14 | res.send(JSON.stringify({ version, status: 'up', uptime: process.uptime() })); 15 | }); 16 | 17 | // redirect all routes to index.html for SPA behaviour 18 | app.get('*', (req, res) => { 19 | console.log('+++++++++++++++++++++++++++++++'); 20 | console.log(req.params); 21 | console.log('+++++++++++++++++++++++++++++++'); 22 | res.sendFile(`${__dirname}/public/index.html`); 23 | }); 24 | 25 | app.listen(port, () => { 26 | console.log(`App is running on http://localhost:${port}`); 27 | }); -------------------------------------------------------------------------------- /src/app/pages/register/register.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | align-items: center; 3 | justify-content: center; 4 | display: flex; 5 | flex-grow: 1; 6 | 7 | form { 8 | width: 26.5em; 9 | 10 | @media (max-width: 26.5em) { 11 | width: 100%; 12 | padding: 1em; 13 | } 14 | 15 | .mat-card { 16 | 17 | .mat-card-image { 18 | height: 9em; 19 | background-image: url('/assets/angular.jpg'); 20 | background-size: 100%; 21 | background-position: center; 22 | 23 | @media (max-width: 26.5em) { 24 | background-size: 150%; 25 | } 26 | } 27 | 28 | .mat-card-content { 29 | display: flex; 30 | flex-direction: column; 31 | 32 | .mat-form-field { 33 | width: 100%; 34 | margin-bottom: .5rem; 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/pages/register/register.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { RegisterRoutingModule } from './register-routing.module'; 5 | import { RegisterComponent } from './register.component'; 6 | import { ReactiveFormsModule } from '@angular/forms'; 7 | import { MatCardModule } from '@angular/material/card'; 8 | import { MatFormFieldModule } from '@angular/material/form-field'; 9 | import { MatIconModule } from '@angular/material/icon'; 10 | import { MatInputModule } from '@angular/material/input'; 11 | import { MatButtonModule } from '@angular/material/button'; 12 | 13 | import { CoreModule } from 'src/app/modules'; 14 | 15 | @NgModule({ 16 | declarations: [RegisterComponent], 17 | imports: [ 18 | CommonModule, 19 | RegisterRoutingModule, 20 | ReactiveFormsModule, 21 | MatCardModule, 22 | MatFormFieldModule, 23 | MatIconModule, 24 | MatInputModule, 25 | MatButtonModule, 26 | CoreModule 27 | ] 28 | }) 29 | export class RegisterModule { } 30 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` 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 | endpoint: { 8 | auth: 'http://localhost:8882/auth-service/v1/login', 9 | logout: 'http://localhost:8882/auth-service/v1/logout', 10 | register: 'http://localhost:8882/auth-service/v1/register', 11 | groups: 'http://localhost:8882/auth-service/v1/groups', 12 | user: 'http://localhost:3000/users', 13 | } 14 | }; 15 | 16 | /* 17 | * For easier debugging in development mode, you can import the following file 18 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 19 | * 20 | * This import should be commented out in production mode because it will have a negative impact 21 | * on performance if an error is thrown. 22 | */ 23 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 24 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.scss: -------------------------------------------------------------------------------- 1 | .app-container { 2 | display: flex; 3 | flex-direction: column; 4 | position: absolute; 5 | top: 0; 6 | bottom: 0; 7 | left: 0; 8 | right: 0; 9 | } 10 | 11 | .app-spacer { 12 | flex: 1 1 auto; 13 | } 14 | 15 | .app-is-mobile .app-toolbar { 16 | position: fixed; 17 | /* Make sure the toolbar will stay on top of the content as it scrolls past. */ 18 | z-index: 2; 19 | } 20 | 21 | h1.app-name { 22 | margin-left: 8px; 23 | } 24 | 25 | .app-sidenav-container { 26 | /* When the sidenav is not fixed, stretch the sidenav container to fill the available space. This 27 | causes `` to act as our scrolling element for desktop layouts. */ 28 | flex: 1; 29 | mat-sidenav { 30 | min-width: 260px; 31 | } 32 | } 33 | 34 | .app-is-mobile .app-sidenav-container { 35 | /* When the sidenav is fixed, don't constrain the height of the sidenav container. This allows the 36 | `` to be our scrolling element for mobile layouts. */ 37 | flex: 1 0 auto; 38 | } 39 | 40 | .app-content { 41 | padding: 1rem; 42 | } -------------------------------------------------------------------------------- /e2e/src/login.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class LoginPage { 4 | selectors = { 5 | 'email': 'input[name="email"]', 6 | 'password': 'input[name="password"]', 7 | 'selectGroup': 'mat-select[name="group"]', 8 | 'form' : 'form' 9 | }; 10 | 11 | navigateToLogin() { 12 | return browser.get('/login'); 13 | } 14 | 15 | setEmail(text) { 16 | element(by.css(this.selectors.email)).sendKeys(text); 17 | } 18 | 19 | setPassword(text) { 20 | element(by.css(this.selectors.password)).sendKeys(text); 21 | } 22 | 23 | selectGroupOptionLastValue() { 24 | element(by.css(this.selectors.selectGroup)).click() 25 | .then(() => element.all(by.css('mat-option')).last().click()); 26 | } 27 | 28 | makeLogIn({ email, password }) { 29 | this.navigateToLogin(); 30 | this.setEmail(email); 31 | this.setPassword(password); 32 | this.selectGroupOptionLastValue(); 33 | 34 | element(by.css(this.selectors['form'])).submit(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/pages/home/pages/dash/dash.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | } 4 | 5 | .mat-card { 6 | margin: 1rem; 7 | width: 100%; 8 | &:first-child { 9 | .mat-card-avatar { 10 | background-image: url('https://media.licdn.com/dms/image/C5103AQHO_xFfVq28zg/profile-displayphoto-shrink_200_200/0?e=1548892800&v=beta&t=KeyIhBziBRVUIUhjPhpMJzKGpAFqGW5yzj4njMDcFtg'); 11 | } 12 | .mat-card-image { 13 | background-image: url('https://www.offing.es/wp-content/uploads/angular.jpg'); 14 | } 15 | } 16 | &:last-child { 17 | .mat-card-avatar { 18 | background-image: url('https://media.licdn.com/dms/image/C5603AQELaVeoZC2HnQ/profile-displayphoto-shrink_800_800/0?e=1548892800&v=beta&t=uovv5TJO74kMjg9Omor3_2z8KyJo4_zh2H3hG8i0VOI'); 19 | } 20 | .mat-card-image { 21 | background-image: url('https://angularfirebase.com/images/thumbs/testing-14.png'); 22 | } 23 | } 24 | } 25 | 26 | .mat-card-avatar { 27 | background-size: cover; 28 | } 29 | 30 | .mat-card-image { 31 | height: 40vh; 32 | background-size: cover; 33 | background-position: center; 34 | } -------------------------------------------------------------------------------- /src/app/modules/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { MatCardModule } from '@angular/material/card'; 4 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 5 | 6 | import { LoadingComponent } from './loading/loading.component'; 7 | import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; 8 | import { HighlightDirective } from './highlight/highlight.directive'; 9 | import { UnlessDirective } from './unless/unless.directive'; 10 | import { FilterActivesPipe } from './filter-actives.pipe'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | LoadingComponent, 15 | PageNotFoundComponent, 16 | HighlightDirective, 17 | UnlessDirective, 18 | FilterActivesPipe 19 | ], 20 | imports: [ 21 | CommonModule, 22 | MatProgressSpinnerModule, 23 | MatCardModule 24 | ], 25 | exports: [ 26 | LoadingComponent, 27 | PageNotFoundComponent, 28 | HighlightDirective, 29 | UnlessDirective, 30 | FilterActivesPipe 31 | ], 32 | providers: [ 33 | FilterActivesPipe 34 | ] 35 | }) 36 | export class CoreModule { } 37 | -------------------------------------------------------------------------------- /src/app/modules/login/login.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable, of } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | import { environment } from 'src/environments/environment'; 7 | 8 | @Injectable() 9 | export class LoginService { 10 | user: User; 11 | fallbackUrl = ''; 12 | 13 | get isLoggedIn(): boolean { 14 | return !!this.user; 15 | } 16 | 17 | constructor( 18 | private http: HttpClient, 19 | ) { } 20 | 21 | clearUser() { 22 | this.user = null; 23 | } 24 | 25 | authenticate(email: String, password: String): Observable { 26 | return this.http.post(environment.endpoint.auth, { 27 | email, password 28 | }).pipe( 29 | map(user => { 30 | this.user = user; 31 | return this.isLoggedIn; 32 | }) 33 | ); 34 | } 35 | 36 | logout(): Promise { 37 | return this.http.post(environment.endpoint.logout, {}) 38 | .toPromise(); 39 | } 40 | } 41 | export interface User { 42 | fullName: string; 43 | email: string; 44 | password: string; 45 | group?: string; 46 | } 47 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { browser, protractor } from 'protractor'; 2 | import { AppPage } from './app.po'; 3 | import { LoginPage } from './login.po'; 4 | 5 | describe('workspace-project App', () => { 6 | let page: AppPage; 7 | let login: LoginPage; 8 | 9 | beforeEach(() => { 10 | page = new AppPage(); 11 | login = new LoginPage(); 12 | }); 13 | 14 | it('should display welcome message', () => { 15 | const EC = protractor.ExpectedConditions; 16 | const expectedUrl = 'http://localhost:4200/'; 17 | 18 | login.makeLogIn({ 19 | email: 'admin', 20 | password: 'admin' 21 | }); 22 | 23 | browser.wait(EC.urlIs(expectedUrl)) 24 | .then(() => { 25 | expect(page.getTitleText()).toEqual('App'); 26 | expect(browser.getCurrentUrl()).toEqual(expectedUrl); 27 | }); 28 | }); 29 | 30 | it('should have correct titles', () => { 31 | const titles = [ 32 | 'What is Lorem Ipsum?', 33 | 'What is Lorem Ipsum?' 34 | ]; 35 | const titilesList = page.getcardTitles(); 36 | 37 | titles.forEach((title, index) => { 38 | expect(titilesList.get(index).getText()).toEqual(title); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/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 | process.env.CHROME_BIN = require('puppeteer').executablePath(); 4 | 5 | module.exports = function (config) { 6 | config.set({ 7 | basePath: '', 8 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 9 | plugins: [ 10 | require('karma-jasmine'), 11 | require('karma-chrome-launcher'), 12 | require('karma-jasmine-html-reporter'), 13 | require('karma-coverage-istanbul-reporter'), 14 | require('karma-spec-reporter'), 15 | require('@angular-devkit/build-angular/plugins/karma') 16 | ], 17 | client: { 18 | clearContext: true 19 | }, 20 | coverageIstanbulReporter: { 21 | dir: require('path').join(__dirname, '../coverage'), 22 | reports: [ 'text-summary', 'html', 'cobertura', 'lcovonly' ], 23 | fixWebpackSourcePaths: true 24 | }, 25 | reporters: [ 'spec', 'coverage-istanbul' ], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: [ 'ChromeHeadless' ], 31 | singleRun: false 32 | }); 33 | }; -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { LoginModule } from './modules/login'; 4 | 5 | import { PageNotFoundComponent } from './modules/core'; 6 | import { LoginComponent, AuthGuard } from './modules/login'; 7 | import { ResolverService } from './modules/login/resolver.service'; 8 | 9 | const routes: Routes = [ 10 | { 11 | path: 'login', 12 | component: LoginComponent, 13 | resolve: { 14 | groups: ResolverService 15 | } 16 | }, 17 | { 18 | path: 'register', 19 | loadChildren: './pages/register/register.module#RegisterModule', 20 | }, 21 | { 22 | path: 'admin', 23 | loadChildren: './pages/admin/admin.module#AdminModule', 24 | resolve: { 25 | groups: ResolverService 26 | } 27 | }, 28 | { 29 | path: '', 30 | loadChildren: './pages/home/home.module#HomeModule', 31 | canLoad: [AuthGuard] 32 | }, 33 | { 34 | path: '**', 35 | component: PageNotFoundComponent 36 | } 37 | ]; 38 | 39 | @NgModule({ 40 | imports: [ 41 | RouterModule.forRoot(routes), 42 | LoginModule 43 | ], 44 | exports: [ RouterModule ] 45 | }) 46 | export class AppRoutingModule { } 47 | -------------------------------------------------------------------------------- /docs/Cuestionario1.md: -------------------------------------------------------------------------------- 1 | **Preguntas Clase 1** 2 | 3 | 1 - ¿ Para que sirve definir la llave de configuración `imports` en un módulo de angular? 4 | * Para incluir otros módulos. 5 | * Para incluir servicios. 6 | * Para incluir librerías externas como jQuery o Bootstrap. 7 | 8 | 2 - ¿ Por qué es importante definir que la llave de configuración `exports` si queremos que un módulo 9 | sea usado por otros módulos? 10 | - Para contarle a Angular que quiero que mi módulo sea "público" en el contexto de mi aplicación. 11 | - Para establecer como "privado" el módulo. 12 | - Para exportar las librerías que fueron importadas en el módulo. 13 | 14 | 3 - ¿ Qué es un servicio y como importarlo en un módulo ? 15 | * Un servicio es una clase que puede ser incluida como instancia cuando se crea un componente y debe ser "decorado" con `@injectable` 16 | * Es una clase generica que por el hecho de ser definido con un nombre que incluya al final "Service" puede ser integrada en un componente. 17 | * Es una directiva que debe ser definida con el formato ``. 18 | 19 | 4. ¿ Qué comando se debe utilizar para crear una versión "lista para producción" del proyecto? 20 | * `npm run build` 21 | * `npm run build --prod` 22 | * `npm run project production` 23 | -------------------------------------------------------------------------------- /src/app/pages/admin/admin.module.ts: -------------------------------------------------------------------------------- 1 | import { CoreModule } from 'src/app/modules'; 2 | import { MatFormFieldModule } from '@angular/material/form-field'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { NgModule } from '@angular/core'; 5 | import { CommonModule } from '@angular/common'; 6 | 7 | import { AdminRoutingModule } from './admin-routing.module'; 8 | import { AdminComponent } from './admin.component'; 9 | import { MatCardModule } from '@angular/material/card'; 10 | import { MatInputModule } from '@angular/material/input'; 11 | import { MatSelectModule } from '@angular/material/select'; 12 | import { MatIconModule } from '@angular/material/icon'; 13 | import { MatButtonModule } from '@angular/material/button'; 14 | import { MatTableModule } from '@angular/material/table'; 15 | import { AdminService } from './admin.service'; 16 | 17 | @NgModule({ 18 | declarations: [AdminComponent], 19 | providers: [ 20 | AdminService 21 | ], 22 | imports: [ 23 | CommonModule, 24 | AdminRoutingModule, 25 | MatCardModule, 26 | ReactiveFormsModule, 27 | MatFormFieldModule, 28 | MatInputModule, 29 | MatSelectModule, 30 | MatIconModule, 31 | MatButtonModule, 32 | MatTableModule, 33 | CoreModule 34 | ] 35 | }) 36 | export class AdminModule { } 37 | -------------------------------------------------------------------------------- /src/app/pages/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormGroup, FormControl } from '@angular/forms'; 3 | import { Validators } from 'src/app/modules/core'; 4 | import { RegisterService } from './register.service'; 5 | 6 | @Component({ 7 | selector: 'app-register', 8 | templateUrl: './register.component.html', 9 | styleUrls: ['./register.component.scss'] 10 | }) 11 | export class RegisterComponent implements OnInit { 12 | isLoading = false; 13 | 14 | form = new FormGroup({ 15 | fullName: new FormControl('', [Validators.required, Validators.minLength(3)]), 16 | email: new FormControl('', [Validators.required, Validators.minLength(3)]), 17 | password: new FormControl('', [Validators.required, Validators.minLength(3)]), 18 | }); 19 | 20 | constructor( 21 | private registerService: RegisterService 22 | ) { } 23 | 24 | ngOnInit() { 25 | } 26 | 27 | onSubmit() { 28 | if (this.form.valid) { 29 | this.isLoading = true; 30 | this.registerService 31 | .register(this.form.value) 32 | .subscribe(() => { 33 | this.isLoading = false; 34 | }, (reason) => { 35 | this.isLoading = false; 36 | alert(JSON.stringify(reason)); 37 | }); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/modules/core/highlight/highlight.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, DebugElement } from '@angular/core'; 2 | import { TestBed, ComponentFixture } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | import { HighlightDirective } from './highlight.directive'; 5 | 6 | @Component({ 7 | template: `
` 8 | }) 9 | class TestingComponent {} 10 | 11 | describe('HighlightDirective', () => { 12 | let component: TestingComponent; 13 | let fixture: ComponentFixture; 14 | let inputEl: DebugElement; 15 | 16 | beforeEach(() => { 17 | TestBed.configureTestingModule({ 18 | declarations: [ 19 | TestingComponent, HighlightDirective 20 | ] 21 | }); 22 | fixture = TestBed.createComponent(TestingComponent); 23 | component = fixture.componentInstance; 24 | inputEl = fixture.debugElement.query(By.css('div')); 25 | }); 26 | 27 | it('hovering over input', () => { 28 | inputEl.triggerEventHandler('mouseenter', null); 29 | fixture.detectChanges(); 30 | expect(inputEl.nativeElement.style.backgroundColor).toBe('red'); 31 | 32 | inputEl.triggerEventHandler('mouseleave', null); 33 | fixture.detectChanges(); 34 | expect(inputEl.nativeElement.style.backgroundColor).toBe(''); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /ci/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "=============================================================" 4 | echo " CONSTRUYENDO APLICACIÓN " 5 | echo "=============================================================" 6 | npm run build:prod 7 | 8 | echo "=============================================================" 9 | echo " ESTRUCTURA DIRECTORIO " 10 | echo "=============================================================" 11 | ls -lha 12 | 13 | echo "=============================================================" 14 | echo " CREANDO IMAGEN DOCKER " 15 | echo "=============================================================" 16 | PACKAGE_VERSION=$(node -p -e "require('./package.json').version") 17 | IMAGE_NAME="registry.heroku.com/${HEROKU_APP_NAME}/web" 18 | 19 | mkdir -p ./docker/public 20 | cp -R dist/web-app/. ./docker/public 21 | cp server/server.js docker/ 22 | 23 | cd docker 24 | 25 | echo "$HEROKU_KEY" | docker login --username ${HEROKU_OWNER_EMAIL} --password-stdin registry.heroku.com 26 | docker build -t registry.heroku.com/${HEROKU_APP_NAME}/web --build-arg app_version=${PACKAGE_VERSION} . 27 | 28 | echo "=============================================================" 29 | echo " PUBLICANDO ... " 30 | echo "=============================================================" 31 | docker push registry.heroku.com/${HEROKU_APP_NAME}/web 32 | -------------------------------------------------------------------------------- /src/app/modules/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { LoginComponent } from './login.component'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { MatFormFieldModule } from '@angular/material/form-field'; 6 | import { MatIconModule } from '@angular/material/icon'; 7 | import { MatOptionModule } from '@angular/material/core'; 8 | import { MatCardModule } from '@angular/material/card'; 9 | import { MatSelectModule } from '@angular/material/select'; 10 | import { MatCheckboxModule } from '@angular/material/checkbox'; 11 | import { CoreModule } from '../core'; 12 | import { LoginService } from './login.service'; 13 | import { AuthGuard } from './auth.guard'; 14 | import { HttpClientModule } from '@angular/common/http'; 15 | import { MatButtonModule } from '@angular/material/button'; 16 | import { MatInputModule } from '@angular/material/input'; 17 | import { GroupService } from './group.service'; 18 | 19 | @NgModule({ 20 | declarations: [ 21 | LoginComponent 22 | ], 23 | providers: [ 24 | LoginService, 25 | AuthGuard, 26 | GroupService 27 | ], 28 | imports: [ 29 | CommonModule, 30 | FormsModule, 31 | MatFormFieldModule, 32 | MatIconModule, 33 | MatOptionModule, 34 | MatCardModule, 35 | MatSelectModule, 36 | MatCheckboxModule, 37 | CoreModule, 38 | HttpClientModule, 39 | MatButtonModule, 40 | MatInputModule 41 | ], 42 | }) 43 | export class LoginModule { } 44 | -------------------------------------------------------------------------------- /src/app/modules/login/group.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { GroupService } from './group.service'; 4 | import { HttpClient } from '@angular/common/http'; 5 | import { environment } from 'src/environments/environment'; 6 | import { of } from 'rxjs'; 7 | 8 | class HttpClientMock { 9 | get = jasmine.createSpy(); 10 | } 11 | 12 | fdescribe('Group Service', () => { 13 | let service: GroupService; 14 | let httpClientMock: HttpClientMock; 15 | 16 | beforeEach(() => { 17 | TestBed.configureTestingModule({ 18 | imports: [ 19 | HttpClientTestingModule 20 | ], 21 | providers: [ 22 | GroupService, 23 | { 24 | provide: HttpClient, 25 | useClass: HttpClientMock 26 | } 27 | ] 28 | }); 29 | service = TestBed.get(GroupService); 30 | httpClientMock = TestBed.get(HttpClient); 31 | }); 32 | 33 | it('Should create an instance', () => { 34 | expect(service).toBeDefined(); 35 | }); 36 | 37 | it('should call http get service', () => { 38 | httpClientMock.get.and.returnValue(of({ list: [] })); 39 | service.getGroups(); 40 | expect(httpClientMock.get).toHaveBeenCalledWith(environment.endpoint.groups); 41 | }); 42 | 43 | it('should set and get', () => { 44 | const list = [1, 2, 3]; 45 | 46 | service.setGroups(list); 47 | 48 | expect(service.getStoredGroups()).toEqual(list); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 3 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { MatMenuModule} from '@angular/material/menu'; 6 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 7 | 8 | import { HomeComponent } from './home.component'; 9 | import { LoginService } from '../../modules/login/login.service'; 10 | 11 | describe('HomeComponent', () => { 12 | let component: HomeComponent; 13 | let fixture: ComponentFixture; 14 | 15 | class LoginServiceStub { 16 | user = {}; 17 | } 18 | 19 | beforeEach(async(() => { 20 | TestBed.configureTestingModule({ 21 | imports: [ 22 | MatMenuModule, 23 | HttpClientTestingModule, 24 | RouterTestingModule, 25 | MatSnackBarModule 26 | ], 27 | providers: [ 28 | { 29 | provide: LoginService, 30 | useClass: LoginServiceStub 31 | } 32 | ], 33 | declarations: [ HomeComponent ], 34 | schemas: [ CUSTOM_ELEMENTS_SCHEMA ] 35 | }) 36 | .compileComponents(); 37 | })); 38 | 39 | beforeEach(() => { 40 | fixture = TestBed.createComponent(HomeComponent); 41 | component = fixture.componentInstance; 42 | fixture.detectChanges(); 43 | }); 44 | 45 | it('should create', () => { 46 | expect(component).toBeTruthy(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/app/pages/admin/admin.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | align-items: center; 3 | justify-content: center; 4 | display: flex; 5 | flex-grow: 1; 6 | overflow-y: scroll; 7 | overflow-x: hidden; 8 | 9 | table { 10 | width: 100%; 11 | margin-top: 1rem; 12 | @media (min-width: 768px) { 13 | margin-top: 4rem; 14 | } 15 | } 16 | 17 | form { 18 | width: 80%; 19 | margin-top: 10%; 20 | margin-bottom: 10%; 21 | 22 | @media (max-width: 26.5em) { 23 | width: 100%; 24 | padding: 1em; 25 | } 26 | 27 | .mat-card { 28 | position: relative; 29 | 30 | 31 | .mat-button { 32 | @media (min-width: 768px) { 33 | position: absolute; 34 | right: 0; 35 | margin-right: 2.5rem; 36 | } 37 | } 38 | .mat-card-image { 39 | height: 9em; 40 | background-image: url('/assets/angular.jpg'); 41 | background-size: 100%; 42 | background-position: center; 43 | 44 | @media (max-width: 26.5em) { 45 | background-size: 150%; 46 | } 47 | } 48 | 49 | .mat-card-content { 50 | display: flex; 51 | flex-direction: column; 52 | flex-wrap: wrap; 53 | 54 | @media (min-width: 768px) { 55 | flex-direction: row; 56 | } 57 | .mat-form-field { 58 | width: 100%; 59 | margin-bottom: .5rem; 60 | @media (min-width: 768px) { 61 | width: 45%; 62 | margin: 0 auto; 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/app/pages/home/pages/dash/dash.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | What is Lorem Ipsum? 5 | Maecenas cursus nulla at elementum. 6 |
7 |
8 | 9 |

Nulla ut ligula neque. Nullam semper scelerisque diam, ut aliquet nisi. Pellentesque cursus, risus a mattis viverra, risus ex ornare lorem, sed pellentesque erat orci ut diam. Pellentesque aliquam mi eu enim tincidunt, id convallis mauris ornare. Praesent luctus volutpat nunc quis egestas. Nulla libero sapien, blandit vitae tortor sit.

10 |
11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 |
20 | What is Lorem Ipsum? 21 | Cras mattis, magna at varius. 22 |
23 |
24 | 25 |

Nullam lacus nibh, malesuada non pharetra ut, consequat tempus nulla. Phasellus sed ipsum quis magna facilisis volutpat eu ut nunc. Proin dui lacus, pretium non elementum sed, laoreet quis tellus. Aenean id massa id ligula aliquam interdum. In consequat leo non neque maximus dictum. Ut scelerisque neque vel magna efficitur.

26 |
27 | 28 | 29 | 30 | 31 |
-------------------------------------------------------------------------------- /src/app/pages/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, ChangeDetectorRef } from '@angular/core'; 2 | import { MediaMatcher } from '@angular/cdk/layout'; 3 | import { LoginService } from 'src/app/modules'; 4 | import { Router } from '@angular/router'; 5 | import { MatSnackBar } from '@angular/material/snack-bar'; 6 | 7 | @Component({ 8 | selector: 'app-home', 9 | templateUrl: './home.component.html', 10 | styleUrls: ['./home.component.scss'] 11 | }) 12 | export class HomeComponent implements OnDestroy { 13 | mobileQuery: MediaQueryList; 14 | private _mobileQueryListener: () => void; 15 | 16 | get user() { 17 | return this.loginService.user; 18 | } 19 | 20 | constructor( 21 | private loginService: LoginService, 22 | private router: Router, 23 | private snackBar: MatSnackBar, 24 | changeDetectorRef: ChangeDetectorRef, 25 | media: MediaMatcher 26 | ) { 27 | this.mobileQuery = media.matchMedia('(max-width: 600px)'); 28 | this._mobileQueryListener = () => changeDetectorRef.detectChanges(); 29 | this.mobileQuery.addListener(this._mobileQueryListener); 30 | } 31 | 32 | ngOnDestroy(): void { 33 | this.mobileQuery.removeListener(this._mobileQueryListener); 34 | } 35 | 36 | logout(): void { 37 | this.loginService.logout() 38 | .then(() => { 39 | this.loginService.clearUser(); 40 | this.router.navigateByUrl('/login'); 41 | }) 42 | .catch(error => { 43 | this.snackBar.open(error.message, null, { duration: 5000 }); 44 | }); 45 | // .subscribe({ 46 | // next: () => { 47 | // this.router.navigateByUrl('/login'); 48 | // }, 49 | // error: (error) => { 50 | // this.snackBar.open(error.message, null, { duration: 5000 }); 51 | // } 52 | // }); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/app/pages/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatCardModule } from '@angular/material/card'; 6 | import { MatDividerModule } from '@angular/material/divider'; 7 | import { MatFormFieldModule } from '@angular/material/form-field'; 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 { MatPaginatorModule } from '@angular/material/paginator'; 13 | import { MatSidenavModule } from '@angular/material/sidenav'; 14 | import { MatTableModule } from '@angular/material/table'; 15 | import { MatToolbarModule } from '@angular/material/toolbar'; 16 | import { CoreModule } from 'src/app/modules'; 17 | 18 | import { HomeRoutingModule } from './home-routing.module'; 19 | import { HomeComponent } from './home.component'; 20 | import { DashComponent } from './pages/dash/dash.component'; 21 | import { ListComponent } from './pages/list/list.component'; 22 | 23 | @NgModule({ 24 | declarations: [ 25 | HomeComponent, 26 | DashComponent, 27 | ListComponent 28 | ], 29 | imports: [ 30 | CommonModule, 31 | FormsModule, 32 | CoreModule, 33 | HomeRoutingModule, 34 | MatButtonModule, 35 | MatCardModule, 36 | MatDividerModule, 37 | MatFormFieldModule, 38 | MatIconModule, 39 | MatInputModule, 40 | MatListModule, 41 | MatMenuModule, 42 | MatPaginatorModule, 43 | MatSidenavModule, 44 | MatTableModule, 45 | MatToolbarModule, 46 | ] 47 | }) 48 | export class HomeModule { } 49 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

App

5 | 6 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | home Home 27 | 28 | 29 | bar_chart List resume 30 | 31 | 32 | 33 | settings Settings 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 |
43 | 44 |
45 |
46 |
-------------------------------------------------------------------------------- /src/app/pages/register/register.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 | FullName 8 | 9 | perm_identity 10 | Type your full Name 11 | 12 | {{ form.get('fullName').errors | json }} 13 | 14 | 15 | 16 | 17 | 18 | Email 19 | 20 | email 21 | Type your email 22 | 23 | {{ form.get('email').errors | json }} 24 | 25 | 26 | 27 | 28 | Password 29 | 30 | lock_open 31 | Type your password 32 | 33 | {{ form.get('password').errors | json }} 34 | 35 | 36 | 37 | 38 | 39 | 42 | 43 |
44 |
45 | 46 |

47 | Estamos trabajando... 48 |

49 | 50 | -------------------------------------------------------------------------------- /src/app/pages/admin/admin.component.ts: -------------------------------------------------------------------------------- 1 | import { MatTableDataSource } from '@angular/material/table'; 2 | import { AdminService } from './admin.service'; 3 | import { User } from './../../modules/login/login.service'; 4 | import { FilterActivesPipe } from './../../modules/core/filter-actives.pipe'; 5 | import { ActivatedRoute } from '@angular/router'; 6 | import { FormGroup, FormControl, Validators } from '@angular/forms'; 7 | import { Component, OnInit } from '@angular/core'; 8 | 9 | @Component({ 10 | selector: 'app-admin', 11 | templateUrl: './admin.component.html', 12 | styleUrls: ['./admin.component.scss'] 13 | }) 14 | export class AdminComponent implements OnInit { 15 | 16 | form = new FormGroup({ 17 | fullName: new FormControl('', [Validators.required, Validators.minLength(3)]), 18 | email: new FormControl('', [Validators.required, Validators.email]), 19 | password: new FormControl('', [Validators.required]), 20 | group : new FormControl('', [Validators.required]) 21 | }); 22 | groups = []; 23 | users: User[]; 24 | usersSource = new MatTableDataSource(); 25 | headers = [ 'name', 'email', 'group' ]; 26 | 27 | constructor( 28 | private route: ActivatedRoute, 29 | private filterActives: FilterActivesPipe, 30 | private adminService: AdminService 31 | ) { } 32 | 33 | ngOnInit() { 34 | this.route.data 35 | .subscribe((data: { groups: [] }) => { 36 | this.groups = this.filterActives.transform(data.groups); 37 | }); 38 | this.adminService 39 | .listUsers() 40 | .subscribe(users => this.usersSource.data = users); 41 | } 42 | 43 | onSubmit() { 44 | // get user 45 | if (this.form.valid) { 46 | let user: User; 47 | user = this.form.value as User; 48 | this.adminService 49 | .createUser(user) 50 | .subscribe(userResponse => { 51 | this.usersSource.data.push(userResponse); 52 | }); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/pages/home/pages/list/list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | list 4 | What is Lorem Ipsum? 5 | Maecenas cursus nulla at elementum. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
ID {{row.id}} Progress {{row.progress}}% Name {{row.name}} Color {{row.color}}
42 | 43 | 44 | 45 |
46 | 47 | 48 | 49 | 50 |
-------------------------------------------------------------------------------- /src/app/modules/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { NgForm } from '@angular/forms'; 3 | import { MatSnackBar } from '@angular/material/snack-bar'; 4 | import { Router, ActivatedRoute } from '@angular/router'; 5 | import { finalize } from 'rxjs/operators'; 6 | import { LoginService } from './login.service'; 7 | import { LoginFormModel } from './login-form.model'; 8 | import { FilterActivesPipe } from '../core/filter-actives.pipe'; 9 | import { Group } from '../core/models/group-interface'; 10 | 11 | interface JSONResponse { 12 | groups: Group[]; 13 | } 14 | @Component({ 15 | selector: 'app-login', 16 | templateUrl: './login.component.html', 17 | styleUrls: ['./login.component.scss'] 18 | }) 19 | export class LoginComponent implements OnInit { 20 | @ViewChild('loginForm') loginForm: NgForm; 21 | 22 | formModel: LoginFormModel; 23 | isLoading: boolean; 24 | groups = []; 25 | 26 | constructor( 27 | private route: ActivatedRoute, 28 | private router: Router, 29 | private snackBar: MatSnackBar, 30 | private loginService: LoginService, 31 | private filterActives: FilterActivesPipe 32 | ) { 33 | this.formModel = new LoginFormModel({ 34 | email: this.route.snapshot.queryParams.email, 35 | group: '', 36 | rememberMe: true, 37 | }); 38 | } 39 | 40 | ngOnInit() { 41 | this.route.data 42 | .subscribe((data: { groups: [] }) => { 43 | this.groups = this.filterActives.transform(data.groups); 44 | }); 45 | } 46 | submit() { 47 | if (this.loginForm.valid) { 48 | this.isLoading = true; 49 | this.loginService 50 | .authenticate(this.formModel.email, this.formModel.password) 51 | .pipe( 52 | finalize(() => this.isLoading = false), 53 | ) 54 | .subscribe(_ => { 55 | this.router.navigateByUrl(this.loginService.fallbackUrl); 56 | }, errorResponse => { 57 | this.snackBar.open(errorResponse.error.message, null, { duration: 5000 }); 58 | }); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "escalando-aplicaciones-con-angular", 3 | "version": "0.4.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "npm-run-all -p stubs serve json-server", 7 | "serve": "ng serve --port 8080", 8 | "json-server": "json-server --watch ./stubs/users.json", 9 | "build:prod": "ng build --prod", 10 | "build": "ng build", 11 | "test": "ng test --code-coverage", 12 | "lint": "ng lint", 13 | "e2e": "ng e2e", 14 | "stubs": "npx stubby -d ./stubs/config.yaml -w", 15 | "inspect-coverage": "npm run test -- --watch=false && http-server coverage" 16 | }, 17 | "private": true, 18 | "dependencies": { 19 | "@angular/animations": "~7.0.4", 20 | "@angular/cdk": "~7.0.4", 21 | "@angular/common": "~7.0.4", 22 | "@angular/compiler": "~7.0.4", 23 | "@angular/core": "~7.0.4", 24 | "@angular/forms": "~7.0.4", 25 | "@angular/http": "~7.0.4", 26 | "@angular/material": "^7.0.4", 27 | "@angular/platform-browser": "~7.0.4", 28 | "@angular/platform-browser-dynamic": "~7.0.4", 29 | "@angular/router": "~7.0.4", 30 | "core-js": "^2.5.4", 31 | "hammerjs": "^2.0.8", 32 | "http-server": "^0.11.1", 33 | "rxjs": "~6.3.3", 34 | "zone.js": "~0.8.26" 35 | }, 36 | "devDependencies": { 37 | "@angular-devkit/build-angular": "~0.10.0", 38 | "@angular/cli": "~7.0.6", 39 | "@angular/compiler-cli": "~7.0.4", 40 | "@angular/language-service": "~7.0.4", 41 | "@types/jasmine": "~3.0.0", 42 | "@types/jasminewd2": "~2.0.3", 43 | "@types/node": "~10.12.9", 44 | "codelyzer": "~4.5.0", 45 | "http-server": "^0.11.1", 46 | "jasmine-core": "~3.3.0", 47 | "jasmine-spec-reporter": "~4.2.1", 48 | "json-server": "^0.14.0", 49 | "karma": "~3.1.1", 50 | "karma-chrome-launcher": "~2.2.0", 51 | "karma-coverage-istanbul-reporter": "~2.0.1", 52 | "karma-jasmine": "~2.0.1", 53 | "karma-jasmine-html-reporter": "^1.4.0", 54 | "karma-spec-reporter": "0.0.32", 55 | "npm-run-all": "^4.1.5", 56 | "protractor": "~5.4.0", 57 | "puppeteer": "^1.10.0", 58 | "stubby": "^4.0.0", 59 | "ts-node": "~7.0.1", 60 | "tslint": "~5.11.0", 61 | "typescript": "~3.1.6" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /stubs/config.yaml: -------------------------------------------------------------------------------- 1 | - request: 2 | url: ^/auth-service/v1/login$ 3 | method: POST 4 | post: '{"email":"admin","password":"admin"}' 5 | response: 6 | status: 200 7 | latency: 1000 8 | headers: 9 | content-type: application/json 10 | file: ./user.json 11 | 12 | - request: 13 | url: ^/auth-service/v1/logout$ 14 | method: POST 15 | post: '' 16 | response: 17 | status: 200 18 | latency: 1000 19 | headers: 20 | content-type: application/json 21 | 22 | - request: 23 | url: ^/auth-service/v1/login$ 24 | method: POST 25 | response: 26 | status: 400 27 | latency: 1000 28 | headers: 29 | content-type: application/json 30 | body: > 31 | { 32 | "timestamp": 1500597044204, 33 | "status": 400, 34 | "error": "Bad Request", 35 | "exception": "AuthenticationException", 36 | "message": "Invalid credentials", 37 | "path": "/login" 38 | } 39 | 40 | - request: 41 | url: ^/auth-service/v1/register$ 42 | method: POST 43 | response: 44 | - status: 500 45 | latency: 500 46 | headers: 47 | content-type: application/json 48 | - status: 500 49 | latency: 1000 50 | headers: 51 | content-type: application/json 52 | - status: 200 53 | latency: 3000 54 | headers: 55 | content-type: application/json 56 | file: ./user.json 57 | 58 | - request: 59 | url: ^/admin/v1/users$ 60 | method: POST 61 | response: 62 | - status: 201 63 | latency: 500 64 | headers: 65 | content-type: application/json 66 | 67 | - request: 68 | url: ^/admin/v1/users$ 69 | method: GET 70 | response: 71 | - status: 200 72 | latency: 500 73 | headers: 74 | content-type: application/json 75 | file: ./users.json 76 | 77 | - request: 78 | url: ^/auth-service/v1/groups$ 79 | method: GET 80 | response: 81 | status: 200 82 | latency: 1000 83 | headers: 84 | content-type: application/json 85 | file: ./groups.json 86 | 87 | - request: 88 | url: ^/auth-service/v1/user$ 89 | method: GET 90 | response: 91 | status: 200 92 | latency: 1000 93 | headers: 94 | content-type: application/json 95 | file: ./users.json 96 | -------------------------------------------------------------------------------- /src/app/modules/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 | Email 8 | 9 | email 10 | Type your email 11 | 12 | {{ email.errors | json }} 13 | 14 | 15 | 16 | 17 | Password 18 | 19 | code 20 | Type your password 21 | 22 | {{ password.errors | json }} 23 | 24 | 25 | 26 | 27 | Grupo 28 | 29 | Seleccione 30 | {{group.value}} 31 | 32 | Seleccione su grupo 33 | 34 | {{ group.errors | json }} 35 | 36 | 37 | 38 | 39 | Recuérdame 40 | {{formModel | json}} 41 | 42 | 45 | 46 | 47 | how_to_reg Register 48 | 49 | 50 | 51 |
52 | 53 |
54 | 55 | -------------------------------------------------------------------------------- /src/app/pages/home/pages/list/list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { MatTableDataSource } from '@angular/material/table'; 3 | import { MatPaginator } from '@angular/material/paginator'; 4 | import { MatSort } from '@angular/material/sort'; 5 | 6 | export interface UserData { 7 | id: string; 8 | name: string; 9 | progress: string; 10 | color: string; 11 | } 12 | 13 | /** Constants used to fill up our data base. */ 14 | const COLORS: string[] = ['maroon', 'red', 'orange', 'yellow', 'olive', 'green', 'purple', 15 | 'fuchsia', 'lime', 'teal', 'aqua', 'blue', 'navy', 'black', 'gray']; 16 | const NAMES: string[] = ['Maia', 'Asher', 'Olivia', 'Atticus', 'Amelia', 'Jack', 17 | 'Charlotte', 'Theodore', 'Isla', 'Oliver', 'Isabella', 'Jasper', 18 | 'Cora', 'Levi', 'Violet', 'Arthur', 'Mia', 'Thomas', 'Elizabeth']; 19 | 20 | @Component({ 21 | selector: 'app-list', 22 | templateUrl: './list.component.html', 23 | styleUrls: ['./list.component.scss'] 24 | }) 25 | export class ListComponent implements OnInit { 26 | 27 | displayedColumns: string[] = ['id', 'name', 'progress', 'color']; 28 | dataSource: MatTableDataSource; 29 | 30 | @ViewChild(MatPaginator) paginator: MatPaginator; 31 | @ViewChild(MatSort) sort: MatSort; 32 | 33 | constructor() { 34 | // Create 100 users 35 | const users = Array.from({ length: 100 }, (_, k) => createNewUser(k + 1)); 36 | 37 | // Assign the data to the data source for the table to render 38 | this.dataSource = new MatTableDataSource(users); 39 | } 40 | 41 | ngOnInit() { 42 | this.dataSource.paginator = this.paginator; 43 | this.dataSource.sort = this.sort; 44 | } 45 | 46 | applyFilter(filterValue: string) { 47 | this.dataSource.filter = filterValue.trim().toLowerCase(); 48 | 49 | if (this.dataSource.paginator) { 50 | this.dataSource.paginator.firstPage(); 51 | } 52 | } 53 | } 54 | 55 | /** Builds and returns a new User. */ 56 | function createNewUser(id: number): UserData { 57 | const name = 58 | NAMES[Math.round(Math.random() * (NAMES.length - 1))] + ' ' + 59 | NAMES[Math.round(Math.random() * (NAMES.length - 1))].charAt(0) + '.'; 60 | 61 | return { 62 | id: id.toString(), 63 | name: name, 64 | progress: Math.round(Math.random() * 100).toString(), 65 | color: COLORS[Math.round(Math.random() * (COLORS.length - 1))] 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "no-output-on-prefix": true, 121 | "use-input-property-decorator": true, 122 | "use-output-property-decorator": true, 123 | "use-host-property-decorator": true, 124 | "no-input-rename": true, 125 | "no-output-rename": true, 126 | "use-life-cycle-interface": true, 127 | "use-pipe-transform-interface": true, 128 | "component-class-suffix": true, 129 | "directive-class-suffix": true 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** 38 | * If the application will be indexed by Google Search, the following is required. 39 | * Googlebot uses a renderer based on Chrome 41. 40 | * https://developers.google.com/search/docs/guides/rendering 41 | **/ 42 | // import 'core-js/es6/array'; 43 | 44 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 45 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 46 | 47 | /** IE10 and IE11 requires the following for the Reflect API. */ 48 | // import 'core-js/es6/reflect'; 49 | 50 | /** 51 | * Web Animations `@angular/platform-browser/animations` 52 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 53 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 54 | **/ 55 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 56 | 57 | /** 58 | * By default, zone.js will patch all possible macroTask and DomEvents 59 | * user can disable parts of macroTask/DomEvents patch by setting following flags 60 | */ 61 | 62 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 63 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 64 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 65 | 66 | /* 67 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 68 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 69 | */ 70 | // (window as any).__Zone_enable_cross_context_check = true; 71 | 72 | /*************************************************************************************************** 73 | * Zone JS is required by default for Angular itself. 74 | */ 75 | import 'zone.js/dist/zone'; // Included with Angular CLI. 76 | 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | -------------------------------------------------------------------------------- /src/app/pages/admin/admin.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | Full Name 7 | 8 | full name 9 | Enter your full name 10 | 11 | {{ form.get('fullName').errors | json }} 12 | 13 | 14 | 15 | 16 | Email 17 | 18 | email 19 | Type your email 20 | 21 | {{ form.get('email').errors | json }} 22 | 23 | 24 | 25 | 26 | Password 27 | 28 | password 29 | Type your password 30 | 31 | {{ form.get('password').errors | json }} 32 | 33 | 34 | 35 | Grupo 36 | 37 | Seleccione 38 | {{group.value}} 39 | 41 | 42 | Seleccione su grupo 43 | 44 | {{ form.get('group').errors | json }} 45 | 46 | 47 | 48 | 51 | 52 | 53 | 54 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
Name {{user.fullName}} Email {{user.email}} Group {{user.group}}
77 |
78 |
79 | 80 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "defaultProject": "web-app", 6 | "projects": { 7 | "web-app": { 8 | "root": "", 9 | "sourceRoot": "src", 10 | "projectType": "application", 11 | "prefix": "app", 12 | "schematics": { 13 | "@schematics/angular:component": { 14 | "styleext": "scss" 15 | } 16 | }, 17 | "architect": { 18 | "build": { 19 | "builder": "@angular-devkit/build-angular:browser", 20 | "options": { 21 | "outputPath": "dist/web-app", 22 | "index": "src/index.html", 23 | "main": "src/main.ts", 24 | "polyfills": "src/polyfills.ts", 25 | "tsConfig": "src/tsconfig.app.json", 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", 32 | "src/styles.scss" 33 | ], 34 | "scripts": [] 35 | }, 36 | "configurations": { 37 | "production": { 38 | "fileReplacements": [ 39 | { 40 | "replace": "src/environments/environment.ts", 41 | "with": "src/environments/environment.prod.ts" 42 | } 43 | ], 44 | "optimization": true, 45 | "outputHashing": "all", 46 | "sourceMap": false, 47 | "extractCss": true, 48 | "namedChunks": false, 49 | "aot": true, 50 | "extractLicenses": true, 51 | "vendorChunk": false, 52 | "buildOptimizer": true, 53 | "budgets": [ 54 | { 55 | "type": "initial", 56 | "maximumWarning": "2mb", 57 | "maximumError": "5mb" 58 | } 59 | ] 60 | } 61 | } 62 | }, 63 | "serve": { 64 | "builder": "@angular-devkit/build-angular:dev-server", 65 | "options": { 66 | "browserTarget": "web-app:build" 67 | }, 68 | "configurations": { 69 | "production": { 70 | "browserTarget": "web-app:build:production" 71 | } 72 | } 73 | }, 74 | "extract-i18n": { 75 | "builder": "@angular-devkit/build-angular:extract-i18n", 76 | "options": { 77 | "browserTarget": "web-app:build" 78 | } 79 | }, 80 | "test": { 81 | "builder": "@angular-devkit/build-angular:karma", 82 | "options": { 83 | "main": "src/test.ts", 84 | "polyfills": "src/polyfills.ts", 85 | "tsConfig": "src/tsconfig.spec.json", 86 | "karmaConfig": "src/karma.conf.js", 87 | "styles": [ 88 | "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", 89 | "src/styles.scss" 90 | ], 91 | "scripts": [], 92 | "assets": [ 93 | "src/favicon.ico", 94 | "src/assets" 95 | ] 96 | } 97 | }, 98 | "e2e": { 99 | "builder": "@angular-devkit/build-angular:protractor", 100 | "options": { 101 | "protractorConfig": "e2e/protractor.conf.js", 102 | "devServerTarget": "web-app:serve" 103 | }, 104 | "configurations": { 105 | "production": { 106 | "devServerTarget": "web-app:serve:production" 107 | } 108 | } 109 | }, 110 | "lint": { 111 | "builder": "@angular-devkit/build-angular:tslint", 112 | "options": { 113 | "tsConfig": [ 114 | "src/tsconfig.app.json", 115 | "src/tsconfig.spec.json" 116 | ], 117 | "exclude": [ 118 | "**/node_modules/**" 119 | ] 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /src/app/modules/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 6 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 7 | 8 | import { MatFormFieldModule } from '@angular/material/form-field'; 9 | import { MatIconModule } from '@angular/material/icon'; 10 | import { MatSelectModule } from '@angular/material/select'; 11 | import { MatCardModule } from '@angular/material/card'; 12 | import { MatButtonModule } from '@angular/material/button'; 13 | import { MatInputModule } from '@angular/material/input'; 14 | import { MatCheckboxModule } from '@angular/material/checkbox'; 15 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 16 | 17 | import { LoginService } from './login.service'; 18 | import { LoginComponent } from './login.component'; 19 | import { GroupService } from './group.service'; 20 | import { CoreModule } from '../core'; 21 | 22 | describe('LoginComponent', () => { 23 | let component: LoginComponent; 24 | let fixture: ComponentFixture; 25 | const authenticateSpy = jasmine.createSpy('loginService.authenticate'); 26 | const getGroupsSpy = jasmine.createSpy('groupService.getGroups'); 27 | 28 | class LoginServiceStub { 29 | // se maneja de esta forma porque loginService es privado 30 | authenticate = authenticateSpy; 31 | } 32 | 33 | class GroupServiceStub { 34 | getGroups = getGroupsSpy; 35 | } 36 | 37 | beforeEach(() => { 38 | TestBed.configureTestingModule({ 39 | imports: [ 40 | FormsModule, 41 | RouterTestingModule, 42 | HttpClientTestingModule, 43 | NoopAnimationsModule, 44 | 45 | MatFormFieldModule, 46 | MatIconModule, 47 | MatSelectModule, 48 | MatCardModule, 49 | CoreModule, 50 | MatCheckboxModule, 51 | MatButtonModule, 52 | MatInputModule, 53 | MatSnackBarModule 54 | ], 55 | declarations: [ LoginComponent ], 56 | providers: [ 57 | { 58 | provide: GroupService, 59 | useClass: GroupServiceStub, 60 | }, 61 | { 62 | provide: LoginService, 63 | useClass: LoginServiceStub, 64 | } 65 | ], 66 | schemas: [ CUSTOM_ELEMENTS_SCHEMA ] 67 | }).compileComponents(); 68 | }); 69 | 70 | beforeEach(() => { 71 | authenticateSpy.calls.reset(); 72 | getGroupsSpy.calls.reset(); 73 | fixture = TestBed.createComponent(LoginComponent); 74 | component = fixture.componentInstance; 75 | 76 | getGroupsSpy.and.returnValue(Promise.resolve({})); 77 | fixture.detectChanges(); 78 | }); 79 | 80 | it('should create', () => { 81 | expect(component).toBeTruthy(); 82 | }); 83 | 84 | // https://codecraft.tv/courses/angular/unit-testing/asynchronous/ 85 | 86 | it('should submit and call authenticate method when the loginForm it is valid', async(() => { 87 | // "A"rrange 88 | authenticateSpy.and.returnValue(Promise.resolve(true)); 89 | 90 | fixture.whenStable() 91 | .then(() => { 92 | // "A"rrange 93 | component.loginForm.setValue({ 94 | email: 'g.pincheira.a@gmail.com', 95 | password: 'superscret123765', 96 | group: 'A', 97 | rememberMe: true 98 | }); 99 | // "A"ct -- Mostrar como esto produce en la terminal : 100 | // WARN LOG: 'Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?' 101 | // WARN: 'Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?' 102 | // Explicar que quiere decir haciendo switch en karma conf de version headless a chrome 103 | 104 | component.submit(); 105 | 106 | // "A"ssert 107 | expect(authenticateSpy).toHaveBeenCalled(); 108 | // mostrar como se puede dar más cobertura y fidelidad a la prueba 109 | // pero que esto no sera reflejado en las métricas. 110 | // Agregar cobertura para variable loading 111 | }); 112 | })); 113 | 114 | // EJERCICIO PARA AUMENTAR BRANCHES COVERAGE 115 | // Mostrar como mejorar la calidad del test corroborando 116 | // que el loader se cerro para el caso donde retornamos false 117 | // para la versión authenticate 118 | 119 | // it('should not call submit and not authenticate method when the loginForm it is invalid ', async(() => { 120 | // authenticateSpy.and.returnValue(Promise.resolve(true)); 121 | 122 | // fixture.whenStable().then(() => { 123 | // component.submit(); 124 | 125 | // expect(authenticateSpy).not.toHaveBeenCalled(); 126 | // }); 127 | // })); 128 | 129 | // Aumentar cobertura para caso donde authenticate retorne una promesa 130 | // rechazada. Acá haremos mock de snackbar y generaremos un objeto Error 131 | // it('should handle error when authenticate method returns error ', async(() => { 132 | // authenticateSpy.and.returnValue(Promise.reject('El sistema está abajo')); 133 | 134 | // fixture.whenStable().then(() => { 135 | // component.submit(); 136 | 137 | // expect(authenticateSpy).not.toHaveBeenCalled(); 138 | // }); 139 | // })); 140 | }); 141 | -------------------------------------------------------------------------------- /src/app/modules/login/login.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 6 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 7 | 8 | import { MatFormFieldModule } from '@angular/material/form-field'; 9 | import { MatIconModule } from '@angular/material/icon'; 10 | import { MatSelectModule } from '@angular/material/select'; 11 | import { MatCardModule } from '@angular/material/card'; 12 | import { MatButtonModule } from '@angular/material/button'; 13 | import { MatInputModule } from '@angular/material/input'; 14 | import { MatCheckboxModule } from '@angular/material/checkbox'; 15 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 16 | 17 | import { LoginService } from './login.service'; 18 | import { LoginComponent } from './login.component'; 19 | import { GroupService } from './group.service'; 20 | import { CoreModule } from '../core'; 21 | import { of } from 'rxjs'; 22 | 23 | 24 | describe('LoginComponent', () => { 25 | let component: LoginComponent; 26 | let fixture: ComponentFixture; 27 | const authenticateSpy = jasmine.createSpy('loginService.authenticate'); 28 | const getGroupsSpy = jasmine.createSpy('groupService.getGroups'); 29 | 30 | class LoginServiceStub { 31 | // se maneja de esta forma porque loginService es privado 32 | authenticate = authenticateSpy; 33 | } 34 | 35 | class GroupServiceStub { 36 | getGroups = getGroupsSpy; 37 | } 38 | 39 | beforeEach(() => { 40 | TestBed.configureTestingModule({ 41 | imports: [ 42 | FormsModule, 43 | RouterTestingModule, 44 | HttpClientTestingModule, 45 | NoopAnimationsModule, 46 | MatFormFieldModule, 47 | MatIconModule, 48 | MatSelectModule, 49 | MatCardModule, 50 | CoreModule, 51 | MatCheckboxModule, 52 | MatButtonModule, 53 | MatInputModule, 54 | MatSnackBarModule 55 | ], 56 | declarations: [LoginComponent], 57 | providers: [ 58 | { 59 | provide: GroupService, 60 | useClass: GroupServiceStub, 61 | }, 62 | { 63 | provide: LoginService, 64 | useClass: LoginServiceStub, 65 | } 66 | ], 67 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 68 | }).compileComponents(); 69 | }); 70 | 71 | beforeEach(() => { 72 | authenticateSpy.calls.reset(); 73 | getGroupsSpy.calls.reset(); 74 | fixture = TestBed.createComponent(LoginComponent); 75 | component = fixture.componentInstance; 76 | 77 | getGroupsSpy.and.returnValue(of({})); 78 | fixture.detectChanges(); 79 | }); 80 | 81 | it('should create', () => { 82 | expect(component).toBeTruthy(); 83 | }); 84 | 85 | // https://codecraft.tv/courses/angular/unit-testing/asynchronous/ 86 | 87 | it('should submit and call authenticate method when the loginForm it is valid', async(() => { 88 | // "A"rrange 89 | authenticateSpy.and.returnValue(of(true)); 90 | 91 | fixture.whenStable() 92 | .then(() => { 93 | // "A"rrange 94 | component.loginForm.setValue({ 95 | email: 'g.pincheira.a@gmail.com', 96 | password: 'superscret123765', 97 | group: 'A', 98 | rememberMe: true 99 | }); 100 | // "A"ct -- Mostrar como esto produce en la terminal : 101 | // WARN LOG: 'Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?' 102 | // WARN: 'Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?' 103 | // Explicar que quiere decir haciendo switch en karma conf de version headless a chrome 104 | 105 | component.submit(); 106 | 107 | // "A"ssert 108 | expect(authenticateSpy).toHaveBeenCalled(); 109 | // mostrar como se puede dar más cobertura y fidelidad a la prueba 110 | // pero que esto no sera reflejado en las métricas. 111 | // Agregar cobertura para variable loading 112 | }); 113 | })); 114 | 115 | // EJERCICIO PARA AUMENTAR BRANCHES COVERAGE 116 | // Mostrar como mejorar la calidad del test corroborando 117 | // que el loader se cerro para el caso donde retornamos false 118 | // para la versión authenticate 119 | 120 | // it('should not call submit and not authenticate method when the loginForm it is invalid ', async(() => { 121 | // authenticateSpy.and.returnValue(Promise.resolve(true)); 122 | 123 | // fixture.whenStable().then(() => { 124 | // component.submit(); 125 | 126 | // expect(authenticateSpy).not.toHaveBeenCalled(); 127 | // }); 128 | // })); 129 | 130 | // Aumentar cobertura para caso donde authenticate retorne una promesa 131 | // rechazada. Acá haremos mock de snackbar y generaremos un objeto Error 132 | // it('should handle error when authenticate method returns error ', async(() => { 133 | // authenticateSpy.and.returnValue(Promise.reject('El sistema está abajo')); 134 | 135 | // fixture.whenStable().then(() => { 136 | // component.submit(); 137 | 138 | // expect(authenticateSpy).not.toHaveBeenCalled(); 139 | // }); 140 | // })); 141 | }); 142 | --------------------------------------------------------------------------------