├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── config ├── development.json ├── env.json ├── production.json └── test.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── favicon.ico ├── hooks ├── post-build.js ├── pre-build.js ├── pre-start.js └── pre-test.js ├── i18n ├── auth.en.json ├── components.en.json ├── en.json └── general.en.json ├── index.html ├── package.json ├── proxy.conf.json ├── src ├── app │ ├── app-config.service.ts │ ├── app-routing.module.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── auth │ │ ├── auth-api-client.service.ts │ │ ├── auth-routing.module.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ └── components │ │ │ └── login │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ ├── login.component.spec.ts │ │ │ └── login.component.ts │ ├── features-modules │ │ ├── dashboard │ │ │ ├── dashboard-routing.module.ts │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.scss │ │ │ ├── dashboard.component.ts │ │ │ └── dashboard.module.ts │ │ └── posts │ │ │ ├── components │ │ │ ├── post-details │ │ │ │ ├── post-details.component.html │ │ │ │ ├── post-details.component.scss │ │ │ │ ├── post-details.component.spec.ts │ │ │ │ └── post-details.component.ts │ │ │ ├── post-form │ │ │ │ ├── post-form.component.html │ │ │ │ ├── post-form.component.scss │ │ │ │ ├── post-form.component.spec.ts │ │ │ │ └── post-form.component.ts │ │ │ └── posts-list │ │ │ │ ├── posts-list.component.html │ │ │ │ ├── posts-list.component.scss │ │ │ │ ├── posts-list.component.spec.ts │ │ │ │ └── posts-list.component.ts │ │ │ ├── posts-api-client.service.ts │ │ │ ├── posts-routing.module.ts │ │ │ ├── posts.module.ts │ │ │ └── posts.service.ts │ └── shared │ │ ├── animations │ │ ├── fadeIn.animation.ts │ │ ├── fallIn.animation.ts │ │ ├── index.ts │ │ ├── moveIn.animation.ts │ │ ├── moveInLeft.animation.ts │ │ └── slideInRight.animation.ts │ │ ├── async-services │ │ └── http │ │ │ ├── data.service.ts │ │ │ ├── http-response-handler.service.ts │ │ │ ├── http.interceptor.ts │ │ │ ├── http.module.ts │ │ │ └── index.ts │ │ ├── components │ │ ├── footer │ │ │ ├── footer.component.html │ │ │ ├── footer.component.scss │ │ │ └── footer.component.ts │ │ ├── header │ │ │ ├── header.component.html │ │ │ ├── header.component.scss │ │ │ └── header.component.ts │ │ ├── index.ts │ │ ├── loading-placeholder │ │ │ ├── loading-placeholder.component.scss │ │ │ └── loading-placeholder.component.ts │ │ ├── page-not-found │ │ │ ├── page-not-found.component.scss │ │ │ └── page-not-found.component.ts │ │ └── spinner │ │ │ ├── spinner.component.scss │ │ │ └── spinner.component.ts │ │ ├── containers │ │ ├── index.ts │ │ └── layout │ │ │ ├── layout.component.scss │ │ │ └── layout.component.ts │ │ ├── errors │ │ ├── components │ │ │ ├── errors.component.html │ │ │ ├── errors.component.scss │ │ │ └── errors.component.ts │ │ ├── errors-handler.ts │ │ ├── errors-routing.module.ts │ │ ├── errors.module.ts │ │ ├── errors.service.ts │ │ ├── index.ts │ │ └── server-errors.interceptor.ts │ │ ├── guards │ │ ├── auth.guard.ts │ │ └── can-deactivate.guard.ts │ │ ├── models │ │ ├── auth │ │ │ ├── login.model.ts │ │ │ ├── register.model.ts │ │ │ └── user.model.ts │ │ └── index.ts │ │ ├── pipes │ │ ├── index.ts │ │ ├── sanitize-html.pipe.ts │ │ └── truncate.pipe.ts │ │ ├── services │ │ └── .gitkeep │ │ └── utility │ │ ├── index.ts │ │ ├── utility.module.ts │ │ ├── utility.service.ts │ │ ├── utilityHelpers.ts │ │ └── validation.service.ts ├── assets │ ├── .gitkeep │ └── images │ │ ├── Martian.png │ │ ├── Stars.png │ │ ├── logo.svg │ │ └── users │ │ └── user.jpg ├── browserslist ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── scss │ ├── _custom-styles.scss │ ├── _custom-variables.scss │ └── mytheme.scss ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── sw-precache-config.js ├── tsconfig.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Maher Sghaier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Scalable Angular Architecture Guide 4 |

5 |
A cohesive guide for building Angular applications for teams.
6 | 7 |

:warning: Work In Progress :warning:

