├── src ├── app │ ├── app.component.scss │ ├── about │ │ ├── about.component.scss │ │ ├── about.component.html │ │ ├── about.component.ts │ │ ├── about.module.ts │ │ ├── about-routing.module.ts │ │ └── about.component.spec.ts │ ├── shell │ │ ├── shell.component.scss │ │ ├── shell.component.html │ │ ├── header │ │ │ ├── header.component.scss │ │ │ ├── header.component.ts │ │ │ ├── header.component.spec.ts │ │ │ └── header.component.html │ │ ├── shell.component.ts │ │ ├── shell.service.ts │ │ ├── shell.module.ts │ │ ├── shell.service.spec.ts │ │ └── shell.component.spec.ts │ ├── app.component.html │ ├── @shared │ │ ├── loader │ │ │ ├── loader.component.scss │ │ │ ├── loader.component.html │ │ │ ├── loader.component.ts │ │ │ └── loader.component.spec.ts │ │ ├── index.ts │ │ ├── shared.module.ts │ │ ├── http │ │ │ ├── api-prefix.interceptor.ts │ │ │ ├── error-handler.interceptor.ts │ │ │ ├── api-prefix.interceptor.spec.ts │ │ │ └── error-handler.interceptor.spec.ts │ │ ├── route-reusable-strategy.ts │ │ ├── logger.service.spec.ts │ │ └── logger.service.ts │ ├── i18n │ │ ├── language-selector.component.scss │ │ ├── index.ts │ │ ├── i18n.module.ts │ │ ├── language-selector.component.html │ │ ├── language-selector.component.ts │ │ ├── language-selector.component.spec.ts │ │ ├── i18n.service.ts │ │ └── i18n.service.spec.ts │ ├── home │ │ ├── home.component.scss │ │ ├── home.component.html │ │ ├── home.module.ts │ │ ├── home-routing.module.ts │ │ ├── home.component.ts │ │ ├── quote.service.ts │ │ ├── home.component.spec.ts │ │ └── quote.service.spec.ts │ ├── auth │ │ ├── index.ts │ │ ├── credentials.service.mock.ts │ │ ├── auth-routing.module.ts │ │ ├── login.component.scss │ │ ├── authentication.service.mock.ts │ │ ├── auth.module.ts │ │ ├── authentication.guard.ts │ │ ├── authentication.service.ts │ │ ├── login.component.spec.ts │ │ ├── login.component.ts │ │ ├── credentials.service.ts │ │ ├── credentials.service.spec.ts │ │ ├── authentication.guard.spec.ts │ │ ├── login.component.html │ │ └── authentication.service.spec.ts │ ├── app-routing.module.ts │ ├── app.component.spec.ts │ ├── app.module.ts │ └── app.component.ts ├── favicon.ico ├── apple-touch-icon.png ├── robots.txt ├── assets │ ├── ngx-rocket-logo.png │ └── ngx-rocket-logo@192.png ├── theme │ ├── theme.scss │ └── theme-variables.scss ├── typings.d.ts ├── translations │ ├── en-US.json │ └── fr-FR.json ├── main.ts ├── manifest.webmanifest ├── main.scss ├── test-config.helper.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── setup-jest.ts └── polyfills.ts ├── .husky └── pre-commit ├── docs ├── readme.md ├── analytics.md ├── updating.md ├── corporate-proxy.md ├── i18n.md ├── backend-proxy.md ├── coding-guides │ ├── html.md │ ├── unit-tests.md │ ├── typescript.md │ ├── sass.md │ ├── angular.md │ └── build-specific-configurations.md └── routing.md ├── tsconfig.app.json ├── tsconfig.spec.json ├── .editorconfig ├── .browserslistrc ├── ngsw-config.json ├── jest.config.js ├── .gitignore ├── .eslintrc.json ├── proxy.conf.js ├── tsconfig.json ├── .stylelintrc ├── tslint.json ├── package.json ├── angular.json └── README.md /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/about/about.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/shell/shell.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngx-rocket/starter-kit/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/app/@shared/loader/loader.component.scss: -------------------------------------------------------------------------------- 1 | .fa { 2 | vertical-align: middle; 3 | } 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /src/app/shell/shell.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/i18n/language-selector.component.scss: -------------------------------------------------------------------------------- 1 | .nav-link.dropdown-toggle { 2 | cursor: pointer; 3 | } 4 | -------------------------------------------------------------------------------- /src/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngx-rocket/starter-kit/HEAD/src/apple-touch-icon.png -------------------------------------------------------------------------------- /src/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /src/assets/ngx-rocket-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngx-rocket/starter-kit/HEAD/src/assets/ngx-rocket-logo.png -------------------------------------------------------------------------------- /src/assets/ngx-rocket-logo@192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngx-rocket/starter-kit/HEAD/src/assets/ngx-rocket-logo@192.png -------------------------------------------------------------------------------- /src/theme/theme.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Global application theme. 3 | * Framework overrides and customization goes here. 4 | */ 5 | 6 | -------------------------------------------------------------------------------- /src/app/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export * from './i18n.module'; 2 | export * from './i18n.service'; 3 | export * from './language-selector.component'; 4 | -------------------------------------------------------------------------------- /src/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | .logo { 2 | width: 100px; 3 | } 4 | 5 | q { 6 | font-style: italic; 7 | font-size: 1.2rem; 8 | quotes: "« " " »"; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/@shared/loader/loader.component.html: -------------------------------------------------------------------------------- 1 |
2 | {{message}} 3 |
4 | 5 | -------------------------------------------------------------------------------- /src/app/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.module'; 2 | export * from './authentication.service'; 3 | export * from './credentials.service'; 4 | export * from './authentication.guard'; 5 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # ngX-Rocket 2 | 3 | Welcome to the project documentation! 4 | 5 | Use `npm run docs` for easier navigation. 6 | 7 | ## Available documentation 8 | 9 | [[index]] 10 | -------------------------------------------------------------------------------- /src/app/shell/header/header.component.scss: -------------------------------------------------------------------------------- 1 | @import "src/theme/theme-variables"; 2 | 3 | .navbar { 4 | margin-bottom: $spacer; 5 | } 6 | 7 | .nav-link.dropdown-toggle { 8 | cursor: pointer; 9 | } 10 | -------------------------------------------------------------------------------- /docs/analytics.md: -------------------------------------------------------------------------------- 1 | # Analytics 2 | 3 | This project does not come with any analytics library. 4 | Should you decide to use one, you may want to consider [Angulartics2](https://github.com/angulartics/angulartics2). 5 | 6 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Extra typings definitions 3 | */ 4 | 5 | // Allow .json files imports 6 | declare module '*.json'; 7 | 8 | // SystemJS module definition 9 | declare var module: NodeModule; 10 | interface NodeModule { 11 | id: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/about/about.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | APP_NAME 5 |

6 |

7 | Version {{version}} 8 |

9 |
10 |
11 | -------------------------------------------------------------------------------- /src/app/@shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './shared.module'; 2 | export * from './http/api-prefix.interceptor'; 3 | export * from './http/error-handler.interceptor'; 4 | export * from './loader/loader.component'; 5 | export * from './route-reusable-strategy'; 6 | export * from './logger.service'; 7 | export * from '@ngneat/until-destroy'; -------------------------------------------------------------------------------- /src/app/shell/shell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-shell', 5 | templateUrl: './shell.component.html', 6 | styleUrls: ['./shell.component.scss'] 7 | }) 8 | export class ShellComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | 5 | Hello world ! 6 |

7 | 8 | {{quote}} 9 |
10 |
11 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "allowJs": true, 6 | "types": [ 7 | "jest", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.mock.ts", 17 | "src/**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.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 | end_of_line = lf 11 | max_line_length = 120 12 | 13 | [*.ts] 14 | quote_type = single 15 | 16 | [*.md] 17 | max_line_length = off 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | // Fallback when no prior route is matched 6 | { path: '**', redirectTo: '', pathMatch: 'full' } 7 | ]; 8 | 9 | @NgModule({ 10 | imports: [RouterModule.forRoot(routes)], 11 | exports: [RouterModule], 12 | providers: [] 13 | }) 14 | export class AppRoutingModule { } 15 | -------------------------------------------------------------------------------- /src/app/@shared/loader/loader.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-loader', 5 | templateUrl: './loader.component.html', 6 | styleUrls: ['./loader.component.scss'] 7 | }) 8 | export class LoaderComponent implements OnInit { 9 | 10 | @Input() isLoading = false; 11 | @Input() message: string | undefined; 12 | 13 | constructor() { } 14 | 15 | ngOnInit() { } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { environment } from '@env/environment'; 4 | 5 | @Component({ 6 | selector: 'app-about', 7 | templateUrl: './about.component.html', 8 | styleUrls: ['./about.component.scss'] 9 | }) 10 | export class AboutComponent implements OnInit { 11 | 12 | version: string | null = environment.version; 13 | 14 | constructor() { } 15 | 16 | ngOnInit() { } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/auth/credentials.service.mock.ts: -------------------------------------------------------------------------------- 1 | import { Credentials } from './credentials.service'; 2 | 3 | export class MockCredentialsService { 4 | 5 | credentials: Credentials | null = { 6 | username: 'test', 7 | token: '123' 8 | }; 9 | 10 | isAuthenticated(): boolean { 11 | return !!this.credentials; 12 | } 13 | 14 | setCredentials(credentials?: Credentials, _remember?: boolean) { 15 | this.credentials = credentials || null; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/@shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | 5 | import { LoaderComponent } from './loader/loader.component'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | TranslateModule, 10 | CommonModule 11 | ], 12 | declarations: [ 13 | LoaderComponent 14 | ], 15 | exports: [ 16 | LoaderComponent 17 | ] 18 | }) 19 | export class SharedModule { } 20 | -------------------------------------------------------------------------------- /src/app/about/about.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | 5 | import { AboutRoutingModule } from './about-routing.module'; 6 | import { AboutComponent } from './about.component'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, 11 | TranslateModule, 12 | AboutRoutingModule 13 | ], 14 | declarations: [ 15 | AboutComponent 16 | ] 17 | }) 18 | export class AboutModule { } 19 | -------------------------------------------------------------------------------- /src/translations/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "APP_NAME": "ngX-Rocket", 3 | "About": "About", 4 | "Hello world !": "Hello world !", 5 | "Home": "Home", 6 | "Logged in as": "Logged in as", 7 | "Login": "Login", 8 | "Logout": "Logout", 9 | "Password": "Password", 10 | "Password is required": "Password is required", 11 | "Username": "Username", 12 | "Username is required": "Username is required", 13 | "Username or password incorrect.": "Username or password incorrect.", 14 | "Remember me": "Remember me", 15 | "Version": "Version" 16 | } 17 | -------------------------------------------------------------------------------- /src/app/auth/auth-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { marker } from '@biesbjerg/ngx-translate-extract-marker'; 4 | 5 | import { LoginComponent } from './login.component'; 6 | 7 | const routes: Routes = [ 8 | { path: 'login', component: LoginComponent, data: { title: marker('Login') } } 9 | ]; 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule], 14 | providers: [] 15 | }) 16 | export class AuthRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | 5 | import { SharedModule } from '@shared'; 6 | import { HomeRoutingModule } from './home-routing.module'; 7 | import { HomeComponent } from './home.component'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | CommonModule, 12 | TranslateModule, 13 | SharedModule, 14 | HomeRoutingModule 15 | ], 16 | declarations: [ 17 | HomeComponent 18 | ] 19 | }) 20 | export class HomeModule { } 21 | -------------------------------------------------------------------------------- /src/translations/fr-FR.json: -------------------------------------------------------------------------------- 1 | { 2 | "APP_NAME": "ngX-Rocket", 3 | "About": "A propos", 4 | "Hello world !": "Bonjour le monde !", 5 | "Home": "Accueil", 6 | "Logged in as": "Connecté en tant que", 7 | "Login": "Connexion", 8 | "Logout": "Déconnexion", 9 | "Password": "Mot de passe", 10 | "Password is required": "Mot de passe requis", 11 | "Username": "Identifiant", 12 | "Username is required": "Identifiant requis", 13 | "Username or password incorrect.": "Identifiant ou mot de passe incorrect.", 14 | "Remember me": "Rester connecté", 15 | "Version": "Version" 16 | } 17 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Entry point of the application. 3 | * Only platform bootstrapping code should be here. 4 | * For app-specific initialization, use `app/app.component.ts`. 5 | */ 6 | 7 | import { enableProdMode } from '@angular/core'; 8 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 9 | 10 | import { AppModule } from '@app/app.module'; 11 | import { environment } from '@env/environment'; 12 | 13 | if (environment.production) { 14 | enableProdMode(); 15 | } 16 | 17 | platformBrowserDynamic().bootstrapModule(AppModule) 18 | .catch(err => console.error(err)); 19 | -------------------------------------------------------------------------------- /src/app/i18n/i18n.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 5 | 6 | import { LanguageSelectorComponent } from './language-selector.component'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, 11 | TranslateModule, 12 | NgbModule, 13 | ], 14 | declarations: [ 15 | LanguageSelectorComponent, 16 | ], 17 | exports: [ 18 | LanguageSelectorComponent, 19 | ] 20 | }) 21 | export class I18nModule { } 22 | -------------------------------------------------------------------------------- /src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngX-Rocket", 3 | "short_name": "ngX-Rocket", 4 | "theme_color": "#488aff", 5 | "background_color": "#488aff", 6 | "display": "standalone", 7 | "scope": "./", 8 | "start_url": "./", 9 | "icons": [ 10 | { 11 | "src": "assets/ngx-rocket-logo.png", 12 | "sizes": "512x512", 13 | "type": "image/png", 14 | "purpose": "maskable any" 15 | }, 16 | { 17 | "src": "assets/ngx-rocket-logo@192.png", 18 | "sizes": "192x192", 19 | "type": "image/png", 20 | "purpose": "maskable any" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/app/auth/login.component.scss: -------------------------------------------------------------------------------- 1 | @import "src/theme/theme-variables"; 2 | @import "~bootstrap/scss/mixins/breakpoints"; 3 | 4 | .login-container { 5 | position: absolute; 6 | top: 0; 7 | bottom: 0; 8 | left: 0; 9 | right: 0; 10 | } 11 | 12 | .login-box { 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | width: 100%; 18 | min-height: 100%; 19 | } 20 | 21 | .ng-invalid.ng-touched:not(form) { 22 | border-left: 4px solid theme-color("danger"); 23 | } 24 | 25 | @include media-breakpoint-down(xs) { 26 | .container { 27 | width: 100%; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/about/about-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { marker } from '@biesbjerg/ngx-translate-extract-marker'; 4 | 5 | import { Shell } from '@app/shell/shell.service'; 6 | import { AboutComponent } from './about.component'; 7 | 8 | const routes: Routes = [ 9 | Shell.childRoutes([ 10 | { path: 'about', component: AboutComponent, data: { title: marker('About') } } 11 | ]) 12 | ]; 13 | 14 | @NgModule({ 15 | imports: [RouterModule.forChild(routes)], 16 | exports: [RouterModule], 17 | providers: [] 18 | }) 19 | export class AboutRoutingModule { } 20 | -------------------------------------------------------------------------------- /src/app/auth/authentication.service.mock.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | 3 | import { LoginContext } from './authentication.service'; 4 | import { Credentials } from './credentials.service'; 5 | 6 | export class MockAuthenticationService { 7 | 8 | credentials: Credentials | null = { 9 | username: 'test', 10 | token: '123' 11 | }; 12 | 13 | login(context: LoginContext): Observable { 14 | return of({ 15 | username: context.username, 16 | token: '123456' 17 | }); 18 | } 19 | 20 | logout(): Observable { 21 | this.credentials = null; 22 | return of(true); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /src/main.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Entry point of global application style. 3 | * Component-specific style should not go here and be included directly as part of the components. 4 | */ 5 | 6 | // Theme variables, must be included before the libraries to allow overriding defaults 7 | @import "theme/theme-variables"; 8 | 9 | // 3rd party libraries 10 | @import "~bootstrap/scss/bootstrap"; 11 | @import "~@fortawesome/fontawesome-free/scss/fontawesome"; 12 | @import "~@fortawesome/fontawesome-free/scss/brands"; 13 | @import "~@fortawesome/fontawesome-free/scss/regular"; 14 | @import "~@fortawesome/fontawesome-free/scss/solid"; 15 | 16 | // Theme customization 17 | @import "theme/theme"; 18 | -------------------------------------------------------------------------------- /src/app/home/home-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { marker } from '@biesbjerg/ngx-translate-extract-marker'; 4 | 5 | import { HomeComponent } from './home.component'; 6 | import { Shell } from '@app/shell/shell.service'; 7 | 8 | const routes: Routes = [ 9 | Shell.childRoutes([ 10 | { path: '', redirectTo: '/home', pathMatch: 'full' }, 11 | { path: 'home', component: HomeComponent, data: { title: marker('Home') } } 12 | ]) 13 | ]; 14 | 15 | @NgModule({ 16 | imports: [RouterModule.forChild(routes)], 17 | exports: [RouterModule], 18 | providers: [] 19 | }) 20 | export class HomeRoutingModule { } 21 | -------------------------------------------------------------------------------- /src/app/shell/shell.service.ts: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from '@angular/router'; 2 | 3 | import { AuthenticationGuard } from '@app/auth'; 4 | import { ShellComponent } from './shell.component'; 5 | 6 | /** 7 | * Provides helper methods to create routes. 8 | */ 9 | export class Shell { 10 | 11 | /** 12 | * Creates routes using the shell component and authentication. 13 | * @param routes The routes to add. 14 | * @return The new route using shell as the base. 15 | */ 16 | static childRoutes(routes: Routes): Route { 17 | return { 18 | path: '', 19 | component: ShellComponent, 20 | children: routes, 21 | canActivate: [AuthenticationGuard] 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/i18n/language-selector.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{currentLanguage | translate}} 4 | 5 | 6 | 9 | 10 |
11 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /src/test-config.helper.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | type CompilerOptions = Partial<{ 4 | providers: any[]; 5 | useJit: boolean; 6 | preserveWhitespaces: boolean; 7 | }>; 8 | export type ConfigureFn = (testBed: typeof TestBed) => void; 9 | 10 | export const configureTests = (configure: ConfigureFn, compilerOptions: CompilerOptions = {}) => { 11 | const compilerConfig: CompilerOptions = { 12 | preserveWhitespaces: false, 13 | ...compilerOptions 14 | }; 15 | 16 | const configuredTestBed = TestBed.configureCompiler(compilerConfig); 17 | 18 | configure(configuredTestBed); 19 | 20 | return configuredTestBed.compileComponents().then(() => configuredTestBed); 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 6 | 7 | import { I18nModule } from '@app/i18n'; 8 | import { AuthRoutingModule } from './auth-routing.module'; 9 | import { LoginComponent } from './login.component'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | CommonModule, 14 | ReactiveFormsModule, 15 | TranslateModule, 16 | NgbModule, 17 | I18nModule, 18 | AuthRoutingModule 19 | ], 20 | declarations: [ 21 | LoginComponent 22 | ] 23 | }) 24 | export class AuthModule { } 25 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/manifest.webmanifest", 13 | "/*.css", 14 | "/*.js" 15 | ] 16 | } 17 | }, 18 | { 19 | "name": "assets", 20 | "installMode": "lazy", 21 | "updateMode": "prefetch", 22 | "resources": { 23 | "files": [ 24 | "/assets/**", 25 | "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" 26 | ] 27 | } 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /src/app/about/about.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AboutComponent } from './about.component'; 4 | 5 | describe('AboutComponent', () => { 6 | let component: AboutComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [AboutComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AboutComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { finalize } from 'rxjs/operators'; 3 | 4 | import { QuoteService } from './quote.service'; 5 | 6 | @Component({ 7 | selector: 'app-home', 8 | templateUrl: './home.component.html', 9 | styleUrls: ['./home.component.scss'] 10 | }) 11 | export class HomeComponent implements OnInit { 12 | 13 | quote: string | undefined; 14 | isLoading = false; 15 | 16 | constructor(private quoteService: QuoteService) { } 17 | 18 | ngOnInit() { 19 | this.isLoading = true; 20 | this.quoteService.getRandomQuote({ category: 'dev' }) 21 | .pipe(finalize(() => { this.isLoading = false; })) 22 | .subscribe((quote: string) => { this.quote = quote; }); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/app/@shared/http/api-prefix.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { environment } from '@env/environment'; 6 | 7 | /** 8 | * Prefixes all requests not starting with `http[s]` with `environment.serverUrl`. 9 | */ 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class ApiPrefixInterceptor implements HttpInterceptor { 14 | 15 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 16 | if (!/^(http|https):/i.test(request.url)) { 17 | request = request.clone({ url: environment.serverUrl + request.url }); 18 | } 19 | return next.handle(request); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | // `.env.ts` is generated by the `npm run env` command 2 | // `npm run env` exposes environment variables as JSON for any usage you might 3 | // want, like displaying the version or getting extra config from your CI bot, etc. 4 | // This is useful for granularity you might need beyond just the environment. 5 | // Note that as usual, any environment variables you expose through it will end up in your 6 | // bundle, and you should not use it for any sensitive information like passwords or keys. 7 | import { env } from './.env'; 8 | 9 | export const environment = { 10 | production: true, 11 | version: env['npm_package_version'], 12 | serverUrl: 'https://api.chucknorris.io', 13 | defaultLanguage: 'en-US', 14 | supportedLanguages: ['en-US', 'fr-FR'] 15 | }; 16 | -------------------------------------------------------------------------------- /src/app/shell/shell.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { RouterModule } from '@angular/router'; 5 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 6 | 7 | import { I18nModule } from '@app/i18n'; 8 | import { AuthModule } from '@app/auth'; 9 | import { ShellComponent } from './shell.component'; 10 | import { HeaderComponent } from './header/header.component'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, 15 | TranslateModule, 16 | NgbModule, 17 | AuthModule, 18 | I18nModule, 19 | RouterModule 20 | ], 21 | declarations: [ 22 | HeaderComponent, 23 | ShellComponent 24 | ] 25 | }) 26 | export class ShellModule { 27 | } 28 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | 5 | import { AppComponent } from './app.component'; 6 | 7 | describe('AppComponent', () => { 8 | 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [ 13 | RouterTestingModule, 14 | TranslateModule.forRoot() 15 | ], 16 | declarations: [AppComponent], 17 | providers: [] 18 | }).compileComponents(); 19 | })); 20 | 21 | it('should create the app', waitForAsync(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | const app = fixture.debugElement.componentInstance; 24 | expect(app).toBeTruthy(); 25 | }), 30000); 26 | }); 27 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest'); 2 | // In the following statement, replace `./tsconfig` with the path to your `tsconfig` file 3 | // which contains the path mapping (ie the `compilerOptions.paths` option): 4 | const { compilerOptions } = require('./tsconfig.json'); 5 | 6 | module.exports = { 7 | preset: 'jest-preset-angular', 8 | roots: ['src'], 9 | coverageDirectory: 'reports', 10 | setupFilesAfterEnv: ['/src/setup-jest.ts'], 11 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/' }), 12 | globals: { 13 | 'ts-jest': { 14 | allowSyntheticDefaultImports: true, 15 | tsconfig: '/tsconfig.spec.json', 16 | diagnostics: { 17 | ignoreCodes: ['TS151001'], 18 | } 19 | }, 20 | }, 21 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'] 22 | }; 23 | -------------------------------------------------------------------------------- /src/app/i18n/language-selector.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | import { I18nService } from './i18n.service'; 4 | 5 | @Component({ 6 | selector: 'app-language-selector', 7 | templateUrl: './language-selector.component.html', 8 | styleUrls: ['./language-selector.component.scss'] 9 | }) 10 | export class LanguageSelectorComponent implements OnInit { 11 | @Input() inNavbar = false; 12 | @Input() menuClass = ''; 13 | 14 | constructor( 15 | private i18nService: I18nService 16 | ) { } 17 | 18 | ngOnInit() { } 19 | 20 | setLanguage(language: string) { 21 | this.i18nService.language = language; 22 | } 23 | 24 | get currentLanguage(): string { 25 | return this.i18nService.language; 26 | } 27 | 28 | get languages(): string[] { 29 | return this.i18nService.supportedLanguages; 30 | } 31 | 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/app/home/quote.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable, of } from 'rxjs'; 4 | import { map, catchError } from 'rxjs/operators'; 5 | 6 | const routes = { 7 | quote: (c: RandomQuoteContext) => `/jokes/random?category=${c.category}` 8 | }; 9 | 10 | export interface RandomQuoteContext { 11 | // The quote's category: 'dev', 'explicit'... 12 | category: string; 13 | } 14 | 15 | @Injectable({ 16 | providedIn: 'root' 17 | }) 18 | export class QuoteService { 19 | 20 | constructor(private httpClient: HttpClient) { } 21 | 22 | getRandomQuote(context: RandomQuoteContext): Observable { 23 | return this.httpClient 24 | .get(routes.quote(context)) 25 | .pipe( 26 | map((body: any) => body.value), 27 | catchError(() => of('Error, could not load joke :-(')) 28 | ); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/app/auth/authentication.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | 4 | import { Logger } from '@shared'; 5 | import { CredentialsService } from './credentials.service'; 6 | 7 | const log = new Logger('AuthenticationGuard'); 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class AuthenticationGuard implements CanActivate { 13 | 14 | constructor(private router: Router, 15 | private credentialsService: CredentialsService) { } 16 | 17 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { 18 | if (this.credentialsService.isAuthenticated()) { 19 | return true; 20 | } 21 | 22 | log.debug('Not authenticated, redirecting and adding redirect url...'); 23 | this.router.navigate(['/login'], { queryParams: { redirect: state.url }, replaceUrl: true }); 24 | return false; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | 4 | import { SharedModule } from '@shared'; 5 | import { HomeComponent } from './home.component'; 6 | import { QuoteService } from './quote.service'; 7 | 8 | describe('HomeComponent', () => { 9 | let component: HomeComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(waitForAsync(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [ 15 | SharedModule, 16 | HttpClientTestingModule 17 | ], 18 | declarations: [HomeComponent], 19 | providers: [QuoteService] 20 | }) 21 | .compileComponents(); 22 | })); 23 | 24 | beforeEach(() => { 25 | fixture = TestBed.createComponent(HomeComponent); 26 | component = fixture.componentInstance; 27 | fixture.detectChanges(); 28 | }); 29 | 30 | it('should create', () => { 31 | expect(component).toBeTruthy(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/app/i18n/language-selector.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { TranslateModule } from '@ngx-translate/core'; 3 | 4 | import { I18nService } from './i18n.service'; 5 | import { LanguageSelectorComponent } from './language-selector.component'; 6 | 7 | describe('LanguageSelectorComponent', () => { 8 | let component: LanguageSelectorComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [ 14 | TranslateModule.forRoot() 15 | ], 16 | declarations: [LanguageSelectorComponent], 17 | providers: [I18nService] 18 | }) 19 | .compileComponents(); 20 | })); 21 | 22 | beforeEach(() => { 23 | fixture = TestBed.createComponent(LanguageSelectorComponent); 24 | component = fixture.componentInstance; 25 | fixture.detectChanges(); 26 | }); 27 | 28 | it('should create', () => { 29 | expect(component).toBeTruthy(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/shell/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { AuthenticationService, CredentialsService } from '@app/auth'; 5 | 6 | @Component({ 7 | selector: 'app-header', 8 | templateUrl: './header.component.html', 9 | styleUrls: ['./header.component.scss'] 10 | }) 11 | export class HeaderComponent implements OnInit { 12 | 13 | menuHidden = true; 14 | 15 | constructor(private router: Router, 16 | private authenticationService: AuthenticationService, 17 | private credentialsService: CredentialsService) { } 18 | 19 | ngOnInit() { } 20 | 21 | toggleMenu() { 22 | this.menuHidden = !this.menuHidden; 23 | } 24 | 25 | logout() { 26 | this.authenticationService.logout() 27 | .subscribe(() => this.router.navigate(['/login'], { replaceUrl: true })); 28 | } 29 | 30 | get username(): string | null { 31 | const credentials = this.credentialsService.credentials; 32 | return credentials ? credentials.username : null; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # Dependencies 11 | /node_modules 12 | 13 | # Cordova 14 | /www 15 | /plugins 16 | /platforms 17 | 18 | # Electron 19 | /dist-electron 20 | /dist-packages 21 | /electron.main.js 22 | 23 | # IDEs and editors 24 | .idea/* 25 | !.idea/runConfigurations/ 26 | !.idea/codeStyleSettings.xml 27 | .project 28 | .classpath 29 | .c9/ 30 | *.launch 31 | .settings/ 32 | xcuserdata/ 33 | *.sublime-workspace 34 | 35 | # IDE - VSCode 36 | .vscode/* 37 | !.vscode/settings.json 38 | !.vscode/tasks.json 39 | !.vscode/launch.json 40 | !.vscode/extensions.json 41 | 42 | # Maven 43 | /target 44 | /log 45 | 46 | # Misc 47 | /.angular/cache 48 | /.sass-cache 49 | /connect.lock 50 | /coverage 51 | /libpeerconnection.log 52 | npm-debug.log 53 | yarn-error.log 54 | testem.log 55 | /typings 56 | /reports 57 | /src/translations/template.* 58 | /src/environments/.env.* 59 | /cypress/videos/ 60 | 61 | # System Files 62 | .DS_Store 63 | Thumbs.db 64 | -------------------------------------------------------------------------------- /src/app/@shared/http/error-handler.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | 6 | import { environment } from '@env/environment'; 7 | import { Logger } from '../logger.service'; 8 | 9 | const log = new Logger('ErrorHandlerInterceptor'); 10 | 11 | /** 12 | * Adds a default error handler to all requests. 13 | */ 14 | @Injectable({ 15 | providedIn: 'root' 16 | }) 17 | export class ErrorHandlerInterceptor implements HttpInterceptor { 18 | 19 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 20 | return next.handle(request).pipe(catchError(error => this.errorHandler(error))); 21 | } 22 | 23 | // Customize the default error handler here if needed 24 | private errorHandler(response: HttpEvent): Observable> { 25 | if (!environment.production) { 26 | // Do something with the error 27 | log.error('Request error', response); 28 | } 29 | throw response; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/app/shell/shell.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { AuthenticationGuard, AuthenticationService } from '@app/auth'; 4 | import { MockAuthenticationService } from '@app/auth/authentication.service.mock'; 5 | import { ShellComponent } from './shell.component'; 6 | import { Shell } from './shell.service'; 7 | 8 | describe('Shell', () => { 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ 13 | ShellComponent 14 | ], 15 | providers: [ 16 | AuthenticationGuard, 17 | { provide: AuthenticationService, useClass: MockAuthenticationService }, 18 | ] 19 | }); 20 | }); 21 | 22 | describe('childRoutes', () => { 23 | it('should create routes as children of shell', () => { 24 | // Prepare 25 | const testRoutes = [{ path: 'test' }]; 26 | 27 | // Act 28 | const result = Shell.childRoutes(testRoutes); 29 | 30 | // Assert 31 | expect(result.path).toBe(''); 32 | expect(result.children).toBe(testRoutes); 33 | expect(result.component).toBe(ShellComponent); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ngX-Rocket 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "tsconfig.json" 14 | ], 15 | "createDefaultProgram": true 16 | }, 17 | "extends": [ 18 | "plugin:@angular-eslint/recommended", 19 | "plugin:@angular-eslint/template/process-inline-templates" 20 | ], 21 | "rules": { 22 | "@angular-eslint/directive-selector": [ 23 | "error", 24 | { 25 | "type": "attribute", 26 | "prefix": "app", 27 | "style": "camelCase" 28 | } 29 | ], 30 | "@angular-eslint/component-selector": [ 31 | "error", 32 | { 33 | "type": "element", 34 | "prefix": "app", 35 | "style": "kebab-case" 36 | } 37 | ], 38 | "@angular-eslint/no-empty-lifecycle-method": "off" 39 | } 40 | }, 41 | { 42 | "files": [ 43 | "*.html" 44 | ], 45 | "extends": [ 46 | "plugin:@angular-eslint/template/recommended" 47 | ], 48 | "rules": {} 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /proxy.conf.js: -------------------------------------------------------------------------------- 1 | const HttpsProxyAgent = require('https-proxy-agent'); 2 | 3 | /* 4 | * API proxy configuration. 5 | * This allows you to proxy HTTP request like `http.get('/api/stuff')` to another server/port. 6 | * This is especially useful during app development to avoid CORS issues while running a local server. 7 | * For more details and options, see https://angular.io/guide/build#using-corporate-proxy 8 | */ 9 | const proxyConfig = [ 10 | { 11 | context: '/api', 12 | pathRewrite: { '^/api': '' }, 13 | target: 'https://api.chucknorris.io', 14 | changeOrigin: true, 15 | secure: false 16 | } 17 | ]; 18 | 19 | /* 20 | * Configures a corporate proxy agent for the API proxy if needed. 21 | */ 22 | function setupForCorporateProxy(proxyConfig) { 23 | if (!Array.isArray(proxyConfig)) { 24 | proxyConfig = [proxyConfig]; 25 | } 26 | 27 | const proxyServer = process.env.http_proxy || process.env.HTTP_PROXY; 28 | let agent = null; 29 | 30 | if (proxyServer) { 31 | console.log(`Using corporate proxy server: ${proxyServer}`); 32 | agent = new HttpsProxyAgent(proxyServer); 33 | proxyConfig.forEach(entry => { entry.agent = agent; }); 34 | } 35 | 36 | return proxyConfig; 37 | } 38 | 39 | module.exports = setupForCorporateProxy(proxyConfig); 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "forceConsistentCasingInFileNames": true, 6 | "noImplicitOverride": true, 7 | "noPropertyAccessFromIndexSignature": true, 8 | "noImplicitReturns": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "sourceMap": true, 11 | "declaration": false, 12 | "module": "es2020", 13 | "moduleResolution": "node", 14 | "downlevelIteration": true, 15 | "experimentalDecorators": true, 16 | "importHelpers": true, 17 | "strict": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "target": "es2020", 20 | "typeRoots": [ 21 | "node_modules/@types" 22 | ], 23 | "lib": [ 24 | "es2020", 25 | "dom" 26 | ], 27 | "baseUrl": "./", 28 | "paths": { 29 | "@app/*": ["src/app/*"], 30 | "@shared": ["src/app/@shared"], 31 | "@shared/*": ["src/app/@shared/*"], 32 | "@env/*": ["src/environments/*"] 33 | } 34 | }, 35 | "angularCompilerOptions": { 36 | "enableI18nLegacyMessageIdFormat": false, 37 | "fullTemplateTypeCheck": true, 38 | "strictInjectionParameters": true, 39 | "strictInputAccessModifiers": true, 40 | "strictTemplates": true, 41 | "preserveWhitespaces": true 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/updating.md: -------------------------------------------------------------------------------- 1 | # Updating npm dependencies 2 | 3 | - Check outdated packages 4 | ```sh 5 | npm outdated 6 | ``` 7 | 8 | - Update local packages according to `package.json` 9 | ```sh 10 | npm update 11 | ``` 12 | 13 | - Upgrade packages manually 14 | ```sh 15 | npm install --save[-dev] @latest 16 | ``` 17 | 18 | Alternatively, you can use [npm-check](https://github.com/dylang/npm-check) to perform an interactive upgrade: 19 | ```sh 20 | npm-check -u --skip-unused 21 | ``` 22 | 23 | ## Locking package versions 24 | 25 | Starting from `npm@5` a new `package-lock.json` file is 26 | [automatically generated](https://docs.npmjs.com/files/package-locks) when using `npm install` commands, to ensure a 27 | reproducible dependency tree and avoid unwanted package updates. 28 | 29 | If you use a previous npm version, it is recommended to use [npm shrinkwrap](https://docs.npmjs.com/cli/shrinkwrap) to 30 | lock down all your dependencies version: 31 | ```sh 32 | npm shrinkwrap --dev 33 | ``` 34 | 35 | This will create a file `npm-shrinkwrap.json` alongside your `package.json` files. 36 | 37 | > Do not forget to run shrinkwrap each time you manually update your dependencies! 38 | 39 | # Updating angular-related dependencies 40 | 41 | See the [Angular update website](https://update.angular.io) to guide you through the updating/upgrading steps. 42 | -------------------------------------------------------------------------------- /src/app/@shared/route-reusable-strategy.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | /** 5 | * A route strategy allowing for explicit route reuse. 6 | * Used as a workaround for https://github.com/angular/angular/issues/18374 7 | * To reuse a given route, add `data: { reuse: true }` to the route definition. 8 | */ 9 | @Injectable() 10 | export class RouteReusableStrategy extends RouteReuseStrategy { 11 | 12 | public shouldDetach(route: ActivatedRouteSnapshot): boolean { 13 | return false; 14 | } 15 | 16 | public store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle | null): void { } 17 | 18 | public shouldAttach(route: ActivatedRouteSnapshot): boolean { 19 | return false; 20 | } 21 | 22 | public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { 23 | return null; 24 | } 25 | 26 | public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { 27 | // Reuse the route if the RouteConfig is the same, or if both routes use the 28 | // same component, because the latter can have different RouteConfigs. 29 | return future.routeConfig === curr.routeConfig || 30 | Boolean(future.routeConfig?.component && 31 | future.routeConfig?.component === curr.routeConfig?.component); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/app/auth/authentication.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, of } from 'rxjs'; 3 | 4 | import { Credentials, CredentialsService } from './credentials.service'; 5 | 6 | export interface LoginContext { 7 | username: string; 8 | password: string; 9 | remember?: boolean; 10 | } 11 | 12 | /** 13 | * Provides a base for authentication workflow. 14 | * The login/logout methods should be replaced with proper implementation. 15 | */ 16 | @Injectable({ 17 | providedIn: 'root' 18 | }) 19 | export class AuthenticationService { 20 | 21 | constructor(private credentialsService: CredentialsService) { } 22 | 23 | /** 24 | * Authenticates the user. 25 | * @param context The login parameters. 26 | * @return The user credentials. 27 | */ 28 | login(context: LoginContext): Observable { 29 | // Replace by proper authentication call 30 | const data = { 31 | username: context.username, 32 | token: '123456' 33 | }; 34 | this.credentialsService.setCredentials(data, context.remember); 35 | return of(data); 36 | } 37 | 38 | /** 39 | * Logs out the user and clear credentials. 40 | * @return True if the user was logged out successfully. 41 | */ 42 | logout(): Observable { 43 | // Customize credentials invalidation here 44 | this.credentialsService.setCredentials(); 45 | return of(true); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | // `.env.ts` is generated by the `npm run env` command 6 | // `npm run env` exposes environment variables as JSON for any usage you might 7 | // want, like displaying the version or getting extra config from your CI bot, etc. 8 | // This is useful for granularity you might need beyond just the environment. 9 | // Note that as usual, any environment variables you expose through it will end up in your 10 | // bundle, and you should not use it for any sensitive information like passwords or keys. 11 | import { env } from './.env'; 12 | 13 | export const environment = { 14 | production: false, 15 | version: env['npm_package_version'] + '-dev', 16 | serverUrl: '/api', 17 | defaultLanguage: 'en-US', 18 | supportedLanguages: ['en-US', 'fr-FR'] 19 | }; 20 | 21 | /* 22 | * For easier debugging in development mode, you can import the following file 23 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 24 | * 25 | * This import should be commented out in production mode because it will have a negative impact 26 | * on performance if an error is thrown. 27 | */ 28 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 29 | -------------------------------------------------------------------------------- /src/setup-jest.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | 3 | /* global mocks for jsdom */ 4 | const storageMock = () => { 5 | let storage: { [key: string]: string } = {}; 6 | return { 7 | getItem: (key: string) => (key in storage ? storage[key] : null), 8 | setItem: (key: string, value: string) => (storage[key] = value || ''), 9 | removeItem: (key: string) => delete storage[key], 10 | clear: () => (storage = {}) 11 | }; 12 | }; 13 | 14 | Object.defineProperty(window, 'localStorage', { value: storageMock() }); 15 | Object.defineProperty(window, 'sessionStorage', { value: storageMock() }); 16 | Object.defineProperty(window, 'getComputedStyle', { 17 | value: () => ['-webkit-appearance'] 18 | }); 19 | 20 | Object.defineProperty(document.body.style, 'transform', { 21 | value: () => { 22 | return { 23 | enumerable: true, 24 | configurable: true 25 | }; 26 | } 27 | }); 28 | 29 | Object.defineProperty(window, 'getComputedStyle', { 30 | value: () => ({ 31 | getPropertyValue: (prop: any) => { 32 | return ''; 33 | } 34 | }) 35 | }); 36 | 37 | Object.defineProperty(window, 'matchMedia', { 38 | value: (query: any) => ({ 39 | matches: false, 40 | media: query, 41 | onchange: null as any, 42 | addListener: () => {}, 43 | removeListener: () => {} 44 | }) 45 | }); 46 | 47 | /* output shorter and more meaningful Zone error stack traces */ 48 | // Error.stackTraceLimit = 2; 49 | -------------------------------------------------------------------------------- /src/theme/theme-variables.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Application global variables. 3 | */ 4 | 5 | // Set Font Awesome font path 6 | $fa-font-path: "~@fortawesome/fontawesome-free/webfonts"; 7 | 8 | // --------------------------------------------------------------------------- 9 | // Bootstrap variables 10 | // 11 | // Override Bootstrap variables here to suite your theme. 12 | // Copy variables you want to customize from node_modules/bootstrap/scss/_variables.scss 13 | 14 | // 15 | // Color system 16 | // 17 | 18 | $white: #fff; 19 | $gray-100: #f8f9fa; 20 | $gray-200: #e9ecef; 21 | $gray-300: #dee2e6; 22 | $gray-400: #ced4da; 23 | $gray-500: #adb5bd; 24 | $gray-600: #868e96; 25 | $gray-700: #495057; 26 | $gray-800: #343a40; 27 | $gray-900: #212529; 28 | $black: #000; 29 | 30 | $blue: #0073dd; 31 | $indigo: #6610f2; 32 | $purple: #6f42c1; 33 | $pink: #e83e8c; 34 | $red: #dc3545; 35 | $orange: #fd7e14; 36 | $yellow: #ffc107; 37 | $green: #28a745; 38 | $teal: #20c997; 39 | $cyan: #17a2b8; 40 | 41 | $theme-colors: ( 42 | primary: $blue, 43 | secondary: $gray-600, 44 | success: $green, 45 | info: $cyan, 46 | warning: $yellow, 47 | danger: $red, 48 | light: $gray-100, 49 | dark: $gray-800 50 | ); 51 | 52 | // Use Bootstrap defaults for other variables, imported here so we can access all app variables in one place when used 53 | // in components. 54 | @import "~bootstrap/scss/functions"; 55 | @import "~bootstrap/scss/variables"; 56 | -------------------------------------------------------------------------------- /src/app/auth/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { TranslateModule } from '@ngx-translate/core'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | import { ReactiveFormsModule } from '@angular/forms'; 5 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 6 | 7 | import { AuthenticationService, CredentialsService } from '@app/auth'; 8 | import { MockAuthenticationService } from '@app/auth/authentication.service.mock'; 9 | import { MockCredentialsService } from '@app/auth/credentials.service.mock'; 10 | import { I18nModule } from '@app/i18n'; 11 | import { LoginComponent } from './login.component'; 12 | 13 | describe('LoginComponent', () => { 14 | let component: LoginComponent; 15 | let fixture: ComponentFixture; 16 | 17 | beforeEach(waitForAsync(() => { 18 | TestBed.configureTestingModule({ 19 | imports: [ 20 | NgbModule, 21 | RouterTestingModule, 22 | TranslateModule.forRoot(), 23 | I18nModule, 24 | ReactiveFormsModule 25 | ], 26 | declarations: [LoginComponent] 27 | }) 28 | .compileComponents(); 29 | })); 30 | 31 | beforeEach(() => { 32 | fixture = TestBed.createComponent(LoginComponent); 33 | component = fixture.componentInstance; 34 | fixture.detectChanges(); 35 | }); 36 | 37 | it('should create', () => { 38 | expect(component).toBeTruthy(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /docs/corporate-proxy.md: -------------------------------------------------------------------------------- 1 | # Working behind a corporate proxy 2 | 3 | ## Environment 4 | 5 | Most tools (including npm and git) use the `HTTP_PROXY` and `HTTPS_PROXY` environment variables to work with a 6 | corporate proxy. 7 | 8 | ### Windows 9 | 10 | In Windows environments, add the `HTTP_PROXY` and `HTTPS_PROXY` system environment variable, with these values: 11 | 12 | - HTTP_PROXY: `http://:@:` 13 | - HTTPS_PROXY: `%HTTP_PROXY%` 14 | 15 | ### Unix 16 | 17 | Add these lines to your `~/.bash_profile` or `~/.profile`: 18 | ```sh 19 | export HTTP_PROXY="http://:@:" 20 | export HTTPS_PROXY="$HTTP_PROXY" 21 | ``` 22 | 23 | ## Proxy with SSL custom certificate 24 | 25 | Some proxy like **zscaler** use a custom SSL certificate to inspect request, which may cause npm commands to fail. 26 | 27 | To solve this problem, you can disable the `strict-ssl` option in npm. 28 | 29 | ## Proxy exceptions 30 | 31 | If you need to access repositories on your local network that should bypass proxy, set the `NO_PROXY` environment 32 | variable, in the same way as `HTTP_PROXY`: 33 | 34 | ### Windows 35 | 36 | - NO_PROXY: `127.0.0.1, localhost, ` 37 | 38 | ### Unix 39 | 40 | ```sh 41 | export NO_PROXY="127.0.0.1, localhost, " 42 | ``` 43 | 44 | ### Npm 45 | 46 | Run this command in your project directory: 47 | ```sh 48 | npm set strict-ssl false 49 | ``` 50 | -------------------------------------------------------------------------------- /src/app/@shared/http/api-prefix.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 4 | import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; 5 | 6 | import { environment } from '@env/environment'; 7 | import { ApiPrefixInterceptor } from './api-prefix.interceptor'; 8 | 9 | describe('ApiPrefixInterceptor', () => { 10 | let http: HttpClient; 11 | let httpMock: HttpTestingController; 12 | 13 | beforeEach(() => { 14 | TestBed.configureTestingModule({ 15 | imports: [HttpClientTestingModule], 16 | providers: [{ 17 | provide: HTTP_INTERCEPTORS, 18 | useClass: ApiPrefixInterceptor, 19 | multi: true 20 | }] 21 | }); 22 | 23 | http = TestBed.inject(HttpClient); 24 | httpMock = TestBed.inject(HttpTestingController as Type); 25 | }); 26 | 27 | afterEach(() => { 28 | httpMock.verify(); 29 | }); 30 | 31 | it('should prepend environment.serverUrl to the request url', () => { 32 | // Act 33 | http.get('/toto').subscribe(); 34 | 35 | // Assert 36 | httpMock.expectOne({ url: environment.serverUrl + '/toto' }); 37 | }); 38 | 39 | it('should not prepend environment.serverUrl to request url', () => { 40 | // Act 41 | http.get('hTtPs://domain.com/toto').subscribe(); 42 | 43 | // Assert 44 | httpMock.expectOne({ url: 'hTtPs://domain.com/toto' }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/app/shell/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | 6 | import { AuthenticationService, CredentialsService } from '@app/auth'; 7 | import { MockAuthenticationService } from '@app/auth/authentication.service.mock'; 8 | import { MockCredentialsService } from '@app/auth/credentials.service.mock'; 9 | import { I18nModule } from '@app/i18n'; 10 | import { HeaderComponent } from './header.component'; 11 | 12 | describe('HeaderComponent', () => { 13 | let component: HeaderComponent; 14 | let fixture: ComponentFixture; 15 | 16 | beforeEach(waitForAsync(() => { 17 | TestBed.configureTestingModule({ 18 | imports: [ 19 | RouterTestingModule, 20 | NgbModule, 21 | TranslateModule.forRoot(), 22 | I18nModule 23 | ], 24 | declarations: [HeaderComponent], 25 | providers: [ 26 | { provide: AuthenticationService, useClass: MockAuthenticationService }, 27 | { provide: CredentialsService, useClass: MockCredentialsService } 28 | ] 29 | }) 30 | .compileComponents(); 31 | })); 32 | 33 | beforeEach(() => { 34 | fixture = TestBed.createComponent(HeaderComponent); 35 | component = fixture.componentInstance; 36 | fixture.detectChanges(); 37 | }); 38 | 39 | it('should create', () => { 40 | expect(component).toBeTruthy(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | "stylelint-config-recommended-scss", 5 | "stylelint-config-prettier" 6 | ], 7 | "rules": { 8 | "font-family-name-quotes": "always-where-recommended", 9 | "function-url-quotes": [ 10 | "always", 11 | { 12 | "except": ["empty"] 13 | } 14 | ], 15 | "selector-attribute-quotes": "always", 16 | "string-quotes": "double", 17 | "max-nesting-depth": 3, 18 | "selector-max-compound-selectors": 3, 19 | "selector-max-specificity": "0,3,2", 20 | "declaration-no-important": true, 21 | "at-rule-no-vendor-prefix": true, 22 | "media-feature-name-no-vendor-prefix": true, 23 | "property-no-vendor-prefix": true, 24 | "selector-no-vendor-prefix": true, 25 | "value-no-vendor-prefix": true, 26 | "no-empty-source": null, 27 | "selector-class-pattern": "[a-z-]+", 28 | "selector-id-pattern": "[a-z-]+", 29 | "selector-max-id": 0, 30 | "selector-no-qualifying-type": true, 31 | "selector-max-universal": 0, 32 | "selector-type-no-unknown": [ 33 | true, 34 | { 35 | "ignore": ["custom-elements", "default-namespace"] 36 | } 37 | ], 38 | "selector-pseudo-element-no-unknown": [ 39 | true, 40 | { 41 | "ignorePseudoElements": ["ng-deep"] 42 | } 43 | ], 44 | "unit-allowed-list": ["px", "%", "em", "rem", "vw", "vh", "deg", "s"], 45 | "max-empty-lines": 2, 46 | "max-line-length": 120, 47 | "no-invalid-position-at-import-rule": null, 48 | "scss/comment-no-empty": null 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/shell/shell.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 5 | 6 | import { AuthenticationService, CredentialsService } from '@app/auth'; 7 | import { MockAuthenticationService } from '@app/auth/authentication.service.mock'; 8 | import { MockCredentialsService } from '@app/auth/credentials.service.mock'; 9 | 10 | import { I18nModule } from '@app/i18n'; 11 | import { ShellComponent } from './shell.component'; 12 | import { HeaderComponent } from './header/header.component'; 13 | 14 | describe('ShellComponent', () => { 15 | let component: ShellComponent; 16 | let fixture: ComponentFixture; 17 | 18 | beforeEach(waitForAsync(() => { 19 | TestBed.configureTestingModule({ 20 | imports: [ 21 | TranslateModule.forRoot(), 22 | I18nModule, 23 | NgbModule, 24 | RouterTestingModule 25 | ], 26 | providers: [ 27 | { provide: AuthenticationService, useClass: MockAuthenticationService }, 28 | { provide: CredentialsService, useClass: MockCredentialsService } 29 | ], 30 | declarations: [ 31 | HeaderComponent, 32 | ShellComponent 33 | ] 34 | }) 35 | .compileComponents(); 36 | })); 37 | 38 | beforeEach(() => { 39 | fixture = TestBed.createComponent(ShellComponent); 40 | component = fixture.componentInstance; 41 | fixture.detectChanges(); 42 | }); 43 | 44 | it('should create', () => { 45 | expect(component).toBeTruthy(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /docs/i18n.md: -------------------------------------------------------------------------------- 1 | # I18n 2 | 3 | The internationalization of the application is managed by [ngx-translate](https://github.com/ngx-translate/core). 4 | 5 | ## Adding translatable strings 6 | 7 | ### In HTML templates 8 | 9 | Use the `translate` directive on an HTML element to automatically translate its content: 10 | ```html 11 | This text will be translated. 12 | ``` 13 | 14 | You can also use the `translate` pipe if needed: 15 | ```html 16 | 17 | ``` 18 | 19 | ### In TypeScript code 20 | 21 | If you need to translate strings in JavaScript code, import the `TranslateService` dependency and use the asynchronous 22 | `get()` method: 23 | 24 | ```typescript 25 | let title; 26 | translateService.get('My page title').subscribe((res: string) => { title = res; }); 27 | ``` 28 | 29 | ## Extracting strings to translate 30 | 31 | Once you are ready to translate your app, just run `npm run translations:extract`. 32 | It will create a `template.json` file in the `src/translations` folder. 33 | 34 | You can then use any text or code editor to generate the `.json` files for each of your supported languages, and put 35 | them in the `src/translations` folder. 36 | 37 | Do no forget to edit the files in `src/environment` to add the supported languages of your application. 38 | 39 | ### Marking strings for extraction 40 | 41 | If strings are not directly passed to `translateService` or put in HTML templates, they may be missing from the 42 | extraction process. 43 | 44 | For these cases, you have to use the dummy `marker()` function: 45 | ```typescript 46 | import { marker } from '@biesbjerg/ngx-translate-extract-marker'; 47 | 48 | function toBeTranslatedLater() { 49 | return marker('A string to be translated'); 50 | } 51 | ``` 52 | 53 | Strings marked like this will then be properly extracted. 54 | -------------------------------------------------------------------------------- /src/app/@shared/http/error-handler.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 4 | import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; 5 | 6 | import { ErrorHandlerInterceptor } from './error-handler.interceptor'; 7 | 8 | describe('ErrorHandlerInterceptor', () => { 9 | let errorHandlerInterceptor: ErrorHandlerInterceptor; 10 | let http: HttpClient; 11 | let httpMock: HttpTestingController; 12 | 13 | function createInterceptor() { 14 | errorHandlerInterceptor = new ErrorHandlerInterceptor(); 15 | return errorHandlerInterceptor; 16 | } 17 | 18 | beforeEach(() => { 19 | TestBed.configureTestingModule({ 20 | imports: [HttpClientTestingModule], 21 | providers: [{ 22 | provide: HTTP_INTERCEPTORS, 23 | useFactory: createInterceptor, 24 | multi: true 25 | }] 26 | }); 27 | 28 | http = TestBed.inject(HttpClient); 29 | httpMock = TestBed.inject(HttpTestingController as Type); 30 | }); 31 | 32 | afterEach(() => { 33 | httpMock.verify(); 34 | }); 35 | 36 | it('should catch error and call error handler', () => { 37 | // Arrange 38 | // Note: here we spy on private method since target is customization here, 39 | // but you should replace it by actual behavior in your app 40 | jest.spyOn(ErrorHandlerInterceptor.prototype as any, 'errorHandler'); 41 | 42 | // Act 43 | http.get('/toto').subscribe(() => fail('should error'), () => { 44 | // Assert 45 | expect((ErrorHandlerInterceptor.prototype as any).errorHandler).toHaveBeenCalled(); 46 | }); 47 | 48 | httpMock.expectOne({}).flush(null, { 49 | status: 404, 50 | statusText: 'error' 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /docs/backend-proxy.md: -------------------------------------------------------------------------------- 1 | # Backend proxy 2 | 3 | Usually when working on a web application you consume data from custom-made APIs. 4 | 5 | To ease development with our development server integrating live reload while keeping your backend API calls working, 6 | we also have setup a backend proxy to redirect API calls to whatever URL and port you want. This allows you: 7 | 8 | - To develop frontend features without the need to run an API backend locally 9 | - To use a local development server without [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) issues 10 | - To debug frontend code with data from a remote testing platform directly 11 | 12 | ## How to configure 13 | 14 | In the root folder you will find a `proxy.conf.js`, containing the backend proxy configuration. 15 | 16 | The interesting part is there: 17 | ```js 18 | const proxyConfig = [ 19 | { 20 | context: '/api', 21 | pathRewrite: {'^/api': ''}, 22 | target: 'http://api.icndb.com', 23 | changeOrigin: true 24 | } 25 | ]; 26 | ``` 27 | 28 | This is where you can setup one or more proxy rules. 29 | 30 | For the complete set of options, see the `http-proxy-middleware` 31 | [documentation](https://github.com/chimurai/http-proxy-middleware#options). 32 | 33 | ### Corporate proxy support 34 | 35 | To allow external API calls redirection through a corporate proxy, you will also find a `setupForCorporateProxy()` 36 | function in the proxy configuration file. By default, this method configures a corporate proxy agent based on the 37 | `HTTP_PROXY` environment variable, see the [corporate proxy documentation](corporate-proxy.md) for more details. 38 | 39 | If you need to, you can further customize this function to fit the network of your working environment. 40 | 41 | If your corporate proxy use a custom SSL certificate, your may need to add the `secure: false` option to your 42 | backend proxy configuration. 43 | -------------------------------------------------------------------------------- /src/app/home/quote.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 4 | 5 | import { QuoteService } from './quote.service'; 6 | 7 | describe('QuoteService', () => { 8 | let quoteService: QuoteService; 9 | let httpMock: HttpTestingController; 10 | 11 | beforeEach(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [ 14 | HttpClientTestingModule 15 | ], 16 | providers: [ 17 | QuoteService 18 | ] 19 | }); 20 | 21 | quoteService = TestBed.inject(QuoteService); 22 | httpMock = TestBed.inject(HttpTestingController as Type); 23 | }); 24 | 25 | afterEach(() => { 26 | httpMock.verify(); 27 | }); 28 | 29 | describe('getRandomQuote', () => { 30 | it('should return a random Chuck Norris quote', () => { 31 | // Arrange 32 | const mockQuote = { value: 'a random quote' }; 33 | 34 | // Act 35 | const randomQuoteSubscription = quoteService.getRandomQuote({ category: 'toto' }); 36 | 37 | // Assert 38 | randomQuoteSubscription.subscribe((quote: string) => { 39 | expect(quote).toEqual(mockQuote.value); 40 | }); 41 | httpMock.expectOne({}).flush(mockQuote); 42 | }); 43 | 44 | it('should return a string in case of error', () => { 45 | // Act 46 | const randomQuoteSubscription = quoteService.getRandomQuote({ category: 'toto' }); 47 | 48 | // Assert 49 | randomQuoteSubscription.subscribe((quote: string) => { 50 | expect(typeof quote).toEqual('string'); 51 | expect(quote).toContain('Error'); 52 | }); 53 | httpMock.expectOne({}).flush(null, { 54 | status: 500, 55 | statusText: 'error' 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/app/shell/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 37 |
38 | -------------------------------------------------------------------------------- /src/app/@shared/loader/loader.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoaderComponent } from './loader.component'; 4 | 5 | describe('LoaderComponent', () => { 6 | let component: LoaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [LoaderComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoaderComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should not be visible by default', () => { 23 | // Arrange 24 | const element = fixture.nativeElement; 25 | const div = element.querySelectorAll('div')[0]; 26 | 27 | // Assert 28 | expect(div.getAttribute('hidden')).not.toBeNull(); 29 | }); 30 | 31 | it('should be visible when app is loading', () => { 32 | // Arrange 33 | const element = fixture.nativeElement; 34 | const div = element.querySelectorAll('div')[0]; 35 | 36 | // Act 37 | fixture.componentInstance.isLoading = true; 38 | fixture.detectChanges(); 39 | 40 | // Assert 41 | expect(div.getAttribute('hidden')).toBeNull(); 42 | }); 43 | 44 | it('should not display a message by default', () => { 45 | // Arrange 46 | const element = fixture.nativeElement; 47 | const span = element.querySelectorAll('span')[0]; 48 | 49 | // Assert 50 | expect(span.textContent).toBe(''); 51 | }); 52 | 53 | it('should display specified message', () => { 54 | // Arrange 55 | const element = fixture.nativeElement; 56 | const span = element.querySelectorAll('span')[0]; 57 | 58 | // Act 59 | fixture.componentInstance.message = 'testing'; 60 | fixture.detectChanges(); 61 | 62 | // Assert 63 | expect(span.textContent).toBe('testing'); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { RouteReuseStrategy, RouterModule } from '@angular/router'; 5 | import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; 6 | import { ServiceWorkerModule } from '@angular/service-worker'; 7 | import { TranslateModule } from '@ngx-translate/core'; 8 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 9 | 10 | import { environment } from '@env/environment'; 11 | import { RouteReusableStrategy, ApiPrefixInterceptor, ErrorHandlerInterceptor, SharedModule } from '@shared'; 12 | import { AuthModule } from '@app/auth'; 13 | import { HomeModule } from './home/home.module'; 14 | import { ShellModule } from './shell/shell.module'; 15 | import { AboutModule } from './about/about.module'; 16 | import { AppComponent } from './app.component'; 17 | import { AppRoutingModule } from './app-routing.module'; 18 | 19 | @NgModule({ 20 | imports: [ 21 | BrowserModule, 22 | ServiceWorkerModule.register('./ngsw-worker.js', { enabled: environment.production }), 23 | FormsModule, 24 | HttpClientModule, 25 | RouterModule, 26 | TranslateModule.forRoot(), 27 | NgbModule, 28 | SharedModule, 29 | ShellModule, 30 | HomeModule, 31 | AboutModule, 32 | AuthModule, 33 | AppRoutingModule // must be imported as the last module as it contains the fallback route 34 | ], 35 | declarations: [AppComponent], 36 | providers: [ 37 | { 38 | provide: HTTP_INTERCEPTORS, 39 | useClass: ApiPrefixInterceptor, 40 | multi: true 41 | }, 42 | { 43 | provide: HTTP_INTERCEPTORS, 44 | useClass: ErrorHandlerInterceptor, 45 | multi: true 46 | }, 47 | { 48 | provide: RouteReuseStrategy, 49 | useClass: RouteReusableStrategy 50 | }, 51 | ], 52 | bootstrap: [AppComponent] 53 | }) 54 | export class AppModule { } 55 | -------------------------------------------------------------------------------- /src/app/auth/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 4 | import { finalize } from 'rxjs/operators'; 5 | 6 | import { environment } from '@env/environment'; 7 | import { Logger, UntilDestroy, untilDestroyed } from '@shared'; 8 | import { AuthenticationService } from './authentication.service'; 9 | 10 | const log = new Logger('Login'); 11 | 12 | @UntilDestroy() 13 | @Component({ 14 | selector: 'app-login', 15 | templateUrl: './login.component.html', 16 | styleUrls: ['./login.component.scss'] 17 | }) 18 | export class LoginComponent implements OnInit { 19 | 20 | version: string | null = environment.version; 21 | error: string | undefined; 22 | loginForm!: FormGroup; 23 | isLoading = false; 24 | 25 | constructor(private router: Router, 26 | private route: ActivatedRoute, 27 | private formBuilder: FormBuilder, 28 | private authenticationService: AuthenticationService) { 29 | this.createForm(); 30 | } 31 | 32 | ngOnInit() { } 33 | 34 | login() { 35 | this.isLoading = true; 36 | const login$ = this.authenticationService.login(this.loginForm.value); 37 | login$.pipe( 38 | finalize(() => { 39 | this.loginForm.markAsPristine(); 40 | this.isLoading = false; 41 | }), 42 | untilDestroyed(this) 43 | ).subscribe(credentials => { 44 | log.debug(`${credentials.username} successfully logged in`); 45 | this.router.navigate([ this.route.snapshot.queryParams['redirect'] || '/'], { replaceUrl: true }); 46 | }, error => { 47 | log.debug(`Login error: ${error}`); 48 | this.error = error; 49 | }); 50 | } 51 | 52 | 53 | private createForm() { 54 | this.loginForm = this.formBuilder.group({ 55 | username: ['', Validators.required], 56 | password: ['', Validators.required], 57 | remember: true 58 | }); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /docs/coding-guides/html.md: -------------------------------------------------------------------------------- 1 | # HTML coding guide 2 | 3 | ## Naming conventions 4 | 5 | - Everything should be named in `kebab-case` (lowercase words separated with a `-`): tags, attributes, IDs, etc, 6 | **except for everything bound to Angular** such variables, directives or events which should be in `camelCase` 7 | - File names should always be in `kebab-case` 8 | 9 | ## Coding rules 10 | 11 | - Use HTML5 doctype: `` 12 | - Use HTML [semantic elements](https://developer.mozilla.org/docs/Web/HTML/Sections_and_Outlines_of_an_HTML5_document) 13 | - Use double quotes `"` around attribute values in tags 14 | - Use a new line for every block, list, or table element, and indent every such child element 15 | - Clearly Separate structure (HTML) from presentation (CSS) from behavior (JavaScript): 16 | * Never use inline CSS or JavaScript 17 | * Keep any logic out of the HTML 18 | - `type` attribute for stylesheets and script tags should be omitted 19 | 20 | ## Common pitfalls 21 | 22 | - **Block**-type tags cannot be nested inside **inline**-type tags: a `
` tag cannot be nested in a ``. 23 | This rule also applies regarding the `display` value of an element. 24 | - HTML is **not** XML: empty tags cannot be self-closing and will result in improper results 25 | * `
` will be interpreted as a simple `
` without closing tag! 26 | * The only tags that allows self-closing are the one that does not require a closing tag in first place: 27 | these are the void elements that do not not accept content `
`, `
`, ``, ``, ``, `` 28 | (and others). 29 | 30 | ## Templates 31 | 32 | In accordance with the [Angular style guide](https://angular.io/guide/styleguide), HTML templates should be extracted in 33 | separate files, when more than 3 lines. 34 | 35 | Only use inline templates sparingly in very simple components with less than 3 lines of HTML. 36 | 37 | ## Enforcement 38 | 39 | Coding rules enforcement and basic sanity checks are done in this project by [HTMLHint](http://htmlhint.com). 40 | -------------------------------------------------------------------------------- /src/app/auth/credentials.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | export interface Credentials { 4 | // Customize received credentials here 5 | username: string; 6 | token: string; 7 | } 8 | 9 | const credentialsKey = 'credentials'; 10 | 11 | /** 12 | * Provides storage for authentication credentials. 13 | * The Credentials interface should be replaced with proper implementation. 14 | */ 15 | @Injectable({ 16 | providedIn: 'root' 17 | }) 18 | export class CredentialsService { 19 | 20 | private _credentials: Credentials | null = null; 21 | 22 | constructor() { 23 | const savedCredentials = sessionStorage.getItem(credentialsKey) || localStorage.getItem(credentialsKey); 24 | if (savedCredentials) { 25 | this._credentials = JSON.parse(savedCredentials); 26 | } 27 | } 28 | 29 | /** 30 | * Checks is the user is authenticated. 31 | * @return True if the user is authenticated. 32 | */ 33 | isAuthenticated(): boolean { 34 | return !!this.credentials; 35 | } 36 | 37 | /** 38 | * Gets the user credentials. 39 | * @return The user credentials or null if the user is not authenticated. 40 | */ 41 | get credentials(): Credentials | null { 42 | return this._credentials; 43 | } 44 | 45 | /** 46 | * Sets the user credentials. 47 | * The credentials may be persisted across sessions by setting the `remember` parameter to true. 48 | * Otherwise, the credentials are only persisted for the current session. 49 | * @param credentials The user credentials. 50 | * @param remember True to remember credentials across sessions. 51 | */ 52 | setCredentials(credentials?: Credentials, remember?: boolean) { 53 | this._credentials = credentials || null; 54 | 55 | if (credentials) { 56 | const storage = remember ? localStorage : sessionStorage; 57 | storage.setItem(credentialsKey, JSON.stringify(credentials)); 58 | } else { 59 | sessionStorage.removeItem(credentialsKey); 60 | localStorage.removeItem(credentialsKey); 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Router, NavigationEnd, ActivatedRoute } from '@angular/router'; 3 | import { Title } from '@angular/platform-browser'; 4 | import { TranslateService } from '@ngx-translate/core'; 5 | import { merge } from 'rxjs'; 6 | import { filter, map, switchMap } from 'rxjs/operators'; 7 | 8 | import { environment } from '@env/environment'; 9 | import { Logger, UntilDestroy, untilDestroyed } from '@shared'; 10 | import { I18nService } from '@app/i18n'; 11 | 12 | const log = new Logger('App'); 13 | 14 | @UntilDestroy() 15 | @Component({ 16 | selector: 'app-root', 17 | templateUrl: './app.component.html', 18 | styleUrls: ['./app.component.scss'] 19 | }) 20 | export class AppComponent implements OnInit, OnDestroy { 21 | 22 | constructor(private router: Router, 23 | private activatedRoute: ActivatedRoute, 24 | private titleService: Title, 25 | private translateService: TranslateService, 26 | private i18nService: I18nService) { } 27 | 28 | ngOnInit() { 29 | // Setup logger 30 | if (environment.production) { 31 | Logger.enableProductionMode(); 32 | } 33 | 34 | log.debug('init'); 35 | 36 | 37 | // Setup translations 38 | this.i18nService.init(environment.defaultLanguage, environment.supportedLanguages); 39 | 40 | const onNavigationEnd = this.router.events.pipe(filter(event => event instanceof NavigationEnd)); 41 | 42 | // Change page title on navigation or language change, based on route data 43 | merge(this.translateService.onLangChange, onNavigationEnd) 44 | .pipe( 45 | map(() => { 46 | let route = this.activatedRoute; 47 | while (route.firstChild) { 48 | route = route.firstChild; 49 | } 50 | return route; 51 | }), 52 | filter(route => route.outlet === 'primary'), 53 | switchMap(route => route.data), 54 | untilDestroyed(this) 55 | ) 56 | .subscribe(event => { 57 | const title = event['title']; 58 | if (title) { 59 | this.titleService.setTitle(this.translateService.instant(title)); 60 | } 61 | }); 62 | } 63 | 64 | ngOnDestroy() { 65 | this.i18nService.destroy(); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /docs/routing.md: -------------------------------------------------------------------------------- 1 | # Browser routing 2 | 3 | To allow navigation without triggering a server request, Angular now use by default the 4 | [HTML5 pushState](https://developer.mozilla.org/en-US/docs/Web/API/History_API#Adding_and_modifying_history_entries) 5 | API enabling natural URL style (like `localhost:4200/home/`), in opposition to Angular 1 which used the *hashbang* hack 6 | routing style (like `localhost:4200/#/home/`). 7 | 8 | This change has several consequences you should know of, be sure to read the 9 | [browser URL styles](https://angular.io/docs/ts/latest/guide/router.html#!#browser-url-styles) notice to fully 10 | understand the differences between the two approaches. 11 | 12 | In short: 13 | 14 | - It is only supported on modern browsers (IE10+), a [polyfill](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills#html5-history-api-pushstate-replacestate-popstate) 15 | is required for older browsers. 16 | 17 | - You have the option to perform *server-side rendering* later if you need to increase your app perceived performance. 18 | 19 | - You need to [configure URL rewriting](#server-configuration) on your server so that all routes serve your index file. 20 | 21 | It is still possible to revert to the hash strategy, but unless you have specific needs, you should stick with the 22 | default HTML5 routing mode. 23 | 24 | ## Server configuration 25 | 26 | To allow your angular application working properly as a *Single Page Application* (SPA) and allow bookmarking or 27 | refreshing any page, you need some configuration on your server, otherwise you will be running into troubles. 28 | 29 | > Note that during development, the live reload server already supports SPA mode. 30 | 31 | The basic idea is simply to serve the `index.html` file for every request aimed at your application. 32 | 33 | Here is an example on how to perform this on an [Express](http://expressjs.com) NodeJS server: 34 | 35 | ```js 36 | // Put this in your `server.js` file, after your other rules (APIs, static files...) 37 | app.get('/*', function(req, res) { 38 | res.sendFile(__dirname + '/index.html') 39 | }); 40 | ``` 41 | 42 | For other servers like [Nginx](https://www.nginx.com/blog/creating-nginx-rewrite-rules/) or 43 | [Apache](http://httpd.apache.org/docs/2.0/misc/rewriteguide.html), you may look for how to perform *URL rewriting*. 44 | -------------------------------------------------------------------------------- /src/app/auth/credentials.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { CredentialsService, Credentials } from './credentials.service'; 4 | 5 | const credentialsKey = 'credentials'; 6 | 7 | describe('CredentialsService', () => { 8 | let credentialsService: CredentialsService; 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({ 12 | providers: [CredentialsService] 13 | }); 14 | 15 | credentialsService = TestBed.inject(CredentialsService); 16 | }); 17 | 18 | afterEach(() => { 19 | // Cleanup 20 | localStorage.removeItem(credentialsKey); 21 | sessionStorage.removeItem(credentialsKey); 22 | }); 23 | 24 | describe('setCredentials', () => { 25 | it('should authenticate user if credentials are set', () => { 26 | // Act 27 | credentialsService.setCredentials({ username: 'me', token: '123' }); 28 | 29 | // Assert 30 | expect(credentialsService.isAuthenticated()).toBe(true); 31 | expect((credentialsService.credentials as Credentials).username).toBe('me'); 32 | }); 33 | 34 | it('should clean authentication', () => { 35 | // Act 36 | credentialsService.setCredentials(); 37 | 38 | // Assert 39 | expect(credentialsService.isAuthenticated()).toBe(false); 40 | }); 41 | 42 | it('should persist credentials for the session', () => { 43 | // Act 44 | credentialsService.setCredentials({ username: 'me', token: '123' }); 45 | 46 | // Assert 47 | expect(sessionStorage.getItem(credentialsKey)).not.toBeNull(); 48 | expect(localStorage.getItem(credentialsKey)).toBeNull(); 49 | }); 50 | 51 | it('should persist credentials across sessions', () => { 52 | // Act 53 | credentialsService.setCredentials({ username: 'me', token: '123' }, true); 54 | 55 | // Assert 56 | expect(localStorage.getItem(credentialsKey)).not.toBeNull(); 57 | expect(sessionStorage.getItem(credentialsKey)).toBeNull(); 58 | }); 59 | 60 | it('should clear user authentication', () => { 61 | // Act 62 | credentialsService.setCredentials(); 63 | 64 | // Assert 65 | expect(credentialsService.isAuthenticated()).toBe(false); 66 | expect(credentialsService.credentials).toBeNull(); 67 | expect(sessionStorage.getItem(credentialsKey)).toBeNull(); 68 | expect(localStorage.getItem(credentialsKey)).toBeNull(); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/app/@shared/logger.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LogLevel, LogOutput } from './logger.service'; 2 | 3 | const logMethods = ['log', 'info', 'warn', 'error']; 4 | 5 | describe('Logger', () => { 6 | let savedConsole: any[]; 7 | let savedLevel: LogLevel; 8 | let savedOutputs: LogOutput[]; 9 | 10 | beforeAll(() => { 11 | savedConsole = []; 12 | logMethods.forEach((m) => { 13 | savedConsole[m] = console[m]; 14 | console[m] = () => {}; 15 | }); 16 | savedLevel = Logger.level; 17 | savedOutputs = Logger.outputs; 18 | }); 19 | 20 | beforeEach(() => { 21 | Logger.level = LogLevel.Debug; 22 | }); 23 | 24 | afterAll(() => { 25 | logMethods.forEach((m) => { console[m] = savedConsole[m]; }); 26 | Logger.level = savedLevel; 27 | Logger.outputs = savedOutputs; 28 | }); 29 | 30 | it('should create an instance', () => { 31 | expect(new Logger()).toBeTruthy(); 32 | }); 33 | 34 | it('should add a new LogOutput and receives log entries', () => { 35 | // Arrange 36 | const outputSpy = jest.fn(); 37 | const log = new Logger('test'); 38 | 39 | // Act 40 | Logger.outputs.push(outputSpy); 41 | 42 | log.debug('d'); 43 | log.info('i'); 44 | log.warn('w'); 45 | log.error('e', { error: true }); 46 | 47 | // Assert 48 | expect(outputSpy).toHaveBeenCalled(); 49 | expect(outputSpy.mock.calls.length).toBe(4); 50 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Debug, 'd'); 51 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Info, 'i'); 52 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Warning, 'w'); 53 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Error, 'e', { error: true }); 54 | }); 55 | 56 | it('should add a new LogOutput and receives only production log entries', () => { 57 | // Arrange 58 | const outputSpy = jest.fn(); 59 | const log = new Logger('test'); 60 | 61 | // Act 62 | Logger.outputs.push(outputSpy); 63 | Logger.enableProductionMode(); 64 | 65 | log.debug('d'); 66 | log.info('i'); 67 | log.warn('w'); 68 | log.error('e', { error: true }); 69 | 70 | // Assert 71 | expect(outputSpy).toHaveBeenCalled(); 72 | expect(outputSpy.mock.calls.length).toBe(2); 73 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Warning, 'w'); 74 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Error, 'e', { error: true }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/app/auth/authentication.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { Router, ActivatedRouteSnapshot } from '@angular/router'; 3 | 4 | import { CredentialsService } from './credentials.service'; 5 | import { MockCredentialsService } from './credentials.service.mock'; 6 | import { AuthenticationGuard } from './authentication.guard'; 7 | 8 | describe('AuthenticationGuard', () => { 9 | let authenticationGuard: AuthenticationGuard; 10 | let credentialsService: MockCredentialsService; 11 | let mockRouter: any; 12 | let mockSnapshot: any; 13 | 14 | beforeEach(() => { 15 | mockRouter = { 16 | navigate: jest.fn() 17 | }; 18 | mockSnapshot = jest.fn(() => ({ 19 | toString: jest.fn() 20 | })); 21 | 22 | TestBed.configureTestingModule({ 23 | providers: [ 24 | AuthenticationGuard, 25 | { provide: CredentialsService, useClass: MockCredentialsService }, 26 | { provide: Router, useValue: mockRouter } 27 | ] 28 | }); 29 | 30 | authenticationGuard = TestBed.inject(AuthenticationGuard); 31 | credentialsService = TestBed.inject(CredentialsService); 32 | }); 33 | 34 | it('should have a canActivate method', () => { 35 | expect(typeof authenticationGuard.canActivate).toBe('function'); 36 | }); 37 | 38 | it('should return true if user is authenticated', () => { 39 | expect(authenticationGuard.canActivate(new ActivatedRouteSnapshot(), mockSnapshot)).toBe(true); 40 | }); 41 | 42 | it('should return false and redirect to login if user is not authenticated', () => { 43 | // Arrange 44 | credentialsService.credentials = null; 45 | 46 | // Act 47 | const result = authenticationGuard.canActivate(new ActivatedRouteSnapshot(), mockSnapshot); 48 | 49 | // Assert 50 | expect(mockRouter.navigate).toHaveBeenCalledWith(['/login'], { 51 | queryParams: { redirect: undefined }, 52 | replaceUrl: true 53 | }); 54 | expect(result).toBe(false); 55 | }); 56 | 57 | it('should save url as queryParam if user is not authenticated', () => { 58 | credentialsService.credentials = null; 59 | mockRouter.url = '/about'; 60 | mockSnapshot.url = '/about'; 61 | 62 | authenticationGuard.canActivate(new ActivatedRouteSnapshot(), mockSnapshot); 63 | expect(mockRouter.navigate).toHaveBeenCalledWith(['/login'], { 64 | queryParams: { redirect: mockRouter.url }, 65 | replaceUrl: true 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/app/auth/login.component.html: -------------------------------------------------------------------------------- 1 | 58 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************************** 2 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. 3 | */ 4 | import '@angular/localize/init'; 5 | 6 | /** 7 | * This file includes polyfills needed by Angular and is loaded before the app. 8 | * You can add your own extra polyfills to this file. 9 | * 10 | * This file is divided into 2 sections: 11 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 12 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 13 | * file. 14 | * 15 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 16 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 17 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 18 | * 19 | * Learn more in https://angular.io/guide/browser-support 20 | */ 21 | 22 | /*************************************************************************************************** 23 | * BROWSER POLYFILLS 24 | */ 25 | 26 | /** 27 | * By default, zone.js will patch all possible macroTask and DomEvents 28 | * user can disable parts of macroTask/DomEvents patch by setting following flags 29 | * because those flags need to be set before `zone.js` being loaded, and webpack 30 | * will put import in the top of bundle, so user need to create a separate file 31 | * in this directory (for example: zone-flags.ts), and put the following flags 32 | * into that file, and then add the following code before importing zone.js. 33 | * import './zone-flags'; 34 | * 35 | * The flags allowed in zone-flags.ts are listed here. 36 | * 37 | * The following flags will work for all browsers. 38 | * 39 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 40 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 41 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 42 | * 43 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 44 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 45 | * 46 | * (window as any).__Zone_enable_cross_context_check = true; 47 | * 48 | */ 49 | 50 | /*************************************************************************************************** 51 | * Zone JS is required by default for Angular itself. 52 | */ 53 | import 'zone.js'; // Included with Angular CLI. 54 | 55 | 56 | /*************************************************************************************************** 57 | * APPLICATION IMPORTS 58 | */ 59 | -------------------------------------------------------------------------------- /docs/coding-guides/unit-tests.md: -------------------------------------------------------------------------------- 1 | # Unit tests coding guide 2 | 3 | The main objective of unit tests is to detect regressions and to help you design software components. A suite of 4 | *good* unit tests can be *immensely* valuable for your project and makes it easier to refactor and expand your code. 5 | But keep in mind that a suite of *bad* unit tests can also be *immensely* painful, and hurt your development by 6 | inhibiting your ability to refactor or alter your code in any way. 7 | 8 | ## What to test? 9 | 10 | Everything! But if you need priorities, at least all business logic code must be tested: services, helpers, models... 11 | Shared directives/components should also be covered by unit tests, if you do not have the time to test every single 12 | component. 13 | 14 | Keep in mind that component unit tests should not overlap with [end-to-end tests](e2e-tests.md): while unit the tests 15 | cover the isolated behavior of the component bindings and methods, the end-to-end tests in opposition should cover the 16 | integration and interactions with other app components based on real use cases scenarios. 17 | 18 | ## Good practices 19 | 20 | - Name your tests cleanly and consistently 21 | - Do not only test nominal cases, the most important tests are the one covering the edge cases 22 | - Each test should be independent to all the others 23 | - Avoid unnecessary assertions: it's counter-productive to assert anything covered by another test, it just increase 24 | pointless failures and maintenance workload 25 | - Test only one code unit at a time: if you cannot do this, it means you have an architecture problem in your app 26 | - Mock out all external dependencies and state: if there is too much to mock, it is often a sign that maybe you 27 | should split your tested module into several more independent modules 28 | - Clearly separate or identify these 3 stages of each unit test (the *3A*): *arrange*, *act* and *assert* 29 | - When you fix a bug, add a test case for it to prevent regression 30 | 31 | ## Pitfalls 32 | 33 | - Sometimes your architecture might mean your code modify static variables during unit tests. Avoid this if you can, 34 | but if you can't, at least make sure each test resets the relevant statics before and after your tests. 35 | - Don’t unit-test configuration settings 36 | - Improving test coverage is good, but having meaningful tests is better: start with the latter first, and **only after 37 | essential features of your code unit are tested**, your can think of improving the coverage. 38 | 39 | ## Unit testing with Angular 40 | 41 | A good starting point for learning is the official 42 | [testing guide](https://angular.io/docs/ts/latest/guide/testing.html). 43 | 44 | But as you will most likely want to go bit further in real world apps, these 45 | [example test snippets](https://gist.github.com/wkwiatek/e8a4a9d92abc4739f04f5abddd3de8a7) are also very helpful to 46 | learn how to cover most common testing use cases. 47 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-config-prettier" 5 | ], 6 | "rulesDirectory": [ 7 | "codelyzer" 8 | ], 9 | "rules": { 10 | "array-type": false, 11 | "arrow-parens": false, 12 | "deprecation": { 13 | "severity": "warning" 14 | }, 15 | "import-blacklist": [ 16 | true, 17 | "rxjs/Rx" 18 | ], 19 | "max-line-length": [ 20 | true, 21 | 120 22 | ], 23 | "member-access": false, 24 | "member-ordering": [ 25 | true, 26 | { 27 | "order": [ 28 | "public-static-field", 29 | "protected-static-field", 30 | "private-static-field", 31 | "public-instance-field", 32 | "protected-instance-field", 33 | "private-instance-field", 34 | "public-static-method", 35 | "protected-static-method", 36 | "private-static-method", 37 | "constructor", 38 | "public-instance-method", 39 | "protected-instance-method", 40 | "private-instance-method" 41 | ] 42 | } 43 | ], 44 | "interface-name": false, 45 | "max-classes-per-file": false, 46 | "no-consecutive-blank-lines": false, 47 | "no-console": [ 48 | true, 49 | "debug", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-duplicate-variable": [ 55 | true, 56 | "check-parameters" 57 | ], 58 | "no-empty": false, 59 | "no-inferrable-types": [ 60 | true, 61 | "ignore-params" 62 | ], 63 | "no-non-null-assertion": true, 64 | "no-redundant-jsdoc": true, 65 | "no-switch-case-fall-through": true, 66 | "no-unnecessary-initializer": true, 67 | "object-literal-sort-keys": false, 68 | "quotemark": [ 69 | true, 70 | "single" 71 | ], 72 | "typedef": [ 73 | true, 74 | "parameter", 75 | "property-declaration" 76 | ], 77 | "variable-name": false, 78 | "no-var-requires": false, 79 | "object-literal-key-quotes": false, 80 | "ordered-imports": false, 81 | "trailing-comma": false, 82 | "no-conflicting-lifecycle": true, 83 | "no-output-native": true, 84 | "directive-selector": [ 85 | true, 86 | "attribute", 87 | "app", 88 | "camelCase" 89 | ], 90 | "component-selector": [ 91 | true, 92 | "element", 93 | "page", 94 | "app", 95 | "kebab-case" 96 | ], 97 | "template-banana-in-box": true, 98 | "template-no-negated-async": true, 99 | "no-output-on-prefix": true, 100 | "no-inputs-metadata-property": true, 101 | "no-outputs-metadata-property": true, 102 | "no-host-metadata-property": true, 103 | "no-input-rename": true, 104 | "no-output-rename": true, 105 | "use-lifecycle-interface": true, 106 | "use-pipe-transform-interface": true, 107 | "component-class-suffix": true, 108 | "contextual-lifecycle": true, 109 | "directive-class-suffix": true 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /docs/coding-guides/typescript.md: -------------------------------------------------------------------------------- 1 | # TypeScript coding guide 2 | 3 | [TypeScript](http://www.typescriptlang.org) is a superset of JavaScript that greatly helps building large web 4 | applications. 5 | 6 | Coding conventions and best practices comes from the 7 | [TypeScript guidelines](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines), and are also detailed in the 8 | [TypeScript Deep Dive Style Guide](https://basarat.gitbooks.io/typescript/content/docs/styleguide/styleguide.html). 9 | In addition, this project also follows the general [Angular style guide](https://angular.io/guide/styleguide). 10 | 11 | ## Naming conventions 12 | 13 | - Use `PascalCase` for types, classes, interfaces, constants and enum values. 14 | - Use `camelCase` for variables, properties and functions 15 | - Avoid prefixing interfaces with a capital `I`, see [Angular style guide](https://angular.io/guide/styleguide#!#03-03) 16 | - Do not use `_` as a prefix for private properties. An exception can be made for backing fields like this: 17 | ```typescript 18 | private _foo: string; 19 | get foo() { return this._foo; } // foo is read-only to consumers 20 | ``` 21 | 22 | ## Ordering 23 | 24 | - Within a file, type definitions should come first 25 | - Within a class, these priorities should be respected: 26 | * Properties comes before functions 27 | * Static symbols comes before instance symbols 28 | * Public symbols comes before private symbols 29 | 30 | ## Coding rules 31 | 32 | - Use single quotes `'` for strings 33 | - Always use strict equality checks: `===` and `!==` instead of `==` or `!=` to avoid comparison pitfalls (see 34 | [JavaScript equality table](https://dorey.github.io/JavaScript-Equality-Table/)). 35 | The only accepted usage for `==` is when you want to check a value against `null` or `undefined`. 36 | - Use `[]` instead of `Array` constructor 37 | - Use `{}` instead of `Object` constructor 38 | - Always specify types for function parameters and returns (if applicable) 39 | - Do not export types/functions unless you need to share it across multiple components 40 | - Do not introduce new types/values to the global namespace 41 | - Use arrow functions over anonymous function expressions 42 | - Only surround arrow function parameters when necessary. 43 | For example, `(x) => x + x` is wrong but the following are correct: 44 | * `x => x + x` 45 | * `(x, y) => x + y` 46 | * `(x: T, y: T) => x === y` 47 | 48 | ## Definitions 49 | 50 | In order to infer types from JavaScript modules, TypeScript language supports external type definitions. They are 51 | located in the `node_modules/@types` folder. 52 | 53 | To manage type definitions, use standard `npm install|update|remove` commands. 54 | 55 | ## Enforcement 56 | 57 | Coding rules are enforced in this project via [TSLint](https://github.com/palantir/tslint). 58 | Angular-specific rules are also enforced via the [Codelyzer](https://github.com/mgechev/codelyzer) rule extensions. 59 | 60 | ## Learn more 61 | 62 | The read of [TypeScript Deep Dive](https://basarat.gitbooks.io/typescript) is recommended, this is a very good 63 | reference book for TypeScript (and also open-source). 64 | -------------------------------------------------------------------------------- /src/app/i18n/i18n.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { TranslateService, LangChangeEvent } from '@ngx-translate/core'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { Logger } from '@shared'; 6 | import enUS from '../../translations/en-US.json'; 7 | import frFR from '../../translations/fr-FR.json'; 8 | 9 | const log = new Logger('I18nService'); 10 | const languageKey = 'language'; 11 | 12 | @Injectable({ 13 | providedIn: 'root' 14 | }) 15 | export class I18nService { 16 | 17 | defaultLanguage!: string; 18 | supportedLanguages!: string[]; 19 | 20 | private langChangeSubscription!: Subscription; 21 | 22 | constructor(private translateService: TranslateService) { 23 | // Embed languages to avoid extra HTTP requests 24 | translateService.setTranslation('en-US', enUS); 25 | translateService.setTranslation('fr-FR', frFR); 26 | } 27 | 28 | /** 29 | * Initializes i18n for the application. 30 | * Loads language from local storage if present, or sets default language. 31 | * @param defaultLanguage The default language to use. 32 | * @param supportedLanguages The list of supported languages. 33 | */ 34 | init(defaultLanguage: string, supportedLanguages: string[]) { 35 | this.defaultLanguage = defaultLanguage; 36 | this.supportedLanguages = supportedLanguages; 37 | this.language = ''; 38 | 39 | // Warning: this subscription will always be alive for the app's lifetime 40 | this.langChangeSubscription = this.translateService.onLangChange 41 | .subscribe((event: LangChangeEvent) => { localStorage.setItem(languageKey, event.lang); }); 42 | } 43 | 44 | /** 45 | * Cleans up language change subscription. 46 | */ 47 | destroy() { 48 | if (this.langChangeSubscription) { 49 | this.langChangeSubscription.unsubscribe(); 50 | } 51 | } 52 | 53 | /** 54 | * Sets the current language. 55 | * Note: The current language is saved to the local storage. 56 | * If no parameter is specified, the language is loaded from local storage (if present). 57 | * @param language The IETF language code to set. 58 | */ 59 | set language(language: string) { 60 | let newLanguage = language || localStorage.getItem(languageKey) || this.translateService.getBrowserCultureLang() || ''; 61 | let isSupportedLanguage = this.supportedLanguages.includes(newLanguage); 62 | 63 | // If no exact match is found, search without the region 64 | if (newLanguage && !isSupportedLanguage) { 65 | newLanguage = newLanguage.split('-')[0]; 66 | newLanguage = this.supportedLanguages.find(supportedLanguage => supportedLanguage.startsWith(newLanguage)) || ''; 67 | isSupportedLanguage = Boolean(newLanguage); 68 | } 69 | 70 | // Fallback if language is not supported 71 | if (!newLanguage || !isSupportedLanguage) { 72 | newLanguage = this.defaultLanguage; 73 | } 74 | 75 | language = newLanguage; 76 | 77 | log.debug(`Language set to ${language}`); 78 | this.translateService.use(language); 79 | } 80 | 81 | /** 82 | * Gets the current language. 83 | * @return The current language code. 84 | */ 85 | get language(): string { 86 | return this.translateService.currentLang; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/app/@shared/logger.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple logger system with the possibility of registering custom outputs. 3 | * 4 | * 4 different log levels are provided, with corresponding methods: 5 | * - debug : for debug information 6 | * - info : for informative status of the application (success, ...) 7 | * - warning : for non-critical errors that do not prevent normal application behavior 8 | * - error : for critical errors that prevent normal application behavior 9 | * 10 | * Example usage: 11 | * ``` 12 | * import { Logger } from 'app/core/logger.service'; 13 | * 14 | * const log = new Logger('myFile'); 15 | * ... 16 | * log.debug('something happened'); 17 | * ``` 18 | * 19 | * To disable debug and info logs in production, add this snippet to your root component: 20 | * ``` 21 | * export class AppComponent implements OnInit { 22 | * ngOnInit() { 23 | * if (environment.production) { 24 | * Logger.enableProductionMode(); 25 | * } 26 | * ... 27 | * } 28 | * } 29 | * 30 | * If you want to process logs through other outputs than console, you can add LogOutput functions to Logger.outputs. 31 | */ 32 | 33 | /** 34 | * The possible log levels. 35 | * LogLevel.Off is never emitted and only used with Logger.level property to disable logs. 36 | */ 37 | export enum LogLevel { 38 | Off = 0, 39 | Error, 40 | Warning, 41 | Info, 42 | Debug 43 | } 44 | 45 | /** 46 | * Log output handler function. 47 | */ 48 | export type LogOutput = (source: string | undefined, level: LogLevel, ...objects: any[]) => void; 49 | 50 | export class Logger { 51 | 52 | /** 53 | * Current logging level. 54 | * Set it to LogLevel.Off to disable logs completely. 55 | */ 56 | static level = LogLevel.Debug; 57 | 58 | /** 59 | * Additional log outputs. 60 | */ 61 | static outputs: LogOutput[] = []; 62 | 63 | /** 64 | * Enables production mode. 65 | * Sets logging level to LogLevel.Warning. 66 | */ 67 | static enableProductionMode() { 68 | Logger.level = LogLevel.Warning; 69 | } 70 | 71 | constructor(private source?: string) { } 72 | 73 | /** 74 | * Logs messages or objects with the debug level. 75 | * Works the same as console.log(). 76 | */ 77 | debug(...objects: any[]) { 78 | this.log(console.log, LogLevel.Debug, objects); 79 | } 80 | 81 | /** 82 | * Logs messages or objects with the info level. 83 | * Works the same as console.log(). 84 | */ 85 | info(...objects: any[]) { 86 | this.log(console.info, LogLevel.Info, objects); 87 | } 88 | 89 | /** 90 | * Logs messages or objects with the warning level. 91 | * Works the same as console.log(). 92 | */ 93 | warn(...objects: any[]) { 94 | this.log(console.warn, LogLevel.Warning, objects); 95 | } 96 | 97 | /** 98 | * Logs messages or objects with the error level. 99 | * Works the same as console.log(). 100 | */ 101 | error(...objects: any[]) { 102 | this.log(console.error, LogLevel.Error, objects); 103 | } 104 | 105 | private log(func: (...args: any[]) => void, level: LogLevel, objects: any[]) { 106 | if (level <= Logger.level) { 107 | const log = this.source ? ['[' + this.source + ']'].concat(objects) : objects; 108 | func.apply(console, log); 109 | Logger.outputs.forEach(output => output.apply(output, [this.source, level, ...objects])); 110 | } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-x-rocket", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "ng": "ng", 7 | "build": "npm run write:env -s && ng build", 8 | "start": "npm run write:env -s && ng serve --proxy-config proxy.conf.js", 9 | "serve:sw": "npm run build -s && npx http-server ./dist -p 4200", 10 | "lint": "ng lint && stylelint \"src/**/*.scss\"", 11 | "test": "npm run write:env -s && ng test", 12 | "test:ci": "npm run write:env -s && npm run lint -s && ng run ng-x-rocket:test:ci", 13 | "translations:extract": "ngx-translate-extract --input ./src --output ./src/translations/template.json --format=json --clean --sort", 14 | "docs": "hads ./docs -o", 15 | "write:env": "ngx-scripts env npm_package_version", 16 | "prettier": "prettier --write \"./src/**/*.{ts,js,html,scss}\"", 17 | "prettier:check": "prettier --list-different \"./src/**/*.{ts,js,html,scss}\"", 18 | "postinstall": "npm run prettier -s && husky install", 19 | "generate": "ng generate" 20 | }, 21 | "dependencies": { 22 | "@angular/animations": "~14.1.3", 23 | "@angular/common": "~14.1.3", 24 | "@angular/compiler": "~14.1.3", 25 | "@angular/core": "~14.1.3", 26 | "@angular/forms": "~14.1.3", 27 | "@angular/localize": "~14.1.3", 28 | "@angular/platform-browser": "~14.1.3", 29 | "@angular/platform-browser-dynamic": "~14.1.3", 30 | "@angular/router": "~14.1.3", 31 | "@ngx-translate/core": "^14.0.0", 32 | "@angular/service-worker": "~14.1.3", 33 | "@ng-bootstrap/ng-bootstrap": "^13.0.0", 34 | "@popperjs/core": "^2.11.0", 35 | "bootstrap": "^5.0.2", 36 | "@fortawesome/fontawesome-free": "^6.1.2", 37 | "rxjs": "^7.5.0", 38 | "tslib": "^2.3.0", 39 | "zone.js": "~0.11.4" 40 | }, 41 | "devDependencies": { 42 | "@angular-builders/jest": "^14.0.1", 43 | "@angular-devkit/build-angular": "~14.1.3", 44 | "@angular-eslint/builder": "~14.0.3", 45 | "@angular-eslint/eslint-plugin": "~14.0.3", 46 | "@angular-eslint/eslint-plugin-template": "~14.0.3", 47 | "@angular-eslint/schematics": "~14.0.3", 48 | "@angular-eslint/template-parser": "~14.0.3", 49 | "@angular/cli": "~14.1.3", 50 | "@angular/compiler-cli": "~14.1.3", 51 | "@angular/language-service": "~14.1.3", 52 | "@biesbjerg/ngx-translate-extract": "^7.0.3", 53 | "@biesbjerg/ngx-translate-extract-marker": "^1.0.0", 54 | "@ngx-rocket/scripts": "^5.2.2", 55 | "@ngneat/until-destroy": "^9.0.0", 56 | "@typescript-eslint/eslint-plugin": "~5.34.0", 57 | "@typescript-eslint/parser": "~5.34.0", 58 | "@types/jest": "^28.1.8", 59 | "@types/node": "^14.18.26", 60 | "eslint": "^8.3.0", 61 | "eslint-plugin-import": "latest", 62 | "eslint-plugin-jsdoc": "latest", 63 | "eslint-plugin-prefer-arrow": "latest", 64 | "hads": "^3.0.0", 65 | "https-proxy-agent": "^5.0.0", 66 | "jest": "^28.1.3", 67 | "ts-jest": "^28.0.8", 68 | "prettier": "^2.2.1", 69 | "stylelint-config-prettier": "^9.0.3", 70 | "pretty-quick": "^3.1.0", 71 | "husky": "^8.0.1", 72 | "stylelint": "~14.11.0", 73 | "stylelint-config-recommended-scss": "~7.0.0", 74 | "stylelint-config-standard": "~28.0.0", 75 | "postcss": "^8.4.5", 76 | "ts-node": "^10.1.0", 77 | "typescript": "~4.7.4" 78 | }, 79 | "prettier": { 80 | "singleQuote": true, 81 | "overrides": [ 82 | { 83 | "files": "*.scss", 84 | "options": { 85 | "singleQuote": false 86 | } 87 | } 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/app/auth/authentication.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, fakeAsync, tick } from '@angular/core/testing'; 2 | 3 | import { AuthenticationService } from './authentication.service'; 4 | import { CredentialsService, Credentials } from './credentials.service'; 5 | import { MockCredentialsService } from './credentials.service.mock'; 6 | 7 | describe('AuthenticationService', () => { 8 | let authenticationService: AuthenticationService; 9 | let credentialsService: MockCredentialsService; 10 | 11 | beforeEach(() => { 12 | TestBed.configureTestingModule({ 13 | providers: [{ provide: CredentialsService, useClass: MockCredentialsService }, AuthenticationService] 14 | }); 15 | 16 | authenticationService = TestBed.inject(AuthenticationService); 17 | credentialsService = TestBed.inject(CredentialsService); 18 | credentialsService.credentials = null; 19 | jest.spyOn(credentialsService, 'setCredentials'); 20 | }); 21 | 22 | describe('login', () => { 23 | it('should return credentials', fakeAsync(() => { 24 | // Act 25 | const request = authenticationService.login({ 26 | username: 'toto', 27 | password: '123' 28 | }); 29 | tick(); 30 | 31 | // Assert 32 | request.subscribe(credentials => { 33 | expect(credentials).toBeDefined(); 34 | expect(credentials.token).toBeDefined(); 35 | }); 36 | })); 37 | 38 | it('should authenticate user', fakeAsync(() => { 39 | expect(credentialsService.isAuthenticated()).toBe(false); 40 | 41 | // Act 42 | const request = authenticationService.login({ 43 | username: 'toto', 44 | password: '123' 45 | }); 46 | tick(); 47 | 48 | // Assert 49 | request.subscribe(() => { 50 | expect(credentialsService.isAuthenticated()).toBe(true); 51 | expect(credentialsService.credentials).not.toBeNull(); 52 | expect((credentialsService.credentials as Credentials).token).toBeDefined(); 53 | expect((credentialsService.credentials as Credentials).token).not.toBeNull(); 54 | }); 55 | })); 56 | 57 | it('should persist credentials for the session', fakeAsync(() => { 58 | // Act 59 | const request = authenticationService.login({ 60 | username: 'toto', 61 | password: '123' 62 | }); 63 | tick(); 64 | 65 | // Assert 66 | request.subscribe(() => { 67 | expect(credentialsService.setCredentials).toHaveBeenCalled(); 68 | expect((credentialsService.setCredentials as jest.Mock).mock.calls[0][1]).toBe(undefined); 69 | }); 70 | })); 71 | 72 | it('should persist credentials across sessions', fakeAsync(() => { 73 | // Act 74 | const request = authenticationService.login({ 75 | username: 'toto', 76 | password: '123', 77 | remember: true 78 | }); 79 | tick(); 80 | 81 | // Assert 82 | request.subscribe(() => { 83 | expect(credentialsService.setCredentials).toHaveBeenCalled(); 84 | expect((credentialsService.setCredentials as jest.Mock).mock.calls[0][1]).toBe(true); 85 | }); 86 | })); 87 | }); 88 | 89 | describe('logout', () => { 90 | it('should clear user authentication', fakeAsync(() => { 91 | // Arrange 92 | const loginRequest = authenticationService.login({ 93 | username: 'toto', 94 | password: '123' 95 | }); 96 | tick(); 97 | 98 | // Assert 99 | loginRequest.subscribe(() => { 100 | expect(credentialsService.isAuthenticated()).toBe(true); 101 | 102 | const request = authenticationService.logout(); 103 | tick(); 104 | 105 | request.subscribe(() => { 106 | expect(credentialsService.isAuthenticated()).toBe(false); 107 | expect(credentialsService.credentials).toBeNull(); 108 | }); 109 | }); 110 | })); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ng-x-rocket": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "inlineStyleLanguage": "scss", 29 | "assets": [ 30 | "src/favicon.ico", 31 | "src/apple-touch-icon.png", 32 | "src/robots.txt", 33 | "src/manifest.webmanifest", 34 | "src/assets" 35 | ], 36 | "styles": [ 37 | "src/main.scss" 38 | ], 39 | "scripts": [] 40 | }, 41 | "configurations": { 42 | "production": { 43 | "budgets": [ 44 | { 45 | "type": "initial", 46 | "maximumWarning": "2mb", 47 | "maximumError": "5mb" 48 | }, 49 | { 50 | "type": "anyComponentStyle", 51 | "maximumWarning": "6kb", 52 | "maximumError": "10kb" 53 | } 54 | ], 55 | "serviceWorker": true, 56 | "fileReplacements": [ 57 | { 58 | "replace": "src/environments/environment.ts", 59 | "with": "src/environments/environment.prod.ts" 60 | } 61 | ], 62 | "outputHashing": "all" 63 | }, 64 | "development": { 65 | "buildOptimizer": false, 66 | "optimization": false, 67 | "vendorChunk": true, 68 | "extractLicenses": false, 69 | "sourceMap": true, 70 | "namedChunks": true 71 | }, 72 | "ci": { 73 | "progress": false 74 | } 75 | }, 76 | "defaultConfiguration": "production" 77 | }, 78 | "serve": { 79 | "builder": "@angular-devkit/build-angular:dev-server", 80 | "configurations": { 81 | "production": { 82 | "browserTarget": "ng-x-rocket:build:production" 83 | }, 84 | "development": { 85 | "browserTarget": "ng-x-rocket:build:development" 86 | }, 87 | "ci": { 88 | "progress": false 89 | } 90 | }, 91 | "defaultConfiguration": "development" 92 | }, 93 | "extract-i18n": { 94 | "builder": "@angular-devkit/build-angular:extract-i18n", 95 | "options": { 96 | "browserTarget": "ng-x-rocket:build" 97 | } 98 | }, 99 | "test": { 100 | "builder": "@angular-builders/jest:run", 101 | "options": { 102 | "watch": true 103 | }, 104 | "configurations": { 105 | "ci": { 106 | "watch": false, 107 | "ci": true, 108 | "coverage": true, 109 | "silent": true 110 | } 111 | } 112 | }, 113 | "lint": { 114 | "builder": "@angular-eslint/builder:lint", 115 | "options": { 116 | "lintFilePatterns": [ 117 | "src/**/*.ts", 118 | "src/**/*.html" 119 | ] 120 | } 121 | } 122 | } 123 | } 124 | }, 125 | "defaultProject": "ng-x-rocket" 126 | } 127 | -------------------------------------------------------------------------------- /src/app/i18n/i18n.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { TranslateService, LangChangeEvent } from '@ngx-translate/core'; 3 | import { Subject } from 'rxjs'; 4 | 5 | import { I18nService } from './i18n.service'; 6 | 7 | const defaultLanguage = 'en-US'; 8 | const supportedLanguages = ['eo', 'en-US', 'fr-FR']; 9 | 10 | class MockTranslateService { 11 | 12 | currentLang = ''; 13 | onLangChange = new Subject(); 14 | 15 | use(language: string) { 16 | this.currentLang = language; 17 | this.onLangChange.next({ 18 | lang: this.currentLang, 19 | translations: {} 20 | }); 21 | } 22 | 23 | getBrowserCultureLang() { 24 | return 'en-US'; 25 | } 26 | 27 | setTranslation(lang: string, translations: object, shouldMerge?: boolean) { } 28 | 29 | } 30 | 31 | describe('I18nService', () => { 32 | let i18nService: I18nService; 33 | let translateService: TranslateService; 34 | let onLangChangeSpy: jest.Mock; 35 | 36 | beforeEach(() => { 37 | TestBed.configureTestingModule({ 38 | providers: [ 39 | I18nService, 40 | { provide: TranslateService, useClass: MockTranslateService }, 41 | ] 42 | }); 43 | 44 | i18nService = TestBed.inject(I18nService); 45 | translateService = TestBed.inject(TranslateService); 46 | 47 | // Create spies 48 | onLangChangeSpy = jest.fn(); 49 | translateService.onLangChange 50 | .subscribe((event: LangChangeEvent) => { 51 | onLangChangeSpy(event.lang); 52 | }); 53 | jest.spyOn(translateService, 'use'); 54 | }); 55 | 56 | afterEach(() => { 57 | // Cleanup 58 | localStorage.removeItem('language'); 59 | }); 60 | 61 | describe('init', () => { 62 | it('should init with default language', () => { 63 | // Act 64 | i18nService.init(defaultLanguage, supportedLanguages); 65 | 66 | // Assert 67 | expect(translateService.use).toHaveBeenCalledWith(defaultLanguage); 68 | expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage); 69 | }); 70 | 71 | it('should init with save language', () => { 72 | // Arrange 73 | const savedLanguage = 'eo'; 74 | localStorage.setItem('language', savedLanguage); 75 | 76 | // Act 77 | i18nService.init(defaultLanguage, supportedLanguages); 78 | 79 | // Assert 80 | expect(translateService.use).toHaveBeenCalledWith(savedLanguage); 81 | expect(onLangChangeSpy).toHaveBeenCalledWith(savedLanguage); 82 | }); 83 | }); 84 | 85 | describe('set language', () => { 86 | it('should change current language', () => { 87 | // Arrange 88 | const newLanguage = 'eo'; 89 | i18nService.init(defaultLanguage, supportedLanguages); 90 | 91 | // Act 92 | i18nService.language = newLanguage; 93 | 94 | // Assert 95 | expect(translateService.use).toHaveBeenCalledWith(newLanguage); 96 | expect(onLangChangeSpy).toHaveBeenCalledWith(newLanguage); 97 | }); 98 | 99 | it('should change current language without a region match', () => { 100 | // Arrange 101 | const newLanguage = 'fr-CA'; 102 | i18nService.init(defaultLanguage, supportedLanguages); 103 | 104 | // Act 105 | i18nService.language = newLanguage; 106 | 107 | // Assert 108 | expect(translateService.use).toHaveBeenCalledWith('fr-FR'); 109 | expect(onLangChangeSpy).toHaveBeenCalledWith('fr-FR'); 110 | }); 111 | 112 | it('should change current language to default if unsupported', () => { 113 | // Arrange 114 | const newLanguage = 'es'; 115 | i18nService.init(defaultLanguage, supportedLanguages); 116 | 117 | // Act 118 | i18nService.language = newLanguage; 119 | 120 | // Assert 121 | expect(translateService.use).toHaveBeenCalledWith(defaultLanguage); 122 | expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage); 123 | }); 124 | }); 125 | 126 | describe('get language', () => { 127 | it('should return current language', () => { 128 | // Arrange 129 | i18nService.init(defaultLanguage, supportedLanguages); 130 | 131 | // Act 132 | const currentLanguage = i18nService.language; 133 | 134 | // Assert 135 | expect(currentLanguage).toEqual(defaultLanguage); 136 | }); 137 | }); 138 | 139 | }); 140 | -------------------------------------------------------------------------------- /docs/coding-guides/sass.md: -------------------------------------------------------------------------------- 1 | # Sass coding guide 2 | 3 | [Sass](http://sass-lang.com) is a superset of CSS, which brings a lot of developer candy to help scaling CSS in large 4 | projects and keeping it maintainable. 5 | 6 | The main benefits of using Sass over plain CSS are *variables*, *nesting* and *mixins*, see the 7 | [basics guide](http://sass-lang.com/guide) for more details. 8 | 9 | > Note that this project use the newer, CSS-compatible **SCSS** syntax over the old 10 | [indented syntax](http://sass-lang.com/documentation/file.INDENTED_SYNTAX.html). 11 | 12 | ## Naming conventions 13 | 14 | - In the CSS world, everything should be named in `kebab-case` (lowercase words separated with a `-`). 15 | - File names should always be in `kebab-case` 16 | 17 | ## Coding rules 18 | 19 | - Use single quotes `'` for strings 20 | - Use this general nesting hierarchy when constructing your styles: 21 | ```scss 22 | // The base component class acts as the namespace, to avoid naming and style collisions 23 | .my-component { 24 | // Put here all component elements (flat) 25 | .my-element { 26 | // Use a third-level only for modifiers and state variations 27 | &.active { ... } 28 | } 29 | } 30 | ``` 31 | Note that with 32 | [Angular view encapsulation](https://angular.io/docs/ts/latest/guide/component-styles.html#!#view-encapsulation), 33 | the first "namespace" level of nesting is not necessary as Angular takes care of the scoping for avoid collisions. 34 | 35 | > As a side note, we are aware of the [BEM naming approach](https://en.bem.info/tools/bem/bem-naming/), but we found 36 | it impractical for large projects. The nesting approach has drawbacks such as increased specificity, but it helps 37 | keeping everything nicely organized, and more importantly, *scoped*. 38 | 39 | 40 | Also keep in mind this general rules: 41 | - Always use **class selectors**, never use ID selectors and avoid element selectors whenever possible 42 | - No more than **3 levels** of nesting 43 | - No more than **3 qualifiers** 44 | 45 | ## Best practices 46 | 47 | - Use object-oriented CSS (OOCSS): 48 | * Factorize common code in base class, and extend it, for example: 49 | ```scss 50 | // Base button class 51 | .btn { ... } 52 | 53 | // Color variation 54 | .btn-warning { ... } 55 | 56 | // Size variation 57 | .btn-small { ... } 58 | ``` 59 | * Try to name class by semantic, not style nor function for better reusability: 60 | Use `.btn-warning`, not `btn-orange` nor `btn-cancel` 61 | * Avoid undoing style, refactor using common base classes and extensions 62 | 63 | - Keep your style scoped 64 | * Clearly separate **global** (think *framework*) and **components** style 65 | * Global style should only go in `src/theme/`, never in components 66 | * Avoid style interactions between components, if some style may need to be shared, refactor it as a framework 67 | component in put it in your global theme. 68 | * Avoid using wider selectors than needed: always use classes if you can! 69 | 70 | - Avoid rules multiplication 71 | * The less CSS the better, factorize rules whenever it's possible 72 | * CSS is code, and like any code frequent refactoring is healthy 73 | 74 | - When ugly hacks cannot be avoided, create an explicit `src/hacks.scss` file and put it in: 75 | * These ugly hacks should only be **temporary** 76 | * Each hack should be documented with the author name, the problem and hack reason 77 | * Limit this file to a reasonable length (~100 lines) and refactor hacks with proper solutions when the limit is 78 | reached. 79 | 80 | ## Pitfalls 81 | 82 | - Never use the `!important` keyword. Ever. 83 | - Never use **inline** style in html, even *just for debugging* (because we **KNOW** it will end up in your commit) 84 | 85 | ## Browser compatibility 86 | 87 | You should never use browser-specific prefixes in your code, as [autoprefixer](https://github.com/postcss/autoprefixer) 88 | takes care of that part for you during the build process. 89 | You just need to declare which browsers you target in the [`browserslist`](https://github.com/ai/browserslist) file. 90 | 91 | ## Enforcement 92 | 93 | Coding rules are enforced in this project with [stylelint](https://stylelint.io). 94 | This tool also checks the compatibility of the rules used against the browsers you are targeting (specified in the 95 | [`browserslist`](https://github.com/ai/browserslist) file), via [doiuse](https://github.com/anandthakker/doiuse). 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngX Starter Kit 2 | 3 | Web project starter kit including modern tools and workflow based on 4 | [angular-cli](https://github.com/angular/angular-cli), best practices from the community, a scalable base template and 5 | a good learning base. 6 | 7 | Generated using [ngX-Rocket](https://github.com/ngx-rocket/generator-ngx-rocket). 8 | 9 | ### Benefits 10 | 11 | - Quickstart a project in seconds and focus on features, not on frameworks or tools 12 | 13 | - Industrial-grade tools, ready for usage in a continuous integration environment and DevOps 14 | 15 | - Scalable architecture with base app template including example components, services and tests 16 | 17 | # Getting started 18 | 19 | 1. Go to project folder and install dependencies: 20 | ```bash 21 | npm install 22 | ``` 23 | 24 | 2. Launch development server, and open `localhost:4200` in your browser: 25 | ```bash 26 | npm start 27 | ``` 28 | 29 | # Project structure 30 | 31 | ``` 32 | dist/ compiled version 33 | docs/ project docs and coding guides 34 | e2e/ end-to-end tests 35 | src/ project source code 36 | |- app/ app components 37 | | |- @shared/ shared module (common services, components, directives and pipes) 38 | | |- app.component.* app root component (shell) 39 | | |- app.module.ts app root module definition 40 | | |- app-routing.module.ts app routes 41 | | +- ... additional modules and components 42 | |- assets/ app assets (images, fonts, sounds...) 43 | |- environments/ values for various build environments 44 | |- theme/ app global scss variables and theme 45 | |- translations/ translations files 46 | |- index.html html entry point 47 | |- main.scss global style entry point 48 | |- main.ts app entry point 49 | |- polyfills.ts polyfills needed by Angular 50 | +- test.ts unit tests entry point 51 | reports/ test and coverage reports 52 | proxy.conf.js backend proxy configuration 53 | ``` 54 | 55 | # Main tasks 56 | 57 | Task automation is based on [NPM scripts](https://docs.npmjs.com/misc/scripts). 58 | 59 | Tasks | Description 60 | ------------------------------|--------------------------------------------------------------------------------------- 61 | npm start | Run development server on `http://localhost:4200/` 62 | npm run build [-- --env=prod] | Lint code and build app for production in `dist/` folder 63 | npm test | Run unit tests via [Karma](https://karma-runner.github.io) in watch mode 64 | npm run test:ci | Lint code and run unit tests once for continuous integration 65 | npm run e2e | Run e2e tests using [Protractor](http://www.protractortest.org) 66 | npm run lint | Lint code 67 | npm run translations:extract | Extract strings from code and templates to `src/app/translations/template.json` 68 | npm run docs | Display project documentation 69 | 70 | When building the application, you can specify the target environment using the additional flag `--env ` (do not 71 | forget to prepend `--` to pass arguments to npm scripts). 72 | 73 | The default build environment is `prod`. 74 | 75 | ## Development server 76 | 77 | Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change 78 | any of the source files. 79 | You should not use `ng serve` directly, as it does not use the backend proxy configuration by default. 80 | 81 | ## Code scaffolding 82 | 83 | Run `npm run generate -- component ` to generate a new component. You can also use 84 | `npm run generate -- directive|pipe|service|class|module`. 85 | 86 | If you have installed [angular-cli](https://github.com/angular/angular-cli) globally with `npm install -g @angular/cli`, 87 | you can also use the command `ng generate` directly. 88 | 89 | ## Additional tools 90 | 91 | Tasks are mostly based on the `angular-cli` tool. Use `ng help` to get more help or go check out the 92 | [Angular-CLI README](https://github.com/angular/angular-cli). 93 | 94 | # What's in the box 95 | 96 | The app template is based on [HTML5](http://whatwg.org/html), [TypeScript](http://www.typescriptlang.org) and 97 | [Sass](http://sass-lang.com). The translation files use the common [JSON](http://www.json.org) format. 98 | 99 | #### Tools 100 | 101 | Development, build and quality processes are based on [angular-cli](https://github.com/angular/angular-cli) and 102 | [NPM scripts](https://docs.npmjs.com/misc/scripts), which includes: 103 | 104 | - Optimized build and bundling process with [Webpack](https://webpack.github.io) 105 | - [Development server](https://webpack.github.io/docs/webpack-dev-server.html) with backend proxy and live reload 106 | - Cross-browser CSS with [autoprefixer](https://github.com/postcss/autoprefixer) and 107 | [browserslist](https://github.com/ai/browserslist) 108 | - Asset revisioning for [better cache management](https://webpack.github.io/docs/long-term-caching.html) 109 | - Unit tests using [Jasmine](http://jasmine.github.io) and [Karma](https://karma-runner.github.io) 110 | - End-to-end tests using [Protractor](https://github.com/angular/protractor) 111 | - Static code analysis: [TSLint](https://github.com/palantir/tslint), [Codelyzer](https://github.com/mgechev/codelyzer), 112 | [Stylelint](http://stylelint.io) and [HTMLHint](http://htmlhint.com/) 113 | - Local knowledgebase server using [Hads](https://github.com/sinedied/hads) 114 | - Automatic code formatting with [Prettier](https://prettier.io) 115 | 116 | #### Libraries 117 | 118 | - [Angular](https://angular.io) 119 | - [Bootstrap](https://getbootstrap.com) 120 | - [Font Awesome](http://fontawesome.io) 121 | - [RxJS](http://reactivex.io/rxjs) 122 | - [ng-bootstrap](https://ng-bootstrap.github.io) 123 | - [ngx-translate](https://github.com/ngx-translate/core) 124 | - [Lodash](https://lodash.com) 125 | 126 | #### Coding guides 127 | 128 | - [Angular](docs/coding-guides/angular.md) 129 | - [TypeScript](docs/coding-guides/typescript.md) 130 | - [Sass](docs/coding-guides/sass.md) 131 | - [HTML](docs/coding-guides/html.md) 132 | - [Unit tests](docs/coding-guides/unit-tests.md) 133 | - [End-to-end tests](docs/coding-guides/e2e-tests.md) 134 | 135 | #### Other documentation 136 | 137 | - [I18n guide](docs/i18n.md) 138 | - [Working behind a corporate proxy](docs/corporate-proxy.md) 139 | - [Updating dependencies and tools](docs/updating.md) 140 | - [Using a backend proxy for development](docs/backend-proxy.md) 141 | - [Browser routing](docs/routing.md) 142 | 143 | # License 144 | 145 | [MIT](https://github.com/ngx-rocket/generator-ngx-rocket/blob/main/LICENSE) 146 | -------------------------------------------------------------------------------- /docs/coding-guides/angular.md: -------------------------------------------------------------------------------- 1 | # Introduction to Angular and modern design patterns 2 | 3 | [Angular](https://angular.io) (aka Angular 2, 4, 5, 6...) is a new framework completely rewritten from the ground up, 4 | replacing the now well-known [AngularJS](https://angularjs.org) framework (aka Angular 1.x). 5 | 6 | More that just a framework, Angular should now be considered as a whole *platform* which comes with a complete set of 7 | tools, like its own [CLI](https://github.com/angular/angular-cli), [debug utilities](https://augury.angular.io) or 8 | [performance tools](https://github.com/angular/angular/tree/master/packages/benchpress). 9 | 10 | Angular has been around for some time now, but I still get the feeling that it’s not getting the love it deserved, 11 | probably because of other players in the field like React or VueJS. While the simplicity behind these frameworks can 12 | definitely be attractive, they lack in my opinion what is essential when making big, enterprise-grade apps: a solid 13 | frame to lead both experienced developers and beginners in the same direction and a rational convergence of tools, 14 | patterns and documentation. Yes, the Angular learning curve may seems a little steep, but it’s definitely worth it. 15 | 16 | ## Getting started 17 | 18 | #### Newcomer 19 | 20 | If you're new to Angular you may feel overwhelmed by the quantity of new concepts to apprehend, so before digging 21 | into this project you may want to start with [this progressive tutorial](https://angular.io/tutorial) that will guide 22 | you step by step into building a complete Angular application. 23 | 24 | #### AngularJS veteran 25 | 26 | If you come from AngularJS and want to dig straight in the new version, you may want to take a look at the 27 | [AngularJS vs 2 quick reference](https://angular.io/guide/ajs-quick-reference). 28 | 29 | #### Cheatsheet 30 | 31 | Until you know the full Angular API by heart, you may want to keep this 32 | [cheatsheet](https://angular.io/guide/cheatsheet) that resumes the syntax and features on a single page at hand. 33 | 34 | ## Style guide 35 | 36 | This project follows the standard [Angular style guide](https://angular.io/guide/styleguide). 37 | 38 | More that just coding rules, this style guide also gives advices and best practices for a good application architecture 39 | and is an **essential reading** for starters. Reading deeper, you can even find many explanations for some design 40 | choices of the framework. 41 | 42 | ## FAQ 43 | 44 | There is a lot to dig in Angular and some questions frequently bother people. In fact, most of unclear stuff seems to be 45 | related to modules, for example the dreaded 46 | [**"Core vs Shared modules"**](https://angular.io/guide/ngmodule-faq#what-kinds-of-modules-should-i-have-and-how-should-i-use-them) 47 | question. 48 | 49 | The guys at Angular may have noticed that since you can now find 50 | [a nice FAQ on their website](https://angular.io/guide/ngmodule-faq#ngmodule-faqs) answering all the common questions 51 | regarding modules. Don't hesitate to take a look at it, even if you think you are experienced enough with Angular :wink:. 52 | 53 | ## Going deeper 54 | 55 | Even though they are not mandatory, Angular was designed for the use of design patterns you may not be accustomed to, 56 | like [reactive programming](#reactive-programming), [unidirectional data flow](#unidirectional-data-flow) and 57 | [centralized state management](#centralized-state-management). 58 | 59 | These concepts are difficult to resume in a few words, and despite being tightly related to each other they concern 60 | specific parts of an application flow, each being quite deep to learn on its own. 61 | 62 | You will essentially find here a list of good starting points to learn more on these subjects. 63 | 64 | #### Reactive programming 65 | 66 | You may not be aware of it, but Angular is now a *reactive system* by design. 67 | Although you are not forced to use reactive programming patterns, they make the core of the framework and it is 68 | definitely recommended to learn them if you want to leverage the best of Angular. 69 | 70 | Angular uses [RxJS](http://reactivex.io/rxjs/) to implement the *Observable* pattern. 71 | 72 | > An *Observable* is a stream of asynchronous events that can be processed with array-like operators. 73 | 74 | ##### From promises to observables 75 | 76 | While AngularJS used to rely heavily on [*Promises*](https://docs.angularjs.org/api/ng/service/$q) to handle 77 | asynchronous events, *Observables* are now used instead in Angular. Even though in specific cases like for HTTP 78 | requests, an *Observable* can be converted into a *Promise*, it is recommended to embrace the new paradigm as it can a 79 | lot more than *Promises*, with way less code. This transition is also explained in the 80 | [Angular tutorial](https://angular.io/tutorial/toh-pt6#!%23observables). 81 | Once you have made the switch, you will never look back again. 82 | 83 | ##### Learning references 84 | 85 | - [What is reactive programming?](http://paulstovell.com/blog/reactive-programming), explained nicely through a simple 86 | imaged story *(5 min)* 87 | 88 | - [The introduction to reactive programming you've been missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754), 89 | the title says it all *(30 min)* 90 | 91 | - [Functional reactive programming for Angular 2 developers](http://blog.angular-university.io/functional-reactive-programming-for-angular-2-developers-rxjs-and-observables/), 92 | see the functional reactive programming principles in practice with Angular *(15 min)* 93 | 94 | - [RxMarbles](http://rxmarbles.com), a graphical representation of Rx operators that greatly help to understand their 95 | usage 96 | 97 | #### Unidirectional data flow 98 | 99 | In opposition with AngularJS where one of its selling points was two-way data binding which ended up causing a lot of 100 | major headaches for complex applications, Angular now enforces unidirectional data flow. 101 | 102 | What does it means? Said with other words, it means that change detection cannot cause cycles, which was one of 103 | AngularJS problematic points. It also helps to maintain simpler and more predictable data flows in applications, along 104 | with substantial performance improvements. 105 | 106 | **Wait, then why the Angular documentation have mention of a 107 | [two-way binding syntax](https://angular.io/guide/template-syntax#binding-syntax-an-overview)?** 108 | 109 | If you look closely, the new two-way binding syntax is just syntactic sugar to combine two *one-way* bindings (a 110 | *property* and *event* binding), keeping the data flow unidirectional. 111 | 112 | This change is really important, as it was often the cause of performance issues with AngularJS, and it one of the 113 | pillars enabling better performance in new Angular apps. 114 | 115 | While Angular tries to stay *pattern-agnostic* and can be used with conventional MV* patterns, it was designed with 116 | reactive programming in mind and really shines when used with reactive data flow patterns like 117 | [redux](http://redux.js.org/docs/basics/DataFlow.html), 118 | [Flux](https://facebook.github.io/flux/docs/in-depth-overview.html#content) or 119 | [MVI](http://futurice.com/blog/reactive-mvc-and-the-virtual-dom). 120 | 121 | #### Centralized state management 122 | 123 | As applications grow in size, keeping track of the all its individual components state and data flows can become 124 | tedious, and tend to be difficult to manage and debug. 125 | 126 | The main goal of using a centralized state management is to make state changes *predictable* by imposing certain 127 | restrictions on how and when updates can happen, using *unidirectional data flow*. 128 | 129 | This approach was first made popular with React with introduction of the 130 | [Flux](https://facebook.github.io/flux/docs/in-depth-overview.html#content) architecture. Many libraries emerged then 131 | trying to adapt and refine the original concept, and one of these gained massive popularity by providing a simpler, 132 | elegant alternative: [Redux](http://redux.js.org/docs/basics/DataFlow.html). 133 | 134 | Redux is at the same time a library (with the big *R*) and a design pattern (with the little *r*), the latter being 135 | framework-agnostic and working very well with Angular. 136 | 137 | The *redux* design pattern is based on these [3 principles](http://redux.js.org/docs/introduction/ThreePrinciples.html): 138 | 139 | - The application state is a *single immutable* data structure 140 | - A state change is triggered by an *action*, an object describing what happened 141 | - Pure functions called *reducers* take the previous state and the next action to compute the new state 142 | 143 | The core concepts behind these principles are nicely explained in 144 | [this example](http://redux.js.org/docs/introduction/CoreConcepts.html) *(3 min)*. 145 | 146 | For those interested, the redux pattern was notably inspired by 147 | [The Elm Architecture](https://guide.elm-lang.org/architecture/) and the [CQRS](https://martinfowler.com/bliki/CQRS.html) 148 | pattern. 149 | 150 | ##### Which library to use? 151 | 152 | You can make Angular work with any state management library you like, but your best bet would be to use 153 | [NGXS](http://ngxs.io) or [@ngrx](https://github.com/ngrx/platform). Both works the same as the popular 154 | [Redux](http://redux.js.org) library, but with a tight integration with Angular and [RxJS](http://reactivex.io/rxjs/), 155 | with some nice additional developer utilities. 156 | 157 | NGXS is based on the same concepts as @ngrx, but with less boilerplate and a nicer syntax, making it less intimidating. 158 | 159 | Here are some resources to get started: 160 | 161 | - [Angular NGXS tutorial with example from scratch](https://appdividend.com/2018/07/03/angular-ngxs-tutorial-with-example-from-scratch/), 162 | a guided tutorial for NGXS *(10 min)* 163 | 164 | - [Build a better Angular 2 application with redux and ngrx](http://onehungrymind.com/build-better-angular-2-application-redux-ngrx/), 165 | a nice tutorial for @ngrx *(30 min)* 166 | 167 | - [Comprehensive introduction to @ngrx/store](https://gist.github.com/btroncone/a6e4347326749f938510), an in-depth 168 | walkthrough to this library usage in Angular *(60 min)* 169 | 170 | ##### When to use it? 171 | 172 | You may have noticed that the starter template does not include a centralized state management system out of the box. 173 | Why is that? Well, while there is many benefits from using this pattern, the choice is ultimately up to your team and 174 | what you want to achieve with your app. 175 | 176 | Keep in mind that using a single centralized state for your app introduces a new layer a complexity 177 | [that might not be needed](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367), depending of your 178 | goal. 179 | 180 | ## Optimizing performance 181 | 182 | While the new Angular version resolves by design most of the performance issues that could be experienced with 183 | AngularJS, there is always room for improvements. Just keep in mind that delivering an app with good performance is 184 | often a matter of common sense and sane development practices. 185 | 186 | Here is [a list of key points](https://github.com/mgechev/angular-performance-checklist) to check for in your app to 187 | make sure you deliver the best experience to your customers. 188 | 189 | After going through the checklist, make sure to also run an audit of your page through 190 | [**Lighthouse**](https://developers.google.com/web/tools/lighthouse/), the latest Google tool that gives you meaningful 191 | insight about your app performance, accessibility, mobile compatibility and more. 192 | 193 | ## Keeping Angular up-to-date 194 | 195 | Angular development is moving fast, and updates to the core libs and tools are pushed regularly. 196 | 197 | Fortunately, the Angular team provides tools to help you follow through the updates: 198 | 199 | - `npm run ng update` allows you to update your app and its dependencies 200 | 201 | - The [Angular update website](https://update.angular.io) guides you through Angular changes and migrations, providing 202 | step by step guides from one version to another. 203 | -------------------------------------------------------------------------------- /docs/coding-guides/build-specific-configurations.md: -------------------------------------------------------------------------------- 1 | # Build-Specific Configuration 2 | 3 | ## tl;dr's 4 | 5 | ngx-rocket comes with a very helpful `env` script that will save environment-variables set at build time to constants 6 | that can be used as configuration for your code. When combined with the `dotenv-cli` package, it enables maximum 7 | configurability while maintaining lots of simplicity for local development and testing. 8 | 9 | ### Cookbook for maximum independence of deployment-specific configuration 10 | 11 | Disclaimer: If you have a full-stack app in a monorepo, keep separate `.env` files for server-side and client-side 12 | configs, and make sure `.env` files are .gitignore'd and that secrets never make it into client-side `.env` file. 13 | 14 | For each configurable variable (e.g. BROWSER_URL, API_URL): 15 | 16 | - Add it to package.json's env script so that the build-time variables will be saved for runtime: 17 | 18 | ```javascript 19 | { 20 | "scripts": { 21 | "env": "ngx-scripts env npm_package_version BROWSER_URL API_URL", 22 |  } 23 | } 24 | ``` 25 | 26 | - Add it to or edit it in src/environments/environment.ts to expose it to your app as e.g. environment.API_URL: 27 | ```typescript 28 | export const environment = { 29 | // ... 30 | API_URL: env.API_URL, 31 | BROWSER_URL: env.BROWSER_URL, 32 | // ... 33 | } 34 | ``` 35 | - Configure your CI's deployment to set the variables and export them to the build script before building - if your CI 36 | gives you a shell script to run, make it something like this: 37 | ```shell 38 | # bourne-like shells... 39 | export API_URL='https://api.staging.example.com' 40 | export BROWSER_URL='https://staging.example.com' 41 | # ... 42 | npm run build:ssr-and-client 43 | ``` 44 | - Finally, to have your cake and eat it too and avoid having to do all that for local development and testing (or clutter 45 | your package.json up), install the `dotenv-cli` package and update your development-related npm scripts to take advantage 46 | of it: 47 | ```shell 48 | # environment.development.env.sh 49 | BROWSER_URL='http://localhost:4200' 50 | API_URL='http://localhost:4200' 51 | ``` 52 | ```javascript 53 | { 54 | "scripts": { 55 | "start": "dotenv -e environment.development.env.sh -- npm run env && ng serve --aot", 56 | } 57 | } 58 | ``` 59 | 60 | This way, app configurations will always come from deploy-specific environment variables, and your development environments 61 | are still easy to work with. 62 | 63 | For configuring the build itself (for example, if you want your QA build to be similar to your production build, but with 64 | source maps enabled), consider avoiding adding a build configuration to angular.json, and instead adding the respective 65 | overriding flag to the `ng` command in package.json: 66 | ```javascript 67 | { 68 | "scripts": { 69 | "build:client-and-server-bundles:qa": "NG_BUILD_OVERRIDES='--sourceMap=true' npm run build:client-and-server-bundles", 70 | "build:client-and-server-bundles": "npm run build:client-bundles && npm run build:server-bundles", 71 | "build:client-bundles": "npm run env && ng build --prod $NG_BUILD_OVERRIDES", 72 | } 73 | } 74 | ``` 75 | 76 | The development server API proxy config can read runtime environment variables, so you can avoid having a superficial 77 | dev-server configuration by taking advantage of them: 78 | ```javascript 79 | { 80 | "scripts": { 81 | "start": "dotenv -e environment.development.env.sh -- npm run env && API_PROXY_HOST='http://localhost:9000' ng serve --aot", 82 | } 83 | } 84 | ``` 85 | ```javascript 86 | const proxyConfig = [ 87 | { 88 | context: '/api', 89 | pathRewrite: { '^/api': '' }, 90 | target: `${process.env.API_PROXY_HOST}/api`, 91 | changeOrigin: true, 92 | secure: false, 93 | }, 94 | { 95 | context: '/auth', 96 | pathRewrite: { '^/auth': '' }, 97 | target: `${process.env.API_PROXY_HOST}/auth`, 98 | changeOrigin: true, 99 | secure: false, 100 | }, 101 | ]; 102 | ``` 103 | 104 | Quick SSR note: SSR works by building all the client bundles like normal, but then rendering them in real-time. So, 105 | - the rest of your app from `main.server.ts` down has access to your build-time environment only, like your normal 106 | client bundles 107 | - but `server.ts` (the file configuring and running express) has access to your serve-time environment variables 108 | 109 | ### Less optimal alternatives 110 | 111 | - On the opposite extreme of the spectrum, you can keep all build-specific configuration in a separate environment 112 | file for each environment using Angular's built-in `fileReplacements`, but then you'll need a separate environment 113 | file even for deployment-specific configuration (like hostnames), which can get out of hand fast. 114 | - For a middle-of-the-road approach, you can divide configuration into two groups: 115 | * Configuration shared by each environment-type: 116 | - Environment-type examples include local development, staging/qa, test, production... 117 | - Examples of configuration like this include: 118 | * In test, animations are always disabled, but for all other environments, they're enabled 119 | * In production, the payment gateway's publishable key is the live key, but all other environments use the 120 | test key 121 | * Configuration that sometimes needs to be specific to an individual deployment of a given environment: 122 | - Examples of configuration like this include: 123 | * This particular staging/qa server's base for constructing URLs is qa-stable.example.com, but qa/staging 124 | environments could also be deployed to preprod.example.com or localhost:8081 or anywhereelse:7000. 125 | * This particular deployment uses a specific bucket for Amazon S3 uploads 126 | * In this approach, you can use Angular's `fileReplacements` for anything environment-specific and ngx-rocket's 127 | `env` for anything deployment-specific. You can even have certain deployment-specific configuration fall back 128 | to environment-specific defaults for certain environments like so: 129 | ```javascript 130 | export const environment = { 131 | // ... 132 | BROWSER_URL: env.BROWSER_URL || 'https://qa.example.com', 133 | // ... 134 | } 135 | ``` 136 | - If you don't have lots of environment variables, you can avoid dotenv-cli and use your particular shell's method 137 | to expose the variables before running the ngx-rocket env tool. 138 | 139 | ## Introduction 140 | 141 | When building any maintainable application, a separation of configuration and code is always desired. In the case 142 | of configuration, some of it will need to vary from environment to environment, build to build, or deployment to 143 | deployment. 144 | 145 | This guide focuses on this type of build-specific configuration in a very broad sense of an Angular app, describing 146 | the specific Angular ways of controlling these configurations, detailing some angular-specific challenges, and 147 | highlighting some ngx-rocket tooling that help with them in mind. 148 | 149 | For an even broader non-Angular introduction of these concepts, see the 150 | [The Twelve-Factor App](https://12factor.net/config) methodology's opinions on how this type of configuration 151 | should be managed. 152 | 153 | ## Types of configuration 154 | 155 | At the highest level, build-specific configuration can be divided into two categories: 156 | 157 | 1. Configuration for how your app is built and served 158 | 2. Configuration used by your codebase 159 | 160 | ### Configuration for how your app is built and served 161 | 162 | This type of build-specific configuration is not used by your code, but is used to control the build system itself. 163 | Configuration like this goes into Angular's 164 | [workspace configuration](https://angular.io/guide/workspace-config#alternate-build-configurations). Instead of 165 | rehashing existing documentation on this, this document will highlight how it relates to this subject. Namely, the 166 | fact that in addition to specifying *HOW* the app is built for each build configuration, the workspace configuration 167 | allows mapping each build configuration to a separate environment configuration file for your codebase as well. It 168 | also allows for making separate dev-server configurations in case you need to run it differently. 169 | 170 | Therefore, each build configuration in the workspace configuration file is a tuple of 171 | (how-to-build, environment-file-for-codebase), and you'll need a separate configuration for each combination. 172 | 173 | ## Angular's out-of-the-box environment configuration 174 | 175 | ### When it works well 176 | 177 | This setup works quite well for configuration that's shared among all instances of an environment, like the following 178 | examples: 179 | 180 | - **test** environment always builds without source maps, disables animations, uses a recaptcha test key, and disables 181 | analytics 182 | - **dev** environment always builds with source maps, enables animations, uses a recaptcha live key, and disables 183 | analytics 184 | - **qa** environment always builds with source maps, enables animations, uses a recaptcha live key, and disables 185 | analytics 186 | - **prod** environment always builds without source maps, enables animations, uses a recaptcha live key, and enables 187 | analytics 188 | 189 | ### Limitations of Angular's `fileReplacements` 190 | 191 | But for certain deployment-specific configuration, things start to get really hairy, like in these examples: 192 | 193 | - QA build configuration needs to be built for local deployment, deployment to a server on the internet for QA 194 | purposes, and also deployment to another server on the internet for staging purposes 195 | - Production build needs multiple different deployments of the same app to different servers 196 | 197 | These cases can cause problems when: 198 | 199 | - Each deployment needs a separate API URL 200 | - Each deployment needs a separate URL for building its own URLs to where it's deployed 201 | - Each deployment needs separate API keys, bucket names, etc 202 | 203 | You *COULD* start creating separate configurations for each deployment, each with its own `fileReplacements`, but that 204 | would be really messy. 205 | 206 | ### Workarounds that don't work well 207 | 208 | One workaround would be to keep such configurations as globals in a separate deployment-specific script file. But 209 | that's pretty messy too. More importantly, there are limitations to where they can be used. For example, because 210 | of AOT, such configuration variables cannot be used in Angular's decorators, because they're not statically 211 | analyzable (i.e. their values knowable at build-time). So it would be better if we can keep everything in the same place. 212 | 213 | ### ngx-rocket to the rescue 214 | 215 | The ngx-rocket `env` task solves this problem really well, and avoids the need for separate `environment.ts` files for 216 | deployment-specific configuration. 217 | 218 | To add a deployment-specific configuration: 219 | 220 | 1. edit the existing `environment.ts` files for whichever environments you'd like to make that variable 221 | deployment-specific for by having it come from the imported "env" object - pro tip: you can even make it fall 222 | back to an environment-based default and still be statically analyzable! 223 | 2. add that variable name to the npm script's `env` task 224 | 225 | Now, as long as you have that environment variable set in the shell running the build, the `env` task will save it into 226 | the `.env.ts` file before building. 227 | 228 | If you really want, you can take things even further to the twelve-factor extreme, and you can even eliminate the 229 | need for `fileReplacements` entirely, and make all configuration come from environment variables. Whether this will be 230 | the right approach for your project will be up to you. 231 | 232 | This makes separate deployments awesome and flexible, but unfortunately makes things a little bit of a hassle for your 233 | local development, test, etc. environments because you have the burden of providing all those keys, settings, etc. as 234 | environment variables. 235 | 236 | To avoid having to do that, you'll can create a .gitignore'd `.env` file with all the variables set, and source it 237 | with your shell (e.g. `source .env.sh && npm env` in bourne-like shells or `env.bat; npm env` in windows). 238 | ```shell 239 | # bourne-like .env.sh 240 | export BROWSER_URL=localhost:4200 241 | ``` 242 | ```shell 243 | REM windows env.bat 244 | SET BROWSER_URL=localhost:4200 245 | ``` 246 | 247 | Luckily for us, there's a package called `dotenv-cli` that uses the `dotenv` package and does this in a cleaner and 248 | cross-platform way and comes with even more bells and whistles. You should use that instead, and make your env file 249 | like this instead: 250 | ```shell 251 | BROWSER_URL=localhost:4200 252 | ``` 253 | 254 | ## When you can use environment variables directly without ngx-rocket `env` 255 | 256 | As a sidenote, ngx-rocket `env` isn't used for the proxy config file, because it isn't built and ran separately. 257 | Fortunately, for that same reason, you can directly use `process.env` within the proxy config file to avoid having 258 | separate proxy configs in most cases. 259 | 260 | On that same note, the `server.ts` for SSR builds can also access `process.env` as it's set at runtime. But keep in mind 261 | that it stops there - the app itself is built, so even in SSR the client app can't access process environment variables. 262 | 263 | ## Security Considerations 264 | 265 | Never forget that your entire Angular app goes to the client, including its configuration, including the environment 266 | variables you pass to the env task! As usual, you should **never add sensitive keys or secrets to the env task**. 267 | 268 | Finally, if your Angular project is the client-side of a full-stack monorepo, make sure to keep the client-side `.env` 269 | file separate from the server-side `.env` file, since your server-side is bound to have secrets. 270 | --------------------------------------------------------------------------------