├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------