8 | 9 | ## Folder Structure 10 | 11 | ``` 12 | |-- config 13 | | |-- development.json 14 | | |-- env.json 15 | | |-- production.json 16 | | `-- test.json 17 | |-- hooks 18 | | |-- post-build.js 19 | | |-- pre-build.js 20 | | |-- pre-start.js 21 | | `-- pre-test.js 22 | |-- i18n 23 | | |-- auth.en.json 24 | | |-- components.en.json 25 | | |-- en.json 26 | | `-- general.en.json 27 | |-- index.html 28 | |-- proxy.conf.json 29 | |-- src 30 | | |-- app 31 | | | |-- app.component.ts 32 | | | |-- app-config.service.ts 33 | | | |-- app.module.ts 34 | | | |-- app-routing.module.ts 35 | | | |-- auth 36 | | | | |-- auth-api-client.service.ts 37 | | | | |-- auth.module.ts 38 | | | | |-- auth-routing.module.ts 39 | | | | |-- auth.service.ts 40 | | | | `-- components 41 | | | | `-- login 42 | | | | |-- login.component.html 43 | | | | |-- login.component.scss 44 | | | | |-- login.component.spec.ts 45 | | | | `-- login.component.ts 46 | | | |-- features-modules 47 | | | | |-- dashboard 48 | | | | | |-- dashboard.component.html 49 | | | | | |-- dashboard.component.scss 50 | | | | | |-- dashboard.component.ts 51 | | | | | |-- dashboard.module.ts 52 | | | | | `-- dashboard-routing.module.ts 53 | | | | |-- posts 54 | | | | | |-- components 55 | | | | | | |-- post-details 56 | | | | | | | |-- post-details.component.html 57 | | | | | | | |-- post-details.component.scss 58 | | | | | | | |-- post-details.component.spec.ts 59 | | | | | | | `-- post-details.component.ts 60 | | | | | | |-- post-form 61 | | | | | | | |-- post-form.component.html 62 | | | | | | | |-- post-form.component.scss 63 | | | | | | | |-- post-form.component.spec.ts 64 | | | | | | | `-- post-form.component.ts 65 | | | | | | `-- posts-list 66 | | | | | | |-- posts-list.component.html 67 | | | | | | |-- posts-list.component.scss 68 | | | | | | |-- posts-list.component.spec.ts 69 | | | | | | `-- posts-list.component.ts 70 | | | | | |-- posts-api-client.service.ts 71 | | | | | |-- posts.module.ts 72 | | | | | |-- posts-routing.module.ts 73 | | | | | `-- posts.service.ts 74 | | | | `-- ... 75 | | | `-- shared 76 | | | |-- animations 77 | | | | |-- fadeIn.animation.ts 78 | | | | |-- fallIn.animation.ts 79 | | | | |-- index.ts 80 | | | | |-- moveIn.animation.ts 81 | | | | |-- moveInLeft.animation.ts 82 | | | | `-- slideInRight.animation.ts 83 | | | |-- async-services 84 | | | | `-- http 85 | | | | |-- data.service.ts 86 | | | | |-- http.interceptor.ts 87 | | | | |-- http.module.ts 88 | | | | |-- http-response-handler.service.ts 89 | | | | `-- index.ts 90 | | | |-- components 91 | | | | |-- footer 92 | | | | | |-- footer.component.html 93 | | | | | |-- footer.component.scss 94 | | | | | `-- footer.component.ts 95 | | | | |-- header 96 | | | | | |-- header.component.html 97 | | | | | |-- header.component.scss 98 | | | | | `-- header.component.ts 99 | | | | |-- index.ts 100 | | | | |-- loading-placeholder 101 | | | | | |-- loading-placeholder.component.scss 102 | | | | | `-- loading-placeholder.component.ts 103 | | | | |-- page-not-found 104 | | | | | |-- page-not-found.component.scss 105 | | | | | `-- page-not-found.component.ts 106 | | | | `-- spinner 107 | | | | |-- spinner.component.scss 108 | | | | `-- spinner.component.ts 109 | | | |-- containers 110 | | | | |-- index.ts 111 | | | | `-- layout 112 | | | | |-- layout.component.scss 113 | | | | `-- layout.component.ts 114 | | | |-- errors 115 | | | | |-- components 116 | | | | | |-- errors.component.html 117 | | | | | |-- errors.component.scss 118 | | | | | `-- errors.component.ts 119 | | | | |-- errors-handler.ts 120 | | | | |-- errors.module.ts 121 | | | | |-- errors-routing.module.ts 122 | | | | |-- errors.service.ts 123 | | | | |-- index.ts 124 | | | | `-- server-errors.interceptor.ts 125 | | | |-- guards 126 | | | | |-- auth.guard.ts 127 | | | | `-- can-deactivate.guard.ts 128 | | | |-- models 129 | | | | |-- auth 130 | | | | | |-- login.model.ts 131 | | | | | |-- register.model.ts 132 | | | | | `-- user.model.ts 133 | | | | `-- index.ts 134 | | | |-- interfaces 135 | | | | `-- post.interface.ts 136 | | | |-- pipes 137 | | | | |-- index.ts 138 | | | | |-- sanitize-html.pipe.ts 139 | | | | `-- truncate.pipe.ts 140 | | | |-- services 141 | | | `-- utility 142 | | | |-- index.ts 143 | | | |-- utilityHelpers.ts 144 | | | |-- utility.module.ts 145 | | | |-- utility.service.ts 146 | | | `-- validation.service.ts 147 | | |-- assets 148 | | | `-- ... 149 | | |-- index.html 150 | | |-- scss 151 | | | |-- _custom-styles.scss 152 | | | |-- _custom-variables.scss 153 | | | `-- mytheme.scss 154 | | |-- styles.scss 155 | | `-- ... 156 | |-- sw-precache-config.js 157 | `-- ... 158 | ``` 159 | 160 | ## Stuff I included 161 | 162 | - Root architecture 163 | - Config app Service 164 | - Route configuration 165 | 166 | - Features modules architecture 167 | - Component architecture 168 | - Directory structure 169 | 170 | - Auth module 171 | - JWT Interceptor 172 | - Guards 173 | 174 | - Shared module contain (animations components, layouts, pipes, etc) 175 | - TypeScript models/interfaces 176 | - Errors Handler (http, client, server, expected/unexpected errors) with tracking errors 177 | - Async Services for (Http, WebSocket, WebRTC, etc) 178 | 179 | - CSS options (Sass/etc) 180 | - Bootstrap integration 181 | - Themes 182 | 183 | - Translation Service 184 | - Structure practices (lifecycle hooks/DI practices) 185 | - Tooling (AoT/Webpack/etc) 186 | 187 | ## Stuff in progress 188 | 189 | - State management (ngrx/store etc) 190 | 191 | Inspired From: 192 | 193 | - Angular architecture patterns http://netmedia.io/blog/angular-architecture-patterns-additional-application-features_5670 194 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-architecture": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "styleext": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/angular-architecture", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "src/tsconfig.app.json", 25 | "assets": [ 26 | "src/favicon.ico", 27 | "src/assets", 28 | { 29 | "glob": "**/*", 30 | "input": "./config", 31 | "output": "./config/" 32 | }, 33 | { 34 | "glob": "en.json", 35 | "input": "./i18n", 36 | "output": "./assets/i18n/" 37 | } 38 | ], 39 | "styles": [ 40 | "src/scss/mytheme.scss", 41 | "src/styles.scss" 42 | ], 43 | "scripts": [] 44 | }, 45 | "configurations": { 46 | "production": { 47 | "fileReplacements": [{ 48 | "replace": "src/environments/environment.ts", 49 | "with": "src/environments/environment.prod.ts" 50 | }], 51 | "optimization": true, 52 | "outputHashing": "all", 53 | "sourceMap": false, 54 | "extractCss": true, 55 | "namedChunks": false, 56 | "aot": true, 57 | "extractLicenses": true, 58 | "vendorChunk": false, 59 | "buildOptimizer": true 60 | } 61 | } 62 | }, 63 | "serve": { 64 | "builder": "@angular-devkit/build-angular:dev-server", 65 | "options": { 66 | "browserTarget": "angular-architecture:build" 67 | }, 68 | "configurations": { 69 | "production": { 70 | "browserTarget": "angular-architecture:build:production" 71 | } 72 | } 73 | }, 74 | "extract-i18n": { 75 | "builder": "@angular-devkit/build-angular:extract-i18n", 76 | "options": { 77 | "browserTarget": "angular-architecture:build" 78 | } 79 | }, 80 | "test": { 81 | "builder": "@angular-devkit/build-angular:karma", 82 | "options": { 83 | "main": "src/test.ts", 84 | "polyfills": "src/polyfills.ts", 85 | "tsConfig": "src/tsconfig.spec.json", 86 | "karmaConfig": "src/karma.conf.js", 87 | "styles": [ 88 | "src/styles.scss" 89 | ], 90 | "scripts": [], 91 | "assets": [ 92 | "src/favicon.ico", 93 | "src/assets" 94 | ] 95 | } 96 | }, 97 | "lint": { 98 | "builder": "@angular-devkit/build-angular:tslint", 99 | "options": { 100 | "tsConfig": [ 101 | "src/tsconfig.app.json", 102 | "src/tsconfig.spec.json" 103 | ], 104 | "exclude": [ 105 | "**/node_modules/**" 106 | ] 107 | } 108 | } 109 | } 110 | }, 111 | "angular-architecture-e2e": { 112 | "root": "e2e/", 113 | "projectType": "application", 114 | "architect": { 115 | "e2e": { 116 | "builder": "@angular-devkit/build-angular:protractor", 117 | "options": { 118 | "protractorConfig": "e2e/protractor.conf.js", 119 | "devServerTarget": "angular-architecture:serve" 120 | }, 121 | "configurations": { 122 | "production": { 123 | "devServerTarget": "angular-architecture:serve:production" 124 | } 125 | } 126 | }, 127 | "lint": { 128 | "builder": "@angular-devkit/build-angular:tslint", 129 | "options": { 130 | "tsConfig": "e2e/tsconfig.e2e.json", 131 | "exclude": [ 132 | "**/node_modules/**" 133 | ] 134 | } 135 | } 136 | } 137 | } 138 | }, 139 | "defaultProject": "angular-architecture" 140 | } 141 | -------------------------------------------------------------------------------- /config/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "baseUrl": "/api" 4 | }, 5 | 6 | "paths": { 7 | "imagesRoot": "/assets/images/", 8 | "userImageFolder": "/assets/images/users/" 9 | }, 10 | 11 | "localization": { 12 | "languages": [{ 13 | "code": "en", 14 | "name": "EN", 15 | "culture": "en-EN" 16 | }], 17 | "defaultLanguage": "en" 18 | }, 19 | 20 | "notifications": { 21 | "options": { 22 | "timeOut": 5000, 23 | "showProgressBar": true, 24 | "pauseOnHover": true, 25 | "position": ["top", "right"], 26 | "theClass": "sy-notification" 27 | }, 28 | "unauthorizedEndpoints": ["api/posts"], 29 | "notFoundEndpoints": ["api/posts", "api/account/login", "api/account/register"] 30 | }, 31 | 32 | "debugging": true 33 | } 34 | -------------------------------------------------------------------------------- /config/env.json: -------------------------------------------------------------------------------- 1 | {"env":"development"} 2 | -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "baseUrl": "/api" 4 | }, 5 | 6 | "paths": { 7 | "imagesRoot": "/assets/images/", 8 | "userImageFolder": "/assets/images/users/" 9 | }, 10 | 11 | "localization": { 12 | "languages": [{ 13 | "code": "en", 14 | "name": "EN", 15 | "culture": "en-EN" 16 | }], 17 | "defaultLanguage": "en" 18 | }, 19 | 20 | "notifications": { 21 | "options": { 22 | "timeOut": 5000, 23 | "showProgressBar": true, 24 | "pauseOnHover": true, 25 | "position": ["top", "right"], 26 | "theClass": "sy-notification" 27 | }, 28 | "unauthorizedEndpoints": ["api/posts"], 29 | "notFoundEndpoints": ["api/posts", "api/account/login", "api/account/register"] 30 | }, 31 | 32 | "debugging": false 33 | } 34 | -------------------------------------------------------------------------------- /config/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "baseUrl": "/api" 4 | }, 5 | 6 | "paths": { 7 | "imagesRoot": "/assets/images/", 8 | "userImageFolder": "/assets/images/users/" 9 | }, 10 | 11 | "localization": { 12 | "languages": [{ 13 | "code": "en", 14 | "name": "EN", 15 | "culture": "en-EN" 16 | }], 17 | "defaultLanguage": "en" 18 | }, 19 | 20 | "notifications": { 21 | "options": { 22 | "timeOut": 5000, 23 | "showProgressBar": true, 24 | "pauseOnHover": true, 25 | "position": ["top", "right"], 26 | "theClass": "sy-notification" 27 | }, 28 | "unauthorizedEndpoints": ["api/posts"], 29 | "notFoundEndpoints": ["api/posts", "api/account/login", "api/account/register"] 30 | }, 31 | 32 | "debugging": true 33 | } 34 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to angular-architecture!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaherSghaier/scalable-angular-architecture/d300805d84ffc8c79b5b25e6a347920baf062ea8/favicon.ico -------------------------------------------------------------------------------- /hooks/post-build.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | var foldersToCopy = [{ 3 | src: './config', 4 | dest: './dist/config' 5 | }, 6 | { 7 | src: './i18n', 8 | dest: './dist/i18n' 9 | } 10 | ]; 11 | 12 | // copies directory, even if it has subdirectories or files 13 | function copyDir(src, dest) { 14 | fs.copy(src, dest, function (err) { 15 | if (err) return console.error(err) 16 | console.log(src + ' folder successfully copied') 17 | }); 18 | } 19 | 20 | for (var i = foldersToCopy.length - 1; i >= 0; i--) { 21 | copyDir(foldersToCopy[i].src, foldersToCopy[i].dest); 22 | } 23 | -------------------------------------------------------------------------------- /hooks/pre-build.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | var jsonConcat = require("json-concat"); 3 | 4 | var localizationSourceFilesEN = [ 5 | "./i18n/general.en.json", 6 | "src/i18n/auth.en.json", 7 | "./i18n/components.en.json" 8 | ]; 9 | 10 | function mergeAndSaveJsonFiles(src, dest) { 11 | jsonConcat({ 12 | src: src, 13 | dest: dest 14 | }, 15 | function (res) { 16 | console.log('Localization files successfully merged!'); 17 | } 18 | ); 19 | } 20 | 21 | function setEnvironment(configPath, environment) { 22 | fs.writeJson(configPath, { 23 | env: environment 24 | }, 25 | function (res) { 26 | console.log('Environment variable set to ' + environment) 27 | } 28 | ); 29 | } 30 | 31 | // Set environment variable to "production" 32 | setEnvironment('./src/config/env.json', 'production'); 33 | 34 | // Merge all localization files into one 35 | mergeAndSaveJsonFiles(localizationSourceFilesEN, "./src/i18n/en.json"); 36 | -------------------------------------------------------------------------------- /hooks/pre-start.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | var jsonConcat = require("json-concat"); 3 | 4 | var localizationSourceFilesEN = [ 5 | "./i18n/general.en.json", 6 | "./i18n/auth.en.json", 7 | "./i18n/components.en.json" 8 | ]; 9 | 10 | 11 | function mergeAndSaveJsonFiles(src, dest) { 12 | jsonConcat({ 13 | src: src, 14 | dest: dest 15 | }, 16 | function (res) { 17 | console.log('Localization files successfully merged!'); 18 | } 19 | ); 20 | } 21 | 22 | function setEnvironment(configPath, environment) { 23 | fs.writeJson(configPath, { 24 | env: environment 25 | }, 26 | function (res) { 27 | console.log('Environment variable set to ' + environment) 28 | } 29 | ); 30 | } 31 | 32 | // Set environment variable to "development" 33 | setEnvironment('./config/env.json', 'development'); 34 | 35 | // Merge all localization files into one 36 | mergeAndSaveJsonFiles(localizationSourceFilesEN, "./i18n/en.json"); 37 | -------------------------------------------------------------------------------- /hooks/pre-test.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | 3 | function setEnvironment(configPath, environment) { 4 | fs.writeJson(configPath, { 5 | env: environment 6 | }, 7 | function (res) { 8 | console.log('Environment variable set to ' + environment) 9 | } 10 | ); 11 | } 12 | 13 | // Set environment variable to "test" 14 | setEnvironment('./config/env.json', 'test'); 15 | -------------------------------------------------------------------------------- /i18n/auth.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Auth": { 3 | "Email": "Email", 4 | "Password": "Password", 5 | "ConfirmPassword": "Confirm Password", 6 | "EmailFormatError": "Email is not in valid format", 7 | "EmailRequiredError": "Email is required", 8 | "PasswordRequiredError": "Password is required", 9 | "ConfirmPasswordError": "Passwords mismatch", 10 | "PasswordLengthError": "Passwords must contain at least 6 characters", 11 | 12 | "Login": { 13 | "Title": "Login", 14 | "Submit": "Login", 15 | "RegisterLink": "Sign Up" 16 | }, 17 | 18 | "Register": { 19 | "Title": "Sign Up", 20 | "Submit": "Create account", 21 | "Back": "Go Back", 22 | "SuccessMessage": "Your account has been successfully created" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /i18n/components.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sidebar": { 3 | "Menu": "Menu", 4 | "ProductsItem": "Products" 5 | }, 6 | 7 | "ProfileActionBar": { 8 | "Logout": "Logout" 9 | }, 10 | 11 | "PageNotFound": { 12 | "Title": "Ooops, Page Not Found", 13 | "Subtitle": "Please, return to the previous page", 14 | "Button": "Go back" 15 | } 16 | } -------------------------------------------------------------------------------- /i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerError401": "Access not allowed. Please login.", 3 | "ServerError403": "Access forbidden. Please provide correct credentials", 4 | "ServerError404": "An error occurred. Please contact your administrator", 5 | "ServerError500": "An error occurred. Please contact your administrator", 6 | "SuccessNotificationTitle": "Success", 7 | "ErrorNotificationTitle": "Error", 8 | "InfoNotificationTitle": "Info", 9 | "WarningNotificationTitle": "Warning", 10 | "SaveBtn": "Save", 11 | "EditBtn": "Edit", 12 | "CancelBtn": "Cancel", 13 | "BackBtn": "Back", 14 | "GridEmptyLabel": "No available items", 15 | 16 | "ConfirmDialog": { 17 | "Title": "Please confirm", 18 | "Content": "Do you want to leave without saving the changes?", 19 | "SubmitBtn": "Yes", 20 | "CancelBtn": "No" 21 | } 22 | , 23 | "Auth": { 24 | "Email": "Email", 25 | "Password": "Password", 26 | "ConfirmPassword": "Confirm Password", 27 | "EmailFormatError": "Email is not in valid format", 28 | "EmailRequiredError": "Email is required", 29 | "PasswordRequiredError": "Password is required", 30 | "ConfirmPasswordError": "Passwords mismatch", 31 | "PasswordLengthError": "Passwords must contain at least 6 characters", 32 | 33 | "Login": { 34 | "Title": "Login", 35 | "Submit": "Login", 36 | "RegisterLink": "Sign Up" 37 | }, 38 | 39 | "Register": { 40 | "Title": "Sign Up", 41 | "Submit": "Create account", 42 | "Back": "Go Back", 43 | "SuccessMessage": "Your account has been successfully created" 44 | } 45 | } 46 | , 47 | "Sidebar": { 48 | "Menu": "Menu", 49 | "ProductsItem": "Products" 50 | }, 51 | 52 | "ProfileActionBar": { 53 | "Logout": "Logout" 54 | }, 55 | 56 | "PageNotFound": { 57 | "Title": "Ooops, Page Not Found", 58 | "Subtitle": "Please, return to the previous page", 59 | "Button": "Go back" 60 | } 61 | } -------------------------------------------------------------------------------- /i18n/general.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerError401": "Access not allowed. Please login.", 3 | "ServerError403": "Access forbidden. Please provide correct credentials", 4 | "ServerError404": "An error occurred. Please contact your administrator", 5 | "ServerError500": "An error occurred. Please contact your administrator", 6 | "SuccessNotificationTitle": "Success", 7 | "ErrorNotificationTitle": "Error", 8 | "InfoNotificationTitle": "Info", 9 | "WarningNotificationTitle": "Warning", 10 | "SaveBtn": "Save", 11 | "EditBtn": "Edit", 12 | "CancelBtn": "Cancel", 13 | "BackBtn": "Back", 14 | "GridEmptyLabel": "No available items", 15 | 16 | "ConfirmDialog": { 17 | "Title": "Please confirm", 18 | "Content": "Do you want to leave without saving the changes?", 19 | "SubmitBtn": "Yes", 20 | "CancelBtn": "No" 21 | } 22 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular Architecture 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Loading... 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-architecture", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "npm run sy-pre-start & ng serve --proxy-config proxy.conf.json", 7 | "lint": "ng lint", 8 | "test": "npm run sy-pre-test & ng test", 9 | "pree2e": "webdriver-manager update --standalone false --gecko false", 10 | "e2e": "ng e2e", 11 | "sy-pre-test": "node hooks/pre-test.js", 12 | "sy-pre-start": "node hooks/pre-start.js", 13 | "sy-pre-build": "node hooks/pre-build.js", 14 | "sy-post-build": "node hooks/post-build.js", 15 | "sw": "sw-precache --root=./dist --config=sw-precache-config.js", 16 | "sy-build": "npm run sy-pre-build & ng build --prod --aot & npm run sy-post-build & npm run sw" 17 | }, 18 | "private": true, 19 | "dependencies": { 20 | "@angular/animations": "^6.0.3", 21 | "@angular/common": "^6.0.3", 22 | "@angular/compiler": "^6.0.3", 23 | "@angular/core": "^6.0.3", 24 | "@angular/forms": "^6.0.3", 25 | "@angular/http": "^6.0.3", 26 | "@angular/platform-browser": "^6.0.3", 27 | "@angular/platform-browser-dynamic": "^6.0.3", 28 | "@angular/router": "^6.0.3", 29 | "@auth0/angular-jwt": "^2.0.0", 30 | "@ng-bootstrap/ng-bootstrap": "^2.0.0", 31 | "@ngx-translate/core": "^10.0.2", 32 | "@ngx-translate/http-loader": "^3.0.1", 33 | "angular2-notifications": "^1.0.2", 34 | "bootstrap": "^4.1.1", 35 | "core-js": "^2.5.4", 36 | "error-stack-parser": "^2.0.1", 37 | "font-awesome": "^4.7.0", 38 | "moment": "^2.22.1", 39 | "ngx-pipes": "^2.1.7", 40 | "rxjs": "^6.0.0", 41 | "zone.js": "^0.8.26" 42 | }, 43 | "devDependencies": { 44 | "@angular-devkit/build-angular": "~0.6.6", 45 | "@angular/cli": "~6.0.7", 46 | "@angular/compiler-cli": "^6.0.3", 47 | "@angular/language-service": "^6.0.3", 48 | "@types/jasmine": "~2.8.6", 49 | "@types/jasminewd2": "~2.0.3", 50 | "@types/node": "~8.9.4", 51 | "codelyzer": "~4.2.1", 52 | "fs-extra": "^6.0.1", 53 | "jasmine-core": "~2.99.1", 54 | "jasmine-spec-reporter": "~4.2.1", 55 | "json-concat": "^0.0.1", 56 | "karma": "~1.7.1", 57 | "karma-chrome-launcher": "~2.2.0", 58 | "karma-coverage-istanbul-reporter": "~2.0.0", 59 | "karma-jasmine": "~1.1.1", 60 | "karma-jasmine-html-reporter": "^0.2.2", 61 | "protractor": "~5.3.0", 62 | "ts-node": "~5.0.1", 63 | "tslint": "~5.9.1", 64 | "typescript": "~2.7.2" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "https://jsonplaceholder.typicode.com", 4 | "secure": true, 5 | "changeOrigin": true, 6 | "pathRewrite": { 7 | "^/api": "" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, throwError } from 'rxjs'; 3 | import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; 4 | import { catchError } from 'rxjs/operators'; 5 | 6 | @Injectable() 7 | export class ConfigService { 8 | private config: Object; 9 | private env: Object; 10 | 11 | constructor(private httpClient: HttpClient) {} 12 | 13 | /** 14 | * Loads the environment config file first. Reads the environment variable from the file 15 | * and based on that loads the appropriate configuration file - development or production 16 | */ 17 | load() { 18 | return new Promise((resolve, reject) => { 19 | const httpOptions = { 20 | headers: new HttpHeaders({ 21 | Accept: 'application/json', 22 | 'Content-Type': 'application/json', 23 | DataType: 'application/json' 24 | }) 25 | }; 26 | 27 | this.httpClient.get<{ env: string }>('/config/env.json').subscribe(env_data => { 28 | this.env = env_data; 29 | this.httpClient 30 | .get('/config/' + env_data.env + '.json') 31 | .pipe( 32 | catchError((err: HttpErrorResponse) => { 33 | return throwError(err.error || 'Server error'); 34 | }) 35 | ) 36 | .subscribe(data => { 37 | this.config = data; 38 | resolve(true); 39 | }); 40 | }); 41 | }); 42 | } 43 | 44 | /** 45 | * Returns environment variable based on given key 46 | * 47 | * @param key 48 | */ 49 | getEnv(key: any) { 50 | return this.env[key]; 51 | } 52 | 53 | /** 54 | * Returns configuration value based on given key 55 | * 56 | * @param key 57 | */ 58 | get(key: any) { 59 | return this.config[key]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | /** Angular core dependencies */ 2 | import { NgModule } from '@angular/core'; 3 | import { Routes, RouterModule } from '@angular/router'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | children: [ 9 | { 10 | path: '', 11 | loadChildren: './features-modules/dashboard/dashboard.module#DashboardModule' 12 | }, 13 | { 14 | path: 'posts', 15 | loadChildren: './features-modules/posts/posts.module#PostsModule' 16 | } 17 | ] 18 | } 19 | ]; 20 | 21 | @NgModule({ 22 | imports: [RouterModule.forRoot(routes)], 23 | exports: [RouterModule] 24 | }) 25 | export class AppRoutingModule {} 26 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TranslateService } from '@ngx-translate/core'; 3 | import { ConfigService } from './app-config.service'; 4 | import { AuthService } from '@app/auth/auth.service'; 5 | 6 | @Component({ 7 | // tslint:disable-next-line:component-selector 8 | selector: 'body', 9 | template: ` 10 |
11 | 12 | 13 | 14 |
15 |
16 | 17 |
18 | 19 | ` 20 | }) 21 | export class AppComponent { 22 | constructor( 23 | private translate: TranslateService, 24 | private configService: ConfigService, 25 | public auth: AuthService 26 | ) { 27 | this.setupLanguage(); 28 | this.getNotificationOptions(); 29 | } 30 | /** 31 | * Sets up default language for the application. Uses browser default language. 32 | */ 33 | public setupLanguage(): void { 34 | const localization: any = this.configService.get('localization'); 35 | const languages: Array = localization.languages.map(lang => lang.code); 36 | const browserLang: string = this.translate.getBrowserLang(); 37 | 38 | this.translate.addLangs(languages); 39 | this.translate.setDefaultLang(localization.defaultLanguage); 40 | const selectedLang = 41 | languages.indexOf(browserLang) > -1 ? browserLang : localization.defaultLanguage; 42 | const selectedCulture = localization.languages.filter(lang => lang.code === selectedLang)[0] 43 | .culture; 44 | this.translate.use(selectedLang); 45 | } 46 | 47 | /** 48 | * Returns global notification options 49 | */ 50 | public getNotificationOptions(): any { 51 | return this.configService.get('notifications').options; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | /** Angular core modules */ 2 | import { NgModule, APP_INITIALIZER } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { HttpClientModule, HttpClient } from '@angular/common/http'; 5 | /** Routes */ 6 | import { AppRoutingModule } from './app-routing.module'; 7 | /** Modules */ 8 | import { AppComponent } from './app.component'; 9 | import { AuthModule } from '@app/auth/auth.module'; 10 | 11 | import { ComponentsModule } from '@shared/components'; 12 | import { ContainersModule } from '@shared/containers'; 13 | import { ErrorsModule } from '@app/shared/errors'; 14 | import { HttpServiceModule } from '@shared/async-services/http'; 15 | import { UtilityModule } from '@shared/utility'; 16 | /** guards */ 17 | import { AuthGuard } from '@shared/guards/auth.guard'; 18 | import { CanDeactivateGuard } from '@shared/guards/can-deactivate.guard'; 19 | /** Services */ 20 | import { ConfigService } from './app-config.service'; 21 | /** Third party modules */ 22 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 23 | import { SimpleNotificationsModule } from 'angular2-notifications'; 24 | import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; 25 | import { TranslateHttpLoader } from '@ngx-translate/http-loader'; 26 | import { JwtModule } from '@auth0/angular-jwt'; 27 | 28 | /** 29 | * Calling functions or calling new is not supported in metadata when using AoT. 30 | * The work-around is to introduce an exported function. 31 | * 32 | * The reason for this limitation is that the AoT compiler needs to generate the code that calls the factory 33 | * and there is no way to import a lambda from a module, you can only import an exported symbol. 34 | */ 35 | 36 | export function configServiceFactory(config: ConfigService) { 37 | return () => config.load(); 38 | } 39 | 40 | export function HttpLoaderFactory(http: HttpClient) { 41 | return new TranslateHttpLoader(http); 42 | } 43 | 44 | export function tokenGetter() { 45 | return localStorage.getItem('id_token'); 46 | } 47 | 48 | @NgModule({ 49 | declarations: [AppComponent], 50 | imports: [ 51 | /** Angular core dependencies */ 52 | BrowserModule, 53 | HttpClientModule, 54 | 55 | /** App custom dependencies */ 56 | AuthModule, 57 | AppRoutingModule, 58 | 59 | ComponentsModule, 60 | ContainersModule, 61 | ErrorsModule, 62 | HttpServiceModule.forRoot(), 63 | UtilityModule.forRoot(), 64 | 65 | /** Third party modules */ 66 | NgbModule.forRoot(), 67 | SimpleNotificationsModule.forRoot(), 68 | TranslateModule.forRoot({ 69 | loader: { 70 | provide: TranslateLoader, 71 | useFactory: HttpLoaderFactory, 72 | deps: [HttpClient] 73 | } 74 | }), 75 | JwtModule.forRoot({ 76 | config: { 77 | tokenGetter: tokenGetter, 78 | whitelistedDomains: ['localhost:3000'], 79 | blacklistedRoutes: [ 80 | '/config/env.json', 81 | '/config/development.json', 82 | '/config/production.json', 83 | '/assets/i18n/en.json', 84 | 'localhost:3000/auth/' 85 | ] 86 | } 87 | }) 88 | ], 89 | providers: [ 90 | AuthGuard, 91 | CanDeactivateGuard, 92 | ConfigService, 93 | { 94 | provide: APP_INITIALIZER, 95 | useFactory: configServiceFactory, 96 | deps: [ConfigService], 97 | multi: true 98 | } 99 | ], 100 | bootstrap: [AppComponent] 101 | }) 102 | export class AppModule {} 103 | -------------------------------------------------------------------------------- /src/app/auth/auth-api-client.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class AuthApiClientService { 5 | constructor() {} 6 | } 7 | -------------------------------------------------------------------------------- /src/app/auth/auth-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { LoginComponent } from './components/login/login.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: 'login', 9 | component: LoginComponent 10 | } 11 | ]; 12 | 13 | @NgModule({ 14 | imports: [RouterModule.forChild(routes)], 15 | exports: [RouterModule] 16 | }) 17 | export class AuthRoutingModule {} 18 | -------------------------------------------------------------------------------- /src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { ReactiveFormsModule } from '@angular/forms'; 5 | 6 | import { TranslateModule } from '@ngx-translate/core'; 7 | 8 | import { AuthRoutingModule } from './auth-routing.module'; 9 | import { LoginComponent } from './components/login/login.component'; 10 | 11 | import { AuthApiClientService } from './auth-api-client.service'; 12 | import { AuthService } from './auth.service'; 13 | 14 | @NgModule({ 15 | imports: [ 16 | CommonModule, 17 | AuthRoutingModule, 18 | BrowserAnimationsModule, 19 | ReactiveFormsModule, 20 | TranslateModule 21 | ], 22 | declarations: [LoginComponent], 23 | providers: [AuthApiClientService, AuthService] 24 | }) 25 | export class AuthModule {} 26 | -------------------------------------------------------------------------------- /src/app/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ValidationService } from '@shared/utility'; 3 | import { Router } from '@angular/router'; 4 | import { JwtHelperService } from '@auth0/angular-jwt'; 5 | const JwtHelper = new JwtHelperService(); 6 | 7 | @Injectable() 8 | export class AuthService { 9 | constructor(public validationService: ValidationService, private router: Router) {} 10 | 11 | public login(form: any, returnUrl?: string): void { 12 | const user = { status: 'loggedIn' }; 13 | localStorage.setItem('currentUser', JSON.stringify(user)); 14 | this.router.navigate([returnUrl || '/']); 15 | } 16 | 17 | public isLoggedIn() { 18 | const token = localStorage.getItem('id_token'); 19 | return token ? JwtHelper.isTokenExpired(token) : false; 20 | } 21 | 22 | get currentUser() { 23 | const token = localStorage.getItem('id_token'); 24 | return token ? JwtHelper.decodeToken(token) : null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/auth/components/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 25 |
26 | -------------------------------------------------------------------------------- /src/app/auth/components/login/login.component.scss: -------------------------------------------------------------------------------- 1 | .login-container { 2 | height: 100%; 3 | background-color: #f5f5f5; 4 | text-align: center; 5 | } 6 | 7 | .form-signin { 8 | width: 100%; 9 | max-width: 330px; 10 | padding: 15px; 11 | margin: auto; 12 | } 13 | 14 | .form-signin .checkbox { 15 | font-weight: 400; 16 | } 17 | 18 | .form-signin .form-control { 19 | position: relative; 20 | box-sizing: border-box; 21 | height: auto; 22 | padding: 10px; 23 | font-size: 16px; 24 | } 25 | 26 | .form-signin .form-control:focus { 27 | z-index: 2; 28 | } 29 | 30 | .form-signin input[type='email'] { 31 | margin-bottom: -1px; 32 | border-bottom-right-radius: 0; 33 | border-bottom-left-radius: 0; 34 | } 35 | 36 | .form-signin input[type='password'] { 37 | margin-bottom: 10px; 38 | border-top-left-radius: 0; 39 | border-top-right-radius: 0; 40 | } 41 | -------------------------------------------------------------------------------- /src/app/auth/components/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [LoginComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(LoginComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/auth/components/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | import { AbstractControl, FormGroup, FormBuilder, Validators } from '@angular/forms'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | 5 | import { TranslateService } from '@ngx-translate/core'; 6 | import { NotificationsService } from 'angular2-notifications'; 7 | import { moveIn } from '@shared/animations'; 8 | 9 | import { AuthService } from '../../auth.service'; 10 | 11 | @Component({ 12 | selector: 'app-login', 13 | templateUrl: './login.component.html', 14 | styleUrls: ['./login.component.scss'], 15 | animations: [moveIn()], 16 | // tslint:disable-next-line:use-host-property-decorator 17 | host: { '[@moveIn]': '' }, 18 | changeDetection: ChangeDetectionStrategy.OnPush 19 | }) 20 | export class LoginComponent implements OnInit { 21 | public submitted = false; 22 | public email: AbstractControl; 23 | public password: AbstractControl; 24 | public loginForm: FormGroup; 25 | 26 | constructor( 27 | private fb: FormBuilder, 28 | private route: ActivatedRoute, 29 | public authService: AuthService 30 | ) {} 31 | 32 | ngOnInit() { 33 | this.initLoginForm(); 34 | } 35 | 36 | /** 37 | * Builds a form instance (using FormBuilder) with corresponding validation rules 38 | */ 39 | public initLoginForm(): void { 40 | this.loginForm = this.fb.group({ 41 | email: ['', [Validators.required, this.authService.validationService.validateEmail]], 42 | password: ['', Validators.required] 43 | }); 44 | 45 | this.email = this.loginForm.controls['email']; 46 | this.password = this.loginForm.controls['password']; 47 | } 48 | 49 | /** 50 | * Handles form 'submit' event. Calls sandbox login function if form is valid. 51 | * 52 | * @param event 53 | * @param form 54 | */ 55 | public onSubmit(event: Event, form: any): void { 56 | event.stopPropagation(); 57 | this.submitted = true; 58 | 59 | if (this.loginForm.valid) { 60 | const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl'); 61 | this.authService.login(form, returnUrl); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/features-modules/dashboard/dashboard-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { DashboardComponent } from './dashboard.component'; 2 | import { NgModule } from '@angular/core'; 3 | import { Routes, RouterModule } from '@angular/router'; 4 | 5 | const routes: Routes = [{ path: '', component: DashboardComponent }]; 6 | 7 | @NgModule({ 8 | imports: [RouterModule.forChild(routes)], 9 | exports: [RouterModule] 10 | }) 11 | export class DashboardRoutingModule {} 12 | -------------------------------------------------------------------------------- /src/app/features-modules/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Scalable Angular Architecture Guide 4 |

5 |
A cohesive guide for building Angular applications for teams.
6 |
7 | -------------------------------------------------------------------------------- /src/app/features-modules/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | .dashboard-container { 2 | padding: 3rem 0; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/features-modules/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-dashboard', 5 | templateUrl: './dashboard.component.html', 6 | styleUrls: ['./dashboard.component.scss'] 7 | }) 8 | export class DashboardComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/features-modules/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { DashboardRoutingModule } from './dashboard-routing.module'; 5 | import { DashboardComponent } from './dashboard.component'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | DashboardRoutingModule 11 | ], 12 | declarations: [DashboardComponent] 13 | }) 14 | export class DashboardModule { } 15 | -------------------------------------------------------------------------------- /src/app/features-modules/posts/components/post-details/post-details.component.html: -------------------------------------------------------------------------------- 1 |

2 | post-details works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/features-modules/posts/components/post-details/post-details.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaherSghaier/scalable-angular-architecture/d300805d84ffc8c79b5b25e6a347920baf062ea8/src/app/features-modules/posts/components/post-details/post-details.component.scss -------------------------------------------------------------------------------- /src/app/features-modules/posts/components/post-details/post-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PostDetailsComponent } from './post-details.component'; 4 | 5 | describe('PostDetailsComponent', () => { 6 | let component: PostDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [PostDetailsComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(PostDetailsComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/features-modules/posts/components/post-details/post-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-post-details', 5 | templateUrl: './post-details.component.html', 6 | styleUrls: ['./post-details.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class PostDetailsComponent implements OnInit { 10 | constructor() {} 11 | 12 | ngOnInit() {} 13 | } 14 | -------------------------------------------------------------------------------- /src/app/features-modules/posts/components/post-form/post-form.component.html: -------------------------------------------------------------------------------- 1 |

Add new post

2 |
3 |
4 | 5 | 6 |
The title is required
7 |
8 |
9 | 10 | 11 |
The body is required
12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/app/features-modules/posts/components/post-form/post-form.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaherSghaier/scalable-angular-architecture/d300805d84ffc8c79b5b25e6a347920baf062ea8/src/app/features-modules/posts/components/post-form/post-form.component.scss -------------------------------------------------------------------------------- /src/app/features-modules/posts/components/post-form/post-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PostFormComponent } from './post-form.component'; 4 | 5 | describe('PostFormComponent', () => { 6 | let component: PostFormComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [PostFormComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(PostFormComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/features-modules/posts/components/post-form/post-form.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | Input, 5 | Output, 6 | EventEmitter, 7 | ChangeDetectionStrategy 8 | } from '@angular/core'; 9 | import { FormBuilder, FormGroup, Validators, AbstractControl } from '@angular/forms'; 10 | 11 | import { PostsApiClient } from '../../posts-api-client.service'; 12 | 13 | @Component({ 14 | selector: 'app-post-form', 15 | templateUrl: './post-form.component.html', 16 | styleUrls: ['./post-form.component.scss'], 17 | changeDetection: ChangeDetectionStrategy.OnPush 18 | }) 19 | export class PostFormComponent implements OnInit { 20 | public post; 21 | 22 | private postForm: FormGroup; 23 | private title: AbstractControl; 24 | private body: AbstractControl; 25 | private submitted = false; 26 | 27 | constructor(private fb: FormBuilder, private postsApiClient: PostsApiClient) {} 28 | 29 | ngOnInit() { 30 | this.buildForm(); 31 | this.initForm(); 32 | } 33 | 34 | buildForm() { 35 | this.postForm = this.fb.group({ 36 | title: ['', Validators.required], 37 | body: ['', Validators.required] 38 | }); 39 | 40 | this.title = this.postForm.controls['title']; 41 | this.body = this.postForm.controls['body']; 42 | } 43 | 44 | initForm() { 45 | if (this.post !== undefined) { 46 | this.postForm.patchValue(this.post); 47 | } 48 | } 49 | 50 | onSubmit(event: Event, form: any) { 51 | this.submitted = true; 52 | if (this.postForm.valid) { 53 | this.postsApiClient.create(form).subscribe(); 54 | } 55 | } 56 | 57 | cancel() { 58 | // this.cancelEvent.emit(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/app/features-modules/posts/components/posts-list/posts-list.component.html: -------------------------------------------------------------------------------- 1 |

List of articles 2 | ({{posts.length}}) 3 | 4 | Add new post 5 |

6 |
7 |

{{post?.title}}

8 |

{{post?.body | truncate : [100]}} 9 | read more 10 | 11 | 12 |

13 |

14 | Created by {{post?.author}} at {{post?.createdAt}} 15 |

16 |
17 | -------------------------------------------------------------------------------- /src/app/features-modules/posts/components/posts-list/posts-list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaherSghaier/scalable-angular-architecture/d300805d84ffc8c79b5b25e6a347920baf062ea8/src/app/features-modules/posts/components/posts-list/posts-list.component.scss -------------------------------------------------------------------------------- /src/app/features-modules/posts/components/posts-list/posts-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PostsListComponent } from './posts-list.component'; 4 | 5 | describe('PostsListComponent', () => { 6 | let component: PostsListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [PostsListComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(PostsListComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/features-modules/posts/components/posts-list/posts-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { PostsApiClient } from '../../posts-api-client.service'; 3 | 4 | @Component({ 5 | selector: 'app-posts-list', 6 | templateUrl: './posts-list.component.html', 7 | styleUrls: ['./posts-list.component.scss'] 8 | }) 9 | export class PostsListComponent implements OnInit { 10 | public posts: any = []; 11 | 12 | constructor(private postsApiClient: PostsApiClient) {} 13 | 14 | ngOnInit() { 15 | this.getPosts(); 16 | } 17 | 18 | public getPosts() { 19 | this.postsApiClient.getAll().subscribe(posts => (this.posts = posts)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/features-modules/posts/posts-api-client.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpEvent } from '@angular/common/http'; 3 | import { HttpResponseHandler } from '@app/shared/async-services/http'; 4 | 5 | import { DataService } from '@shared/async-services/http'; 6 | 7 | @Injectable() 8 | export class PostsApiClient extends DataService { 9 | constructor(httpClient: HttpClient, responseHandler: HttpResponseHandler) { 10 | super('/api/posts', httpClient, responseHandler); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/features-modules/posts/posts-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { AuthGuard } from '@shared/guards/auth.guard'; 5 | 6 | import { PostsListComponent } from './components/posts-list/posts-list.component'; 7 | import { PostFormComponent } from './components/post-form/post-form.component'; 8 | import { PostDetailsComponent } from './components/post-details/post-details.component'; 9 | 10 | const routes: Routes = [ 11 | { path: '', component: PostsListComponent, canActivate: [AuthGuard] }, 12 | { path: 'add', component: PostFormComponent, canActivate: [AuthGuard] }, 13 | { path: 'edit/:id', component: PostFormComponent, canActivate: [AuthGuard] }, 14 | { path: ':id', component: PostDetailsComponent, canActivate: [AuthGuard] } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forChild(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class PostsRoutingModule {} 22 | -------------------------------------------------------------------------------- /src/app/features-modules/posts/posts.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { PostsRoutingModule } from './posts-routing.module'; 6 | 7 | import { PipesModule } from '@shared/pipes'; 8 | 9 | import { PostsListComponent } from './components/posts-list/posts-list.component'; 10 | import { PostDetailsComponent } from './components/post-details/post-details.component'; 11 | import { PostFormComponent } from './components/post-form/post-form.component'; 12 | 13 | import { PostsApiClient } from './posts-api-client.service'; 14 | 15 | @NgModule({ 16 | imports: [CommonModule, PostsRoutingModule, ReactiveFormsModule, PipesModule], 17 | declarations: [PostsListComponent, PostDetailsComponent, PostFormComponent], 18 | providers: [PostsApiClient] 19 | }) 20 | export class PostsModule {} 21 | -------------------------------------------------------------------------------- /src/app/features-modules/posts/posts.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class PostsService { 5 | constructor() {} 6 | } 7 | -------------------------------------------------------------------------------- /src/app/shared/animations/fadeIn.animation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | animate, 3 | state, 4 | style, 5 | transition, 6 | trigger, 7 | AnimationTriggerMetadata 8 | } from '@angular/animations'; 9 | 10 | export const fadeInAnimation: AnimationTriggerMetadata = trigger('fadeInAnimation', [ 11 | state('true', style({ opacity: 1 })), 12 | state('false', style({ opacity: 0 })), 13 | transition('1 => 0', animate('100ms')), 14 | transition('0 => 1', animate('250ms')) 15 | ]); 16 | // trigger('fadeInAnimation', [ 17 | // transition('void => *', [ 18 | // style({opacity:0}), //style only for transition transition (after transiton it removes) 19 | // animate(100, style({opacity:1})) // the new state of the transition(after transiton it removes) 20 | // ]), 21 | // transition('* => void', [ 22 | // animate(100, style({opacity:0})) // the new state of the transition(after transiton it removes) 23 | // ]) 24 | // ]); 25 | -------------------------------------------------------------------------------- /src/app/shared/animations/fallIn.animation.ts: -------------------------------------------------------------------------------- 1 | import { trigger, state, animate, style, transition } from '@angular/animations'; 2 | 3 | export function fallIn() { 4 | return trigger('fallIn', [ 5 | transition(':enter', [ 6 | style({ opacity: '0', transform: 'translateY(40px)' }), 7 | animate('.4s .2s ease-in-out', style({ opacity: '1', transform: 'translateY(0)' })) 8 | ]), 9 | transition(':leave', [ 10 | style({ opacity: '1', transform: 'translateX(0)' }), 11 | animate('.3s ease-in-out', style({ opacity: '0', transform: 'translateX(-200px)' })) 12 | ]) 13 | ]); 14 | } 15 | -------------------------------------------------------------------------------- /src/app/shared/animations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fadeIn.animation'; 2 | export * from './slideInRight.animation'; 3 | export * from './moveIn.animation'; 4 | export * from './fallIn.animation'; 5 | export * from './moveInLeft.animation'; 6 | -------------------------------------------------------------------------------- /src/app/shared/animations/moveIn.animation.ts: -------------------------------------------------------------------------------- 1 | import { trigger, state, animate, style, transition } from '@angular/animations'; 2 | 3 | export function moveIn() { 4 | return trigger('moveIn', [ 5 | state('void', style({ position: 'fixed', width: '100%' })), 6 | state('*', style({ position: 'fixed', width: '100%' })), 7 | transition(':enter', [ 8 | style({ opacity: '0', transform: 'translateX(100px)' }), 9 | animate('.6s ease-in-out', style({ opacity: '1', transform: 'translateX(0)' })) 10 | ]), 11 | transition(':leave', [ 12 | style({ opacity: '1', transform: 'translateX(0)' }), 13 | animate('.3s ease-in-out', style({ opacity: '0', transform: 'translateX(-200px)' })) 14 | ]) 15 | ]); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/shared/animations/moveInLeft.animation.ts: -------------------------------------------------------------------------------- 1 | import { trigger, state, animate, style, transition } from '@angular/animations'; 2 | 3 | export function moveInLeft() { 4 | return trigger('moveInLeft', [ 5 | transition(':enter', [ 6 | style({ opacity: '0', transform: 'translateX(-100px)' }), 7 | animate('.6s .2s ease-in-out', style({ opacity: '1', transform: 'translateX(0)' })) 8 | ]) 9 | ]); 10 | } 11 | -------------------------------------------------------------------------------- /src/app/shared/animations/slideInRight.animation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | animate, 3 | state, 4 | style, 5 | transition, 6 | trigger, 7 | AnimationTriggerMetadata 8 | } from '@angular/animations'; 9 | 10 | // Component transition animations 11 | export const slideInRightAnimation: AnimationTriggerMetadata = trigger('slideInRightAnimation', [ 12 | state('in', style({ opacity: 1, transform: 'translateX(0)' })), 13 | transition('void => *', [ 14 | style({ 15 | opacity: 0, 16 | transform: 'translateX(100%)' 17 | }), 18 | animate('0.2s ease-in') 19 | ]), 20 | transition('* => void', [ 21 | animate( 22 | '0.2s 10 ease-out', 23 | style({ 24 | opacity: 0, 25 | transform: 'translateX(100%)' 26 | }) 27 | ) 28 | ]) 29 | ]); 30 | -------------------------------------------------------------------------------- /src/app/shared/async-services/http/data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpEvent } from '@angular/common/http'; 3 | import { Observable, throwError } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | 6 | import { HttpResponseHandler } from './http-response-handler.service'; 7 | 8 | @Injectable() 9 | export class DataService { 10 | constructor( 11 | protected url: string, 12 | protected httpClient: HttpClient, 13 | protected responseHandler: HttpResponseHandler 14 | ) {} 15 | 16 | getOneById(id) { 17 | return this.httpClient.get(this.url + '/' + id); 18 | } 19 | 20 | public getAll() { 21 | return this.httpClient 22 | .get(this.url) 23 | .pipe(catchError((err, source) => this.responseHandler.onCatch(err, source))); 24 | } 25 | 26 | public create(post: any) { 27 | return this.httpClient 28 | .post(`${this.url}`, JSON.stringify(post)) 29 | .pipe(catchError((err, source) => this.responseHandler.onCatch(err, source))); 30 | } 31 | 32 | public update(post: any) { 33 | return this.httpClient 34 | .patch(`${this.url}/${post.id}`, JSON.stringify(post)) 35 | .pipe(catchError((err, source) => this.responseHandler.onCatch(err, source))); 36 | } 37 | 38 | public delete(id: any) { 39 | return this.httpClient 40 | .delete(`${this.url}/${id}`) 41 | .pipe(catchError((err, source) => this.responseHandler.onCatch(err, source))); 42 | } 43 | /**private handleError(error: Response) { 44 | if (error.status === 400) { 45 | return throwError(new BadInput(error)); 46 | } 47 | if (error.status === 404) { 48 | return throwError(new NotFoundError(error)); 49 | } 50 | return throwError(new AppError(error)); 51 | }**/ 52 | } 53 | -------------------------------------------------------------------------------- /src/app/shared/async-services/http/http-response-handler.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { TranslateService } from '@ngx-translate/core'; 3 | import { NotificationsService } from 'angular2-notifications'; 4 | import { ConfigService } from '@app/app-config.service'; 5 | import { Router } from '@angular/router'; 6 | import { Observable, throwError } from 'rxjs'; 7 | 8 | @Injectable() 9 | export class HttpResponseHandler { 10 | constructor( 11 | private router: Router, 12 | private translateService: TranslateService, 13 | private notificationsService: NotificationsService, 14 | private configService: ConfigService 15 | ) {} 16 | 17 | /** 18 | * Global http error handler. 19 | * 20 | * @param error 21 | * @param source 22 | * @returns {ErrorObservable} 23 | */ 24 | public onCatch(response: any, source: Observable): Observable { 25 | switch (response.status) { 26 | case 400: 27 | this.handleBadRequest(response); 28 | break; 29 | 30 | case 401: 31 | this.handleUnauthorized(response); 32 | break; 33 | 34 | case 403: 35 | this.handleForbidden(); 36 | break; 37 | 38 | case 404: 39 | this.handleNotFound(response); 40 | break; 41 | 42 | case 500: 43 | this.handleServerError(); 44 | break; 45 | 46 | default: 47 | break; 48 | } 49 | 50 | return throwError(response); 51 | } 52 | 53 | /** 54 | * Shows notification errors when server response status is 401 55 | * 56 | * @param error 57 | */ 58 | private handleBadRequest(responseBody: any): void { 59 | if (responseBody._body) { 60 | try { 61 | const bodyParsed = responseBody.json(); 62 | this.handleErrorMessages(bodyParsed); 63 | } catch (error) { 64 | this.handleServerError(); 65 | } 66 | } else { 67 | this.handleServerError(); 68 | } 69 | } 70 | 71 | /** 72 | * Shows notification errors when server response status is 401 and redirects user to login page 73 | * 74 | * @param responseBody 75 | */ 76 | private handleUnauthorized(responseBody: any): void { 77 | // Read configuration in order to see if we need to display 401 notification message 78 | let unauthorizedEndpoints: Array = this.configService.get('notifications') 79 | .unauthorizedEndpoints; 80 | 81 | unauthorizedEndpoints = unauthorizedEndpoints.filter( 82 | endpoint => this.getRelativeUrl(responseBody.url) === endpoint 83 | ); 84 | this.router.navigate(['/login']); 85 | 86 | if (unauthorizedEndpoints.length) { 87 | this.notificationsService.info( 88 | 'Info', 89 | this.translateService.instant('ServerError401'), 90 | this.configService.get('notifications').options 91 | ); 92 | } 93 | } 94 | 95 | /** 96 | * Shows notification errors when server response status is 403 97 | */ 98 | private handleForbidden(): void { 99 | this.notificationsService.error( 100 | 'error', 101 | this.translateService.instant('ServerError403'), 102 | this.configService.get('notifications').options 103 | ); 104 | this.router.navigate(['/login']); 105 | } 106 | 107 | /** 108 | * Shows notification errors when server response status is 404 109 | * 110 | * @param responseBody 111 | */ 112 | private handleNotFound(responseBody: any): void { 113 | // Read configuration in order to see if we need to display 401 notification message 114 | let notFoundEndpoints: Array = this.configService.get('notifications') 115 | .notFoundEndpoints; 116 | notFoundEndpoints = notFoundEndpoints.filter( 117 | endpoint => this.getRelativeUrl(responseBody.url) === endpoint 118 | ); 119 | 120 | if (notFoundEndpoints.length) { 121 | const message = this.translateService.instant('ServerError404'), 122 | title = this.translateService.instant('ErrorNotificationTitle'); 123 | 124 | this.showNotificationError(title, message); 125 | } 126 | } 127 | 128 | /** 129 | * Shows notification errors when server response status is 500 130 | */ 131 | private handleServerError(): void { 132 | const message = this.translateService.instant('ServerError500'), 133 | title = this.translateService.instant('ErrorNotificationTitle'); 134 | 135 | this.showNotificationError(title, message); 136 | } 137 | 138 | /** 139 | * Parses server response and shows notification errors with translated messages 140 | * 141 | * @param response 142 | */ 143 | private handleErrorMessages(response: any): void { 144 | if (!response) { 145 | return; 146 | } 147 | 148 | for (const key of Object.keys(response)) { 149 | if (Array.isArray(response[key])) { 150 | response[key].forEach(value => 151 | this.showNotificationError('Error', this.getTranslatedValue(value)) 152 | ); 153 | } else { 154 | this.showNotificationError('Error', this.getTranslatedValue(response[key])); 155 | } 156 | } 157 | } 158 | 159 | /** 160 | * Extracts and returns translated value from server response 161 | * 162 | * @param value 163 | * @returns {string} 164 | */ 165 | private getTranslatedValue(value: string): string { 166 | if (value.indexOf('[') > -1) { 167 | const key = value.substring(value.lastIndexOf('[') + 1, value.lastIndexOf(']')); 168 | value = this.translateService.instant(key); 169 | } 170 | 171 | return value; 172 | } 173 | 174 | /** 175 | * Returns relative url from the absolute path 176 | * 177 | * @param responseBody 178 | * @returns {string} 179 | */ 180 | private getRelativeUrl(url: string): string { 181 | return url.toLowerCase().replace(/^(?:\/\/|[^\/]+)*\//, ''); 182 | } 183 | 184 | /** 185 | * Shows error notification with given title and message 186 | * 187 | * @param title 188 | * @param message 189 | */ 190 | private showNotificationError(title: string, message: string): void { 191 | this.notificationsService.error( 192 | title, 193 | message, 194 | this.configService.get('notifications').options 195 | ); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/app/shared/async-services/http/http.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | HttpInterceptor, 4 | HttpRequest, 5 | HttpResponse, 6 | HttpErrorResponse, 7 | HttpHandler, 8 | HttpEvent 9 | } from '@angular/common/http'; 10 | 11 | import { Observable, throwError } from 'rxjs'; 12 | import { tap, catchError } from 'rxjs/operators'; 13 | 14 | @Injectable() 15 | export class HttpResponseInterceptor implements HttpInterceptor { 16 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 17 | // add a custom header 18 | const customReq = request.clone({ 19 | headers: request.headers.set('Accept', 'application/json') 20 | }); 21 | 22 | // pass on the modified request object 23 | return next.handle(customReq).pipe( 24 | tap((ev: HttpEvent) => { 25 | if (ev instanceof HttpResponse) { 26 | console.log('processing response', ev); 27 | } 28 | }), 29 | catchError(response => { 30 | if (response instanceof HttpErrorResponse) { 31 | console.log('Processing http error', response); 32 | } 33 | return throwError(response); 34 | }) 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/shared/async-services/http/http.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule, ModuleWithProviders } from '@angular/core'; 3 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 4 | 5 | import { DataService } from './data.service'; 6 | import { HttpResponseHandler } from './http-response-handler.service'; 7 | import { HttpResponseInterceptor } from './http.interceptor'; 8 | 9 | @NgModule({ 10 | imports: [CommonModule, HttpClientModule] 11 | }) 12 | export class HttpServiceModule { 13 | static forRoot(): ModuleWithProviders { 14 | return { 15 | ngModule: HttpServiceModule, 16 | providers: [ 17 | DataService, 18 | HttpResponseHandler, 19 | { provide: HTTP_INTERCEPTORS, useClass: HttpResponseInterceptor, multi: true } 20 | ] 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/shared/async-services/http/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data.service'; 2 | export * from './http.module'; 3 | export * from './http-response-handler.service'; 4 | export * from './http.interceptor'; 5 | -------------------------------------------------------------------------------- /src/app/shared/components/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /src/app/shared/components/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | border-top: 1px solid #ccc; 3 | text-align: center; 4 | padding: 1rem 0; 5 | } 6 | 7 | .fa-heart { 8 | color: #d14; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/shared/components/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | templateUrl: './footer.component.html', 6 | styleUrls: ['./footer.component.scss'] 7 | }) 8 | export class FooterComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit() {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/components/header/header.component.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /src/app/shared/components/header/header.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaherSghaier/scalable-angular-architecture/d300805d84ffc8c79b5b25e6a347920baf062ea8/src/app/shared/components/header/header.component.scss -------------------------------------------------------------------------------- /src/app/shared/components/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-header', 5 | templateUrl: './header.component.html', 6 | styleUrls: ['./header.component.scss'] 7 | }) 8 | export class HeaderComponent implements OnInit { 9 | public isCollapsed = true; 10 | constructor() {} 11 | 12 | ngOnInit() {} 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/components/index.ts: -------------------------------------------------------------------------------- 1 | /** Angular core dependencies */ 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { RouterModule } from '@angular/router'; 5 | 6 | /** Third party modules */ 7 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 8 | import { TranslateModule } from '@ngx-translate/core'; 9 | 10 | /** Custom Components */ 11 | import { FooterComponent } from './footer/footer.component'; 12 | import { HeaderComponent } from './header/header.component'; 13 | import { LoadingPlaceholderComponent } from './loading-placeholder/loading-placeholder.component'; 14 | import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; 15 | import { SpinnerComponent } from './spinner/spinner.component'; 16 | 17 | /** Custom Components Registration*/ 18 | export const COMPONENTS = [ 19 | FooterComponent, 20 | HeaderComponent, 21 | LoadingPlaceholderComponent, 22 | PageNotFoundComponent, 23 | SpinnerComponent 24 | ]; 25 | 26 | @NgModule({ 27 | imports: [ 28 | /** Angular core dependencies */ 29 | CommonModule, 30 | RouterModule, 31 | /** Third party modules */ 32 | NgbModule, 33 | TranslateModule, 34 | ], 35 | declarations: COMPONENTS, 36 | exports: COMPONENTS 37 | }) 38 | export class ComponentsModule { } 39 | -------------------------------------------------------------------------------- /src/app/shared/components/loading-placeholder/loading-placeholder.component.scss: -------------------------------------------------------------------------------- 1 | .timeline-item { 2 | background: #fff; 3 | border-radius: 3px; 4 | padding: 12px; 5 | margin: 0 auto; 6 | min-height: 200px; 7 | } 8 | 9 | @keyframes placeHolderShimmer { 10 | 0% { 11 | background-position: -468px 0 12 | } 13 | 100% { 14 | background-position: 468px 0 15 | } 16 | } 17 | 18 | .animated-background { 19 | animation-duration: 1s; 20 | animation-fill-mode: forwards; 21 | animation-iteration-count: infinite; 22 | animation-name: placeHolderShimmer; 23 | animation-timing-function: linear; 24 | background: #f6f7f8; 25 | background: linear-gradient(to right, #eeeeee 8%, #dddddd 18%, #eeeeee 33%); 26 | background-size: 800px 104px; 27 | height: 180px; 28 | position: relative; 29 | } 30 | 31 | .background-masker { 32 | background: #fff; 33 | position: absolute; 34 | } 35 | 36 | /* Every thing below this is just positioning */ 37 | 38 | .background-masker.header-top, 39 | .background-masker.header-bottom, 40 | .background-masker.subheader-bottom { 41 | top: 0; 42 | left: 120px; 43 | right: 0; 44 | height: 10px; 45 | } 46 | 47 | .background-masker.header-left, 48 | .background-masker.subheader-left, 49 | .background-masker.header-right, 50 | .background-masker.subheader-right { 51 | top: 10px; 52 | left: 120px; 53 | height: 12px; 54 | width: 10px; 55 | } 56 | 57 | .background-masker.header-bottom { 58 | top: 22px; 59 | height: 12px; 60 | } 61 | 62 | .background-masker.subheader-left, 63 | .background-masker.subheader-right { 64 | top: 34px; 65 | height: 6px; 66 | } 67 | 68 | .background-masker.header-right, 69 | .background-masker.subheader-right { 70 | width: auto; 71 | left: 50%; 72 | right: 0; 73 | } 74 | 75 | .background-masker.subheader-right { 76 | left: 30%; 77 | } 78 | 79 | .background-masker.subheader-bottom { 80 | top: 40px; 81 | height: 60px; 82 | } 83 | 84 | .background-masker.content-top, 85 | .background-masker.content-second-line, 86 | .background-masker.content-third-line, 87 | .background-masker.content-second-end, 88 | .background-masker.content-third-end, 89 | .background-masker.content-first-end { 90 | top: 100px; 91 | left: 0; 92 | right: 0; 93 | height: 6px; 94 | } 95 | 96 | .background-masker.content-top { 97 | height: 34px; 98 | } 99 | 100 | .background-masker.content-first-end, 101 | .background-masker.content-second-end, 102 | .background-masker.content-third-end { 103 | width: auto; 104 | left: 70%; 105 | right: 0; 106 | top: 134px; 107 | height: 12px; 108 | } 109 | 110 | .background-masker.content-second-line { 111 | top: 146px; 112 | } 113 | 114 | .background-masker.content-second-end { 115 | left: 80%; 116 | top: 152px; 117 | } 118 | 119 | .background-masker.content-third-line { 120 | top: 162px; 121 | } 122 | 123 | .background-masker.content-third-end { 124 | left: 60%; 125 | top: 168px; 126 | } 127 | -------------------------------------------------------------------------------- /src/app/shared/components/loading-placeholder/loading-placeholder.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Output, 4 | Input, 5 | EventEmitter, 6 | ChangeDetectionStrategy, 7 | ElementRef 8 | } from '@angular/core'; 9 | 10 | @Component({ 11 | // tslint:disable-next-line:component-selector 12 | selector: 'loading-placeholder', 13 | template: ` 14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | `, 32 | styleUrls: ['./loading-placeholder.component.scss'], 33 | changeDetection: ChangeDetectionStrategy.OnPush 34 | }) 35 | export class LoadingPlaceholderComponent { 36 | @Input() isRunning: boolean; 37 | 38 | constructor() {} 39 | } 40 | -------------------------------------------------------------------------------- /src/app/shared/components/page-not-found/page-not-found.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../scss/_custom-variables.scss"; 2 | .page-not-found { 3 | position: fixed; 4 | top: 0; 5 | right: 0; 6 | bottom: 0; 7 | left: 0; 8 | background-color: $color-theme-violet; 9 | background: $color-theme-violet url(/assets/images/stars.png) center center; 10 | background-size: 100%; 11 | .page-not-found-content { 12 | position: absolute; 13 | width: 550px; 14 | text-align: center; 15 | left: 50%; 16 | margin-left: -275px; 17 | top: 50%; 18 | margin-top: -230px; 19 | } 20 | img { 21 | width: 22%; 22 | } 23 | h1 { 24 | color: $color-white; 25 | font-weight: 100; 26 | font-size: 45px; 27 | letter-spacing: 1px; 28 | margin-bottom: 10px; 29 | } 30 | h3 { 31 | color: $color-white; 32 | font-weight: 100; 33 | font-size: 22px; 34 | margin: 0; 35 | } 36 | button { 37 | text-transform: uppercase; 38 | background: none; 39 | color: $color-white; 40 | border: 1px solid $color-white; 41 | border-radius: 5px; 42 | padding: 8px 30px; 43 | margin-top: 30px; 44 | outline: none; 45 | &:hover { 46 | background-color: $color-theme-darkviolet; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/shared/components/page-not-found/page-not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core'; 2 | import { Location } from '@angular/common'; 3 | 4 | @Component({ 5 | // tslint:disable-next-line:component-selector 6 | selector: 'page-not-found', 7 | template: ` 8 |
9 |
10 | 11 |

