├── 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 | LIKE
13 | SHARE
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 | LIKE
29 | SHARE
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 |
--------------------------------------------------------------------------------
/src/app/pages/register/register.component.html:
--------------------------------------------------------------------------------
1 |
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 | ID
18 | {{row.id}}
19 |
20 |
21 |
22 |
23 | Progress
24 | {{row.progress}}%
25 |
26 |
27 |
28 |
29 | Name
30 | {{row.name}}
31 |
32 |
33 |
34 |
35 | Color
36 | {{row.color}}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | LIKE
48 | SHARE
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 |
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 |
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 |
--------------------------------------------------------------------------------