{{ 'PageNotFound.Title' | translate }}

12 |

{{ 'PageNotFound.Subtitle' | translate }}

13 | 14 |
15 |
16 | `, 17 | styleUrls: ['./page-not-found.component.scss'], 18 | changeDetection: ChangeDetectionStrategy.OnPush 19 | }) 20 | export class PageNotFoundComponent { 21 | constructor(private location: Location) {} 22 | 23 | public goBack() { 24 | this.location.back(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/shared/components/spinner/spinner.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../scss/_custom-variables.scss"; 2 | .spinner { 3 | position: absolute; 4 | top: 0; 5 | right: 0; 6 | bottom: 0; 7 | left: 0; 8 | border-radius: 8px; 9 | } 10 | 11 | .spinner-inner-wrapper { 12 | width: 40px; 13 | height: 40px; 14 | position: absolute; 15 | top: 50%; 16 | left: 50%; 17 | margin: -20px; 18 | &.spinner-small { 19 | width: 20px; 20 | height: 20px; 21 | margin: -10px; 22 | } 23 | } 24 | 25 | .double-bounce1, 26 | .double-bounce2 { 27 | width: 100%; 28 | height: 100%; 29 | border-radius: 50%; 30 | background-color: #46a9d4; 31 | opacity: 0.7; 32 | position: absolute; 33 | top: 0; 34 | left: 0; 35 | -webkit-animation: sk-bounce 2.0s infinite ease-in-out; 36 | animation: sk-bounce 2.0s infinite ease-in-out; 37 | } 38 | 39 | .double-bounce2 { 40 | -webkit-animation-delay: -1.0s; 41 | animation-delay: -1.0s; 42 | } 43 | 44 | @-webkit-keyframes sk-bounce { 45 | 0%, 46 | 100% { 47 | -webkit-transform: scale(0.0) 48 | } 49 | 50% { 50 | -webkit-transform: scale(1.0) 51 | } 52 | } 53 | 54 | @keyframes sk-bounce { 55 | 0%, 56 | 100% { 57 | transform: scale(0.0); 58 | -webkit-transform: scale(0.0); 59 | } 60 | 50% { 61 | transform: scale(1.0); 62 | -webkit-transform: scale(1.0); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/shared/components/spinner/spinner.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | // tslint:disable-next-line:component-selector 5 | selector: 'spinner', 6 | template: ` 7 |
8 |
9 |
10 |
11 |
12 |
13 | `, 14 | styleUrls: ['./spinner.component.scss'] 15 | }) 16 | export class SpinnerComponent { 17 | // private currentTimeout: any; 18 | // private isDelayedRunning: boolean = false; 19 | 20 | // @Input() 21 | // public delay: number = 300; 22 | 23 | @Input() isRunning: boolean; 24 | @Input() isSmall: string; 25 | // public set isRunning(value: boolean) { 26 | // if (!value) { 27 | // this.cancelTimeout(); 28 | // this.isDelayedRunning = false; 29 | // return; 30 | // } 31 | 32 | // if (this.currentTimeout) { 33 | // return; 34 | // } 35 | 36 | // this.currentTimeout = setTimeout(() => { 37 | // this.isDelayedRunning = value; 38 | // this.cancelTimeout(); 39 | // }, this.delay); 40 | // } 41 | 42 | // private cancelTimeout(): void { 43 | // clearTimeout(this.currentTimeout); 44 | // this.currentTimeout = undefined; 45 | // } 46 | 47 | // ngOnDestroy(): any { 48 | // this.cancelTimeout(); 49 | // } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/shared/containers/index.ts: -------------------------------------------------------------------------------- 1 | /** Angular core dependencies */ 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | 5 | import { ComponentsModule } from '@shared/components'; 6 | 7 | /** Custom Containers */ 8 | import { LayoutComponent } from './layout/layout.component'; 9 | 10 | /** Custom Containers Registration */ 11 | const CONTAINERS = [LayoutComponent]; 12 | 13 | @NgModule({ 14 | imports: [ 15 | /** Angular core dependencies */ 16 | CommonModule, 17 | ComponentsModule 18 | ], 19 | declarations: CONTAINERS, 20 | exports: CONTAINERS 21 | }) 22 | export class ContainersModule { } 23 | -------------------------------------------------------------------------------- /src/app/shared/containers/layout/layout.component.scss: -------------------------------------------------------------------------------- 1 | .layout-content { 2 | height: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/shared/containers/layout/layout.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-layout', 5 | template: ` 6 | 7 |
8 | 9 |
10 | 11 | `, 12 | styleUrls: ['./layout.component.scss'] 13 | }) 14 | export class LayoutComponent implements OnInit { 15 | constructor() {} 16 | 17 | ngOnInit() {} 18 | } 19 | -------------------------------------------------------------------------------- /src/app/shared/errors/components/errors.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

ERROR {{ data?.error}}

5 |
Not found :(
6 | 7 |
Go Home
8 |
9 |
10 | 11 |
12 |

ERROR {{ routeParams?.status }}

13 |
14 |

{{ routeParams?.message }}

15 |
16 |

Error in {{ routeParams?.url | uppercase }} page, sorry {{ routeParams?.user }} :(

17 |

This error has been reported to the Administrator with the ID: 18 |
{{ routeParams?.id}}

19 | 20 |
Go Back to {{routeParams?.url}}
21 |
22 | 23 |
Go Back to home
24 |
25 |
26 | 27 |
28 |

Unknown error, sorry :(

29 | 30 |
Go Home
31 |
32 |
33 |
34 | 35 |
36 |

Error sent to the server

37 |
38 |     {{ this.routeParams | json }}
39 |   
40 |
41 | -------------------------------------------------------------------------------- /src/app/shared/errors/components/errors.component.scss: -------------------------------------------------------------------------------- 1 | h1, 2 | h3, 3 | h4, 4 | h5 { 5 | margin-bottom: 0; 6 | margin-top: 10px; 7 | } 8 | 9 | .error-container { 10 | // width: 220px; 11 | height: 100%; 12 | margin: 0 auto; 13 | text-align: center; 14 | overflow-wrap: break-word; 15 | } 16 | 17 | .pre-container { 18 | width: 400px; 19 | padding: 15px; 20 | max-width: 90%; 21 | margin: 0 auto; 22 | background-color: lightgrey; 23 | box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12); 24 | } 25 | 26 | pre { 27 | overflow: scroll; 28 | } 29 | -------------------------------------------------------------------------------- /src/app/shared/errors/components/errors.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'app-errors', 7 | templateUrl: './errors.component.html', 8 | styleUrls: ['./errors.component.scss'] 9 | }) 10 | export class ErrorsComponent implements OnInit { 11 | routeParams; 12 | data; 13 | 14 | constructor(private activatedRoute: ActivatedRoute) {} 15 | 16 | ngOnInit() { 17 | this.routeParams = this.activatedRoute.snapshot.queryParams; 18 | this.data = this.activatedRoute.snapshot.data; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/shared/errors/errors-handler.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler, Injectable, Injector } from '@angular/core'; 2 | import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common'; 3 | import { HttpErrorResponse } from '@angular/common/http'; 4 | import { Router } from '@angular/router'; 5 | 6 | import * as StackTraceParser from 'error-stack-parser'; 7 | import { NotificationsService } from 'angular2-notifications'; 8 | 9 | import { ErrorsService } from './errors.service'; 10 | 11 | @Injectable() 12 | export class ErrorsHandler implements ErrorHandler { 13 | constructor(private injector: Injector) {} 14 | 15 | handleError(error: Error | HttpErrorResponse) { 16 | const notificationService = this.injector.get(NotificationsService); 17 | const errorsService = this.injector.get(ErrorsService); 18 | const router = this.injector.get(Router); 19 | 20 | if (error instanceof HttpErrorResponse) { 21 | // Server error happened 22 | if (!navigator.onLine) { 23 | // No Internet connection 24 | return notificationService.warn('No Internet Connection'); 25 | } 26 | // Http Error 27 | // Send the error to the server 28 | errorsService.log(error).subscribe(); 29 | // Show notification to the user 30 | return notificationService.error(`${error.status} - ${error.message}`); 31 | } else { 32 | // Client Error Happend 33 | // Client Error Happend 34 | // Send the error to the server and then 35 | // redirect the user to the page with all the info 36 | errorsService.log(error).subscribe(errorWithContextInfo => { 37 | router.navigate(['/error'], { queryParams: errorWithContextInfo }); 38 | }); 39 | } 40 | // Log the error anyway 41 | console.error(error); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/shared/errors/errors-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { ErrorsComponent } from './components/errors.component'; 5 | 6 | const routes: Routes = [ 7 | { path: 'error', component: ErrorsComponent }, 8 | { path: '**', component: ErrorsComponent, data: { error: 404 } } 9 | ]; 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule] 14 | }) 15 | export class ErrorsRoutingModule {} 16 | -------------------------------------------------------------------------------- /src/app/shared/errors/errors.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ErrorHandler } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 4 | 5 | import { ErrorsRoutingModule } from './errors-routing.module'; 6 | 7 | import { ErrorsHandler } from './errors-handler'; 8 | import { ServerErrorsInterceptor } from './server-errors.interceptor'; 9 | import { ErrorsService } from './errors.service'; 10 | 11 | import { ErrorsComponent } from './components/errors.component'; 12 | 13 | @NgModule({ 14 | imports: [CommonModule, ErrorsRoutingModule], 15 | declarations: [ErrorsComponent], 16 | providers: [ 17 | ErrorsService, 18 | { 19 | provide: ErrorHandler, 20 | useClass: ErrorsHandler 21 | }, 22 | { 23 | provide: HTTP_INTERCEPTORS, 24 | useClass: ServerErrorsInterceptor, 25 | multi: true 26 | } 27 | ] 28 | }) 29 | export class ErrorsModule {} 30 | -------------------------------------------------------------------------------- /src/app/shared/errors/errors.service.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler, Injectable, Injector } from '@angular/core'; 2 | import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common'; 3 | import { HttpErrorResponse } from '@angular/common/http'; 4 | import { Router, Event, NavigationError } from '@angular/router'; 5 | 6 | import { Observable, of } from 'rxjs'; 7 | 8 | import * as StackTraceParser from 'error-stack-parser'; 9 | 10 | @Injectable() 11 | export class ErrorsService { 12 | constructor(private injector: Injector, private router: Router) { 13 | // Subscribe to the NavigationError 14 | this.router.events.subscribe((event: Event) => { 15 | // Redirect to the ErrorComponent 16 | if (event instanceof NavigationError) { 17 | if (!navigator.onLine) { 18 | return; 19 | } 20 | // Redirect to the ErrorComponent 21 | this.log(event.error).subscribe(errorWithContext => { 22 | this.router.navigate(['/error'], { queryParams: errorWithContext }); 23 | }); 24 | } 25 | }); 26 | } 27 | 28 | log(error) { 29 | // Log the error to the console 30 | console.error(error); 31 | // Send error to server 32 | const errorToSend = this.addContextInfo(error); 33 | return FakeHttpService.post(errorToSend); 34 | } 35 | 36 | addContextInfo(error) { 37 | // You can include context details here (usually coming from other services: UserService...) 38 | const name = error.name || null; 39 | const appId = 'shthppnsApp'; 40 | const user = 'ShthppnsUser'; 41 | const time = new Date().getTime(); 42 | const id = `${appId}-${user}-${time}`; 43 | const location = this.injector.get(LocationStrategy as any); 44 | const url = location instanceof PathLocationStrategy ? location.path() : ''; 45 | const status = error.status || null; 46 | const message = error.message || error.toString(); 47 | const stack = error instanceof HttpErrorResponse ? null : StackTraceParser.parse(error); 48 | 49 | const errorWithContext = { name, appId, user, time, id, url, status, message, stack }; 50 | return errorWithContext; 51 | } 52 | } 53 | 54 | class FakeHttpService { 55 | static post(error): Observable { 56 | console.log('Error sent to the server: ', error); 57 | return of(error); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/shared/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors.module'; 2 | export * from './errors-handler'; 3 | export * from './components/errors.component'; 4 | -------------------------------------------------------------------------------- /src/app/shared/errors/server-errors.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | HttpRequest, 4 | HttpHandler, 5 | HttpEvent, 6 | HttpInterceptor, 7 | HttpErrorResponse 8 | } from '@angular/common/http'; 9 | import { Router } from '@angular/router'; 10 | 11 | import { Observable } from 'rxjs'; 12 | import { retry } from 'rxjs/operators'; 13 | 14 | @Injectable() 15 | export class ServerErrorsInterceptor implements HttpInterceptor { 16 | constructor(private router: Router) {} 17 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 18 | return next.handle(request).pipe(retry(5)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/shared/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Injectable() 6 | export class AuthGuard implements CanActivate { 7 | constructor(private router: Router) {} 8 | 9 | canActivate( 10 | next: ActivatedRouteSnapshot, 11 | state: RouterStateSnapshot 12 | ): Observable | Promise | boolean { 13 | return this.checkLogin(state.url); 14 | } 15 | 16 | checkLogin(url: string): boolean { 17 | const currentUser = JSON.parse(localStorage.getItem('currentUser')); 18 | if (currentUser) { 19 | return true; 20 | } 21 | 22 | // Navigate to the login page with extras 23 | this.router.navigate(['/login'], { queryParams: { returnUrl: url } }); 24 | return false; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/shared/guards/can-deactivate.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanDeactivate } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | export interface CanComponentDeactivate { 6 | canDeactivate: () => Observable | Promise | boolean; 7 | } 8 | 9 | @Injectable() 10 | export class CanDeactivateGuard implements CanDeactivate { 11 | canDeactivate(component: CanComponentDeactivate) { 12 | return component.canDeactivate ? component.canDeactivate() : true; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/shared/models/auth/login.model.ts: -------------------------------------------------------------------------------- 1 | export class LoginForm { 2 | public email: string; 3 | public password: string; 4 | 5 | constructor(loginForm: any) { 6 | this.email = loginForm.email || ''; 7 | this.password = loginForm.password || ''; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/shared/models/auth/register.model.ts: -------------------------------------------------------------------------------- 1 | export class RegisterForm { 2 | public email: string; 3 | public password: string; 4 | public confirmPassword: string; 5 | 6 | constructor(registerForm: any) { 7 | this.email = registerForm.email || ''; 8 | this.password = registerForm.password || ''; 9 | this.confirmPassword = registerForm.confirmPassword || ''; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shared/models/auth/user.model.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | public email: string; 3 | public isLoggedIn: boolean; 4 | 5 | constructor(user?: any) { 6 | this.email = user ? user.email : ''; 7 | this.isLoggedIn = this.email ? true : false; 8 | } 9 | 10 | /** 11 | * Saves user into local storage 12 | * 13 | * @param user 14 | */ 15 | public save(): void { 16 | localStorage.setItem('currentUser', JSON.stringify(this)); 17 | } 18 | 19 | /** 20 | * Saves user into local storage 21 | */ 22 | public remove(): void { 23 | localStorage.setItem('currentUser', null); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/shared/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth/user.model'; 2 | export * from './auth/login.model'; 3 | export * from './auth/register.model'; 4 | -------------------------------------------------------------------------------- /src/app/shared/pipes/index.ts: -------------------------------------------------------------------------------- 1 | /** Angular core dependencies */ 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { NgPipesModule } from 'ngx-pipes'; 5 | 6 | /** Custom Pipes */ 7 | import { SanitizeHtmlPipe } from './sanitize-html.pipe'; 8 | import { TruncatePipe } from './truncate.pipe'; 9 | 10 | /** Custom Pipes Registration */ 11 | const PIPES = [SanitizeHtmlPipe, TruncatePipe]; 12 | const PIPES_MODULES = [NgPipesModule]; 13 | 14 | @NgModule({ 15 | declarations: PIPES, 16 | imports: PIPES_MODULES, 17 | exports: [...PIPES, PIPES_MODULES] 18 | }) 19 | export class PipesModule {} 20 | -------------------------------------------------------------------------------- /src/app/shared/pipes/sanitize-html.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'sanitizeHtml' 5 | }) 6 | export class SanitizeHtmlPipe implements PipeTransform { 7 | transform(value: any, args?: any): any { 8 | return null; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/shared/pipes/truncate.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'truncate' 5 | }) 6 | export class TruncatePipe implements PipeTransform { 7 | transform(value: string, args: string[]): string { 8 | const limit = args.length > 0 ? parseInt(args[0], 10) : 20; 9 | const trail = args.length > 1 ? args[1] : '...'; 10 | return value.length > limit ? value.substring(0, limit) + trail : value; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/services/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaherSghaier/scalable-angular-architecture/d300805d84ffc8c79b5b25e6a347920baf062ea8/src/app/shared/services/.gitkeep -------------------------------------------------------------------------------- /src/app/shared/utility/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utility.module'; 2 | export * from './utility.service'; 3 | export * from './utilityHelpers'; 4 | export * from './validation.service'; 5 | -------------------------------------------------------------------------------- /src/app/shared/utility/utility.module.ts: -------------------------------------------------------------------------------- 1 | /** Angular core modules */ 2 | import { NgModule, ModuleWithProviders } from '@angular/core'; 3 | /** Custom Utilities Services */ 4 | import { ValidationService } from './validation.service'; 5 | import { UtilityService } from './utility.service'; 6 | 7 | @NgModule() 8 | export class UtilityModule { 9 | static forRoot(): ModuleWithProviders { 10 | return { 11 | ngModule: UtilityModule, 12 | providers: [UtilityService, ValidationService] 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/shared/utility/utility.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { TranslateService } from '@ngx-translate/core'; 3 | import { NotificationsService } from 'angular2-notifications'; 4 | import { ConfigService } from '@app/app-config.service'; 5 | import { Observable } from 'rxjs'; 6 | 7 | @Injectable() 8 | export class UtilityService { 9 | constructor( 10 | private translateService: TranslateService, 11 | private notificationService: NotificationsService, 12 | private configService: ConfigService 13 | ) {} 14 | 15 | /** 16 | * Translates given message code and title code and displays corresponding notification 17 | * 18 | * @param messageTranslationCode 19 | * @param type 20 | * @param titleTranslationCode 21 | */ 22 | public displayNotification( 23 | messageTranslationCode: string, 24 | type: string = 'info', 25 | titleTranslationCode?: string 26 | ) { 27 | const message: string = this.translateService.instant(messageTranslationCode); 28 | let title: string = titleTranslationCode 29 | ? this.translateService.instant(titleTranslationCode) 30 | : null; 31 | 32 | switch (type) { 33 | case 'error': 34 | title = this.translateService.instant('ErrorNotificationTitle'); 35 | break; 36 | 37 | case 'success': 38 | title = this.translateService.instant('SuccessNotificationTitle'); 39 | break; 40 | 41 | case 'alert': 42 | title = this.translateService.instant('WarningNotificationTitle'); 43 | break; 44 | 45 | default: 46 | title = this.translateService.instant('InfoNotificationTitle'); 47 | break; 48 | } 49 | 50 | this.notificationService[type](title, message, this.configService.get('notifications').options); 51 | } 52 | 53 | /** 54 | * Translates lookup names by looking into lookup code 55 | * 56 | * @param data 57 | */ 58 | public translateLookupData(data: Array): Array { 59 | // Translate quantity stock adjustment reasons 60 | return data.map(lookup => { 61 | lookup.name = lookup.code 62 | ? this.translateService.instant('Lookups')[lookup.code] 63 | : lookup.name; 64 | return lookup; 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/shared/utility/utilityHelpers.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | 3 | const typeCache: { [label: string]: boolean } = {}; 4 | 5 | type Predicate = (oldValues: Array, newValues: Array) => boolean; 6 | 7 | /** 8 | * This function coerces a string into a string literal type. 9 | * Using tagged union types in TypeScript 2.0, this enables 10 | * powerful typechecking of our reducers. 11 | * 12 | * Since every action label passes through this function it 13 | * is a good place to ensure all of our action labels are unique. 14 | * 15 | * @param label 16 | */ 17 | export function type(label: T | ''): T { 18 | if (typeCache[label]) { 19 | throw new Error(`Action type "${label}" is not unqiue"`); 20 | } 21 | 22 | typeCache[label] = true; 23 | 24 | return label; 25 | } 26 | 27 | /** 28 | * Runs through every condition, compares new and old values and returns true/false depends on condition state. 29 | * This is used to distinct if two observable values have changed. 30 | * 31 | * @param oldValues 32 | * @param newValues 33 | * @param conditions 34 | */ 35 | export function distinctChanges( 36 | oldValues: Array, 37 | newValues: Array, 38 | conditions: Predicate[] 39 | ): boolean { 40 | if (conditions.every(cond => cond(oldValues, newValues))) { 41 | return false; 42 | } 43 | return true; 44 | } 45 | 46 | /** 47 | * Returns true if the given value is type of Object 48 | * 49 | * @param val 50 | */ 51 | export function isObject(val: any) { 52 | if (val === null) { 53 | return false; 54 | } 55 | 56 | return typeof val === 'function' || typeof val === 'object'; 57 | } 58 | 59 | /** 60 | * Capitalizes the first character in given string 61 | * 62 | * @param s 63 | */ 64 | export function capitalize(s: string) { 65 | if (!s || typeof s !== 'string') { 66 | return s; 67 | } 68 | return s && s[0].toUpperCase() + s.slice(1); 69 | } 70 | 71 | /** 72 | * Uncapitalizes the first character in given string 73 | * 74 | * @param s 75 | */ 76 | export function uncapitalize(s: string) { 77 | if (!s || typeof s !== 'string') { 78 | return s; 79 | } 80 | return s && s[0].toLowerCase() + s.slice(1); 81 | } 82 | 83 | /** 84 | * Flattens multi dimensional object into one level deep 85 | * 86 | * @param obj 87 | * @param preservePath 88 | */ 89 | export function flattenObject(ob: any, preservePath: boolean = false): any { 90 | const toReturn = {}; 91 | 92 | for (const i in ob) { 93 | if (!ob.hasOwnProperty(i)) { 94 | continue; 95 | } 96 | 97 | if (typeof ob[i] === 'object') { 98 | const flatObject = flattenObject(ob[i], preservePath); 99 | for (const x in flatObject) { 100 | if (!flatObject.hasOwnProperty(x)) { 101 | continue; 102 | } 103 | 104 | const path = preservePath ? i + '.' + x : x; 105 | 106 | toReturn[path] = flatObject[x]; 107 | } 108 | } else { 109 | toReturn[i] = ob[i]; 110 | } 111 | } 112 | 113 | return toReturn; 114 | } 115 | 116 | /** 117 | * Returns formated date based on given culture 118 | * 119 | * @param dateString 120 | * @param culture 121 | */ 122 | export function localeDateString(dateString: string, culture: string = 'en-EN'): string { 123 | const date = new Date(dateString); 124 | return date.toLocaleDateString(culture); 125 | } 126 | -------------------------------------------------------------------------------- /src/app/shared/utility/validation.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { FormControl, FormGroup } from '@angular/forms'; 3 | @Injectable() 4 | export class ValidationService { 5 | /** 6 | * Validates email address 7 | * 8 | * @param formControl 9 | */ 10 | public validateEmail(formControl: FormControl): { [error: string]: any } { 11 | // tslint:disable-next-line:max-line-length 12 | const EMAIL_REGEXP = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 13 | return EMAIL_REGEXP.test(formControl.value) ? null : { validateEmail: { valid: false } }; 14 | } 15 | 16 | /** 17 | * Validates required numeric values 18 | * 19 | * @param formControl 20 | */ 21 | public numericRequired(formControl: FormControl): { [error: string]: any } { 22 | return formControl.value && formControl.value > 0 23 | ? null 24 | : { numericRequired: { valid: false } }; 25 | } 26 | 27 | /** 28 | * Validates matching string values 29 | * 30 | * @param controlKey 31 | * @param matchingControlKey 32 | */ 33 | public matchingPasswords( 34 | controlKey: string, 35 | matchingControlKey: string 36 | ): { [error: string]: any } { 37 | return (group: FormGroup): { [key: string]: any } => { 38 | if (group.controls[controlKey].value !== group.controls[matchingControlKey].value) { 39 | return { mismatch: { valid: false } }; 40 | } 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaherSghaier/scalable-angular-architecture/d300805d84ffc8c79b5b25e6a347920baf062ea8/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/images/Martian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaherSghaier/scalable-angular-architecture/d300805d84ffc8c79b5b25e6a347920baf062ea8/src/assets/images/Martian.png -------------------------------------------------------------------------------- /src/assets/images/Stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaherSghaier/scalable-angular-architecture/d300805d84ffc8c79b5b25e6a347920baf062ea8/src/assets/images/Stars.png -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/images/users/user.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaherSghaier/scalable-angular-architecture/d300805d84ffc8c79b5b25e6a347920baf062ea8/src/assets/images/users/user.jpg -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaherSghaier/scalable-angular-architecture/d300805d84ffc8c79b5b25e6a347920baf062ea8/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AngularArchitecture 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Web Animations `@angular/platform-browser/animations` 51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 53 | **/ 54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 55 | 56 | /** 57 | * By default, zone.js will patch all possible macroTask and DomEvents 58 | * user can disable parts of macroTask/DomEvents patch by setting following flags 59 | */ 60 | 61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 64 | 65 | /* 66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 68 | */ 69 | // (window as any).__Zone_enable_cross_context_check = true; 70 | 71 | /*************************************************************************************************** 72 | * Zone JS is required by default for Angular itself. 73 | */ 74 | import 'zone.js/dist/zone'; // Included with Angular CLI. 75 | 76 | 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | -------------------------------------------------------------------------------- /src/scss/_custom-styles.scss: -------------------------------------------------------------------------------- 1 | // Add additional styles here. For example, overwrite certain styles or add new components. 2 | // Tip: You can use bootstrap's powerful mixins here! 3 | 4 | // .alert-myalert { 5 | // @include alert-variant(#60667d, #1d1d1d, #f4fdff); 6 | // } 7 | 8 | // @each $color, $value in $theme-colors { 9 | // .alert-#{$color} { 10 | // box-shadow: 3px 3px theme-color-level($color, -3); 11 | // } 12 | // } 13 | -------------------------------------------------------------------------------- /src/scss/_custom-variables.scss: -------------------------------------------------------------------------------- 1 | // Overwrite Bootstrap's variables here 2 | // You can find them in node_modules/bootstrap/scss/_variables.scss 3 | // Copy the variables you need into this file, don't modify files under node_modules/ 4 | 5 | // Some example variables that you can uncomment: 6 | 7 | // Enabling shadows and gradients 8 | //$enable-shadows: true; 9 | //$enable-gradients: true; 10 | 11 | // Changing the body background and text 12 | //$body-bg: #d3e9eb; 13 | //$body-color: #151417; 14 | 15 | // Changing the border radius of buttons 16 | //$border-radius: 15px; 17 | 18 | // Changing the theme colors 19 | //$primary: #202f41; 20 | //$secondary: #436296; 21 | //$success: #2bc550; 22 | //$info: #495dff; 23 | //$warning: #ef8143; 24 | //$danger: #ff293a; 25 | //$light: #dfe6ee; 26 | //$dark: #0f1319; 27 | 28 | // Adding (!) an additional theme color (ex. classes btn-cool, bg-cool) 29 | //$theme-colors: ( 30 | // "cool": #4d3fa3 31 | //); 32 | $color-theme-violet: #8a2be2 ; 33 | $color-theme-darkviolet: #9400d3; 34 | $color-white: #fff; 35 | 36 | $fa-font-path : '../../node_modules/font-awesome/fonts'; -------------------------------------------------------------------------------- /src/scss/mytheme.scss: -------------------------------------------------------------------------------- 1 | /*! Application Bootstrap 4 Theme 2 | * 3 | * Built on top of Bootstrap 4 (https://getbootstrap.com) 4 | * 5 | */ 6 | @import 'custom-variables'; 7 | @import '../../node_modules/bootstrap/scss/bootstrap'; 8 | @import '../../node_modules/font-awesome/scss/font-awesome'; 9 | @import 'custom-styles'; 10 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | } 7 | 8 | app-login, 9 | app-errors, 10 | full-height { 11 | height: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "types": [] 7 | }, 8 | "exclude": [ 9 | "src/test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sw-precache-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | navigateFallback: '/index.html', 3 | stripPrefix: './dist', 4 | root: './dist/', 5 | staticFileGlobs: [ 6 | './dist/index.html', 7 | './dist/**.js', 8 | './dist/**.css', 9 | './dist/**.ttf', 10 | './dist/assets/images/*', 11 | './dist/config/*', 12 | './dist/i18n/en.json', 13 | './dist/i18n/hr.json' 14 | ], 15 | runtimeCaching: [{ 16 | urlPattern: '', 17 | handler: 'fastest' 18 | }] 19 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": ["node_modules/@types"], 12 | "baseUrl": ".", 13 | "paths": { 14 | "@env/*": ["src/environments/*"], 15 | "@shared/*": ["src/app/shared/*"], 16 | "@e2e/*": ["e2e/*"], 17 | "@app/*": ["src/app/*"] 18 | }, 19 | "lib": ["es2017", "dom"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "use-input-property-decorator": true, 121 | "use-output-property-decorator": true, 122 | "use-host-property-decorator": true, 123 | "no-input-rename": true, 124 | "no-output-rename": true, 125 | "use-life-cycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | --------------------------------------------------------------------------------