├── .DS_Store ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── .DS_Store ├── app │ ├── .DS_Store │ ├── app-routing.module.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── core │ │ ├── core.module.ts │ │ ├── guards │ │ │ ├── admin.guard.spec.ts │ │ │ ├── admin.guard.ts │ │ │ ├── auth.guard.spec.ts │ │ │ ├── auth.guard.ts │ │ │ └── module-import.guard.ts │ │ ├── interceptors │ │ │ ├── auth.interceptor.ts │ │ │ └── spinner.interceptor.ts │ │ └── services │ │ │ ├── auth.service.ts │ │ │ ├── globar-error.handler.ts │ │ │ ├── notification.service.ts │ │ │ ├── spinner.service.spec.ts │ │ │ └── spinner.service.ts │ ├── custom-material │ │ ├── custom-material.module.ts │ │ └── select-check-all │ │ │ ├── select-check-all.component.css │ │ │ ├── select-check-all.component.html │ │ │ └── select-check-all.component.ts │ ├── features │ │ ├── about │ │ │ ├── about-page │ │ │ │ ├── about-page.component.css │ │ │ │ ├── about-page.component.html │ │ │ │ ├── about-page.component.spec.ts │ │ │ │ └── about-page.component.ts │ │ │ ├── about-routing.module.ts │ │ │ └── about.module.ts │ │ ├── account │ │ │ ├── account-page │ │ │ │ ├── account-page.component.css │ │ │ │ ├── account-page.component.html │ │ │ │ └── account-page.component.ts │ │ │ ├── account-routing.module.ts │ │ │ ├── account.module.ts │ │ │ ├── change-password │ │ │ │ ├── change-password.component.css │ │ │ │ ├── change-password.component.html │ │ │ │ └── change-password.component.ts │ │ │ └── profile-details │ │ │ │ ├── profile-details.component.css │ │ │ │ ├── profile-details.component.html │ │ │ │ └── profile-details.component.ts │ │ ├── auth │ │ │ ├── auth-routing.module.ts │ │ │ ├── auth.module.ts │ │ │ ├── login │ │ │ │ ├── login.component.css │ │ │ │ ├── login.component.html │ │ │ │ └── login.component.ts │ │ │ ├── password-reset-request │ │ │ │ ├── password-reset-request.component.css │ │ │ │ ├── password-reset-request.component.html │ │ │ │ └── password-reset-request.component.ts │ │ │ └── password-reset │ │ │ │ ├── password-reset.component.css │ │ │ │ ├── password-reset.component.html │ │ │ │ └── password-reset.component.ts │ │ ├── customers │ │ │ ├── customer-list │ │ │ │ ├── customer-list.component.css │ │ │ │ ├── customer-list.component.html │ │ │ │ └── customer-list.component.ts │ │ │ ├── customers-routing.module.ts │ │ │ └── customers.module.ts │ │ ├── dashboard │ │ │ ├── dashboard-home │ │ │ │ ├── dashboard-home.component.css │ │ │ │ ├── dashboard-home.component.html │ │ │ │ └── dashboard-home.component.ts │ │ │ ├── dashboard-routing.module.ts │ │ │ └── dashboard.module.ts │ │ ├── icons │ │ │ ├── icons-routing.module.ts │ │ │ ├── icons.module.ts │ │ │ └── icons │ │ │ │ ├── icons.component.css │ │ │ │ ├── icons.component.html │ │ │ │ ├── icons.component.spec.ts │ │ │ │ └── icons.component.ts │ │ ├── typography │ │ │ ├── typography-routing.module.ts │ │ │ ├── typography.module.ts │ │ │ └── typography │ │ │ │ ├── typography.component.css │ │ │ │ ├── typography.component.html │ │ │ │ ├── typography.component.spec.ts │ │ │ │ └── typography.component.ts │ │ └── users │ │ │ ├── user-list │ │ │ ├── user-list.component.css │ │ │ ├── user-list.component.html │ │ │ └── user-list.component.ts │ │ │ ├── users-routing.module.ts │ │ │ └── users.module.ts │ └── shared │ │ ├── confirm-dialog │ │ ├── confirm-dialog.component.css │ │ ├── confirm-dialog.component.html │ │ └── confirm-dialog.component.ts │ │ ├── content-placeholder-animation │ │ ├── content-placeholder-animation.component.css │ │ ├── content-placeholder-animation.component.html │ │ └── content-placeholder-animation.component.ts │ │ ├── layout │ │ ├── layout.component.css │ │ ├── layout.component.html │ │ └── layout.component.ts │ │ ├── mocks │ │ └── spinner-consumer.ts │ │ ├── pipes │ │ ├── limit-to.pipe.spec.ts │ │ ├── limit-to.pipe.ts │ │ ├── local-date.pipe.spec.ts │ │ ├── local-date.pipe.ts │ │ ├── yes-no.pipe.spec.ts │ │ └── yes-no.pipe.ts │ │ ├── shared.module.ts │ │ └── validators │ │ └── autocompleteSelectionValidator.ts ├── assets │ ├── .gitkeep │ ├── favicon │ │ ├── android-icon-144x144.png │ │ ├── android-icon-192x192.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── apple-icon-precomposed.png │ │ ├── apple-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── manifest.json │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ └── ms-icon-70x70.png │ └── images │ │ └── user.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css └── test.ts ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "tsconfig.json" 14 | ], 15 | "createDefaultProgram": true 16 | }, 17 | "extends": [ 18 | "plugin:@angular-eslint/recommended", 19 | "plugin:@angular-eslint/template/process-inline-templates" 20 | ], 21 | "rules": { 22 | "@angular-eslint/directive-selector": [ 23 | "error", 24 | { 25 | "type": "attribute", 26 | "prefix": "app", 27 | "style": "camelCase" 28 | } 29 | ], 30 | "@angular-eslint/component-selector": [ 31 | "error", 32 | { 33 | "type": "element", 34 | "prefix": "app", 35 | "style": "kebab-case" 36 | } 37 | ] 38 | } 39 | }, 40 | { 41 | "files": [ 42 | "*.html" 43 | ], 44 | "extends": [ 45 | "plugin:@angular-eslint/template/recommended" 46 | ], 47 | "rules": {} 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | dist/ 27 | 28 | # Visual Studio 2015/2017 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # Visual Studio 2017 auto generated files 34 | Generated\ Files/ 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | 44 | # Build Results of an ATL Project 45 | [Dd]ebugPS/ 46 | [Rr]eleasePS/ 47 | dlldata.c 48 | 49 | # Benchmark Results 50 | BenchmarkDotNet.Artifacts/ 51 | 52 | # .NET Core 53 | project.lock.json 54 | project.fragment.lock.json 55 | artifacts/ 56 | **/Properties/launchSettings.json 57 | 58 | # StyleCop 59 | StyleCopReport.xml 60 | 61 | # Files built by Visual Studio 62 | *_i.c 63 | *_p.c 64 | *_i.h 65 | *.ilk 66 | *.meta 67 | *.obj 68 | *.iobj 69 | *.pch 70 | *.pdb 71 | *.ipdb 72 | *.pgc 73 | *.pgd 74 | *.rsp 75 | *.sbr 76 | *.tlb 77 | *.tli 78 | *.tlh 79 | *.tmp 80 | *.tmp_proj 81 | *.log 82 | *.vspscc 83 | *.vssscc 84 | .builds 85 | *.pidb 86 | *.svclog 87 | *.scc 88 | 89 | # Chutzpah Test files 90 | _Chutzpah* 91 | 92 | # Visual C++ cache files 93 | ipch/ 94 | *.aps 95 | *.ncb 96 | *.opendb 97 | *.opensdf 98 | *.sdf 99 | *.cachefile 100 | *.VC.db 101 | *.VC.VC.opendb 102 | 103 | # Visual Studio profiler 104 | *.psess 105 | *.vsp 106 | *.vspx 107 | *.sap 108 | 109 | # Visual Studio Trace Files 110 | *.e2e 111 | 112 | # TFS 2012 Local Workspace 113 | $tf/ 114 | 115 | # Guidance Automation Toolkit 116 | *.gpState 117 | 118 | # ReSharper is a .NET coding add-in 119 | _ReSharper*/ 120 | *.[Rr]e[Ss]harper 121 | *.DotSettings.user 122 | 123 | # JustCode is a .NET coding add-in 124 | .JustCode 125 | 126 | # TeamCity is a build add-in 127 | _TeamCity* 128 | 129 | # DotCover is a Code Coverage Tool 130 | *.dotCover 131 | 132 | # AxoCover is a Code Coverage Tool 133 | .axoCover/* 134 | !.axoCover/settings.json 135 | 136 | # Visual Studio code coverage results 137 | *.coverage 138 | *.coveragexml 139 | 140 | # NCrunch 141 | _NCrunch_* 142 | .*crunch*.local.xml 143 | nCrunchTemp_* 144 | 145 | # MightyMoose 146 | *.mm.* 147 | AutoTest.Net/ 148 | 149 | # Web workbench (sass) 150 | .sass-cache/ 151 | 152 | # Installshield output folder 153 | [Ee]xpress/ 154 | 155 | # DocProject is a documentation generator add-in 156 | DocProject/buildhelp/ 157 | DocProject/Help/*.HxT 158 | DocProject/Help/*.HxC 159 | DocProject/Help/*.hhc 160 | DocProject/Help/*.hhk 161 | DocProject/Help/*.hhp 162 | DocProject/Help/Html2 163 | DocProject/Help/html 164 | 165 | # Click-Once directory 166 | publish/ 167 | 168 | # Publish Web Output 169 | *.[Pp]ublish.xml 170 | *.azurePubxml 171 | # Note: Comment the next line if you want to checkin your web deploy settings, 172 | # but database connection strings (with potential passwords) will be unencrypted 173 | *.pubxml 174 | *.publishproj 175 | 176 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 177 | # checkin your Azure Web App publish settings, but sensitive information contained 178 | # in these scripts will be unencrypted 179 | PublishScripts/ 180 | 181 | # NuGet Packages 182 | *.nupkg 183 | # The packages folder can be ignored because of Package Restore 184 | **/[Pp]ackages/* 185 | # except build/, which is used as an MSBuild target. 186 | !**/[Pp]ackages/build/ 187 | # Uncomment if necessary however generally it will be regenerated when needed 188 | #!**/[Pp]ackages/repositories.config 189 | # NuGet v3's project.json files produces more ignorable files 190 | *.nuget.props 191 | *.nuget.targets 192 | 193 | # Microsoft Azure Build Output 194 | csx/ 195 | *.build.csdef 196 | 197 | # Microsoft Azure Emulator 198 | ecf/ 199 | rcf/ 200 | 201 | # Windows Store app package directories and files 202 | AppPackages/ 203 | BundleArtifacts/ 204 | Package.StoreAssociation.xml 205 | _pkginfo.txt 206 | *.appx 207 | 208 | # Visual Studio cache files 209 | # files ending in .cache can be ignored 210 | *.[Cc]ache 211 | # but keep track of directories ending in .cache 212 | !*.[Cc]ache/ 213 | 214 | # Others 215 | ClientBin/ 216 | ~$* 217 | *~ 218 | *.dbmdl 219 | *.dbproj.schemaview 220 | *.jfm 221 | *.pfx 222 | *.publishsettings 223 | orleans.codegen.cs 224 | 225 | # Including strong name files can present a security risk 226 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 227 | #*.snk 228 | 229 | # Since there are multiple workflows, uncomment next line to ignore bower_components 230 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 231 | #bower_components/ 232 | 233 | # RIA/Silverlight projects 234 | Generated_Code/ 235 | 236 | # Backup & report files from converting an old project file 237 | # to a newer Visual Studio version. Backup files are not needed, 238 | # because we have git ;-) 239 | _UpgradeReport_Files/ 240 | Backup*/ 241 | UpgradeLog*.XML 242 | UpgradeLog*.htm 243 | ServiceFabricBackup/ 244 | *.rptproj.bak 245 | 246 | # SQL Server files 247 | *.mdf 248 | *.ldf 249 | *.ndf 250 | 251 | # Business Intelligence projects 252 | *.rdl.data 253 | *.bim.layout 254 | *.bim_*.settings 255 | *.rptproj.rsuser 256 | 257 | # Microsoft Fakes 258 | FakesAssemblies/ 259 | 260 | # GhostDoc plugin setting file 261 | *.GhostDoc.xml 262 | 263 | # Node.js Tools for Visual Studio 264 | .ntvs_analysis.dat 265 | node_modules/ 266 | 267 | # Visual Studio 6 build log 268 | *.plg 269 | 270 | # Visual Studio 6 workspace options file 271 | *.opt 272 | 273 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 274 | *.vbw 275 | 276 | # Visual Studio LightSwitch build output 277 | **/*.HTMLClient/GeneratedArtifacts 278 | **/*.DesktopClient/GeneratedArtifacts 279 | **/*.DesktopClient/ModelManifest.xml 280 | **/*.Server/GeneratedArtifacts 281 | **/*.Server/ModelManifest.xml 282 | _Pvt_Extensions 283 | 284 | # Paket dependency manager 285 | .paket/paket.exe 286 | paket-files/ 287 | 288 | # FAKE - F# Make 289 | .fake/ 290 | 291 | # JetBrains Rider 292 | .idea/ 293 | *.sln.iml 294 | 295 | # CodeRush 296 | .cr/ 297 | 298 | # Python Tools for Visual Studio (PTVS) 299 | __pycache__/ 300 | *.pyc 301 | 302 | # Cake - Uncomment if you are using it 303 | # tools/** 304 | # !tools/packages.config 305 | 306 | # Tabs Studio 307 | *.tss 308 | 309 | # Telerik's JustMock configuration file 310 | *.jmconfig 311 | 312 | # BizTalk build output 313 | *.btp.cs 314 | *.btm.cs 315 | *.odx.cs 316 | *.xsd.cs 317 | 318 | # OpenCover UI analysis results 319 | OpenCover/ 320 | 321 | # Azure Stream Analytics local run output 322 | ASALocalRun/ 323 | 324 | # MSBuild Binary and Structured Log 325 | *.binlog 326 | 327 | # NVidia Nsight GPU debugger configuration file 328 | *.nvuser 329 | 330 | # MFractors (Xamarin productivity tool) working folder 331 | .mfractor/ 332 | .angular/ 333 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Umut Esen 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 | [![Build status](https://dev.azure.com/umutesen/onthecode/_apis/build/status/Material%20Template%20CI)](https://dev.azure.com/umutesen/onthecode/_build/latest?definitionId=11) 2 | 3 | # Angular Material Starter Template 4 | 5 | ![Product Gif](https://github.com/umutesen/angular-material-template/blob/media/material-template-demo.gif) 6 | 7 | 8 | Angular Material Starter Template is a free template built with Angular and Angular Material. You can use it out of the box without having to change any file paths. Everything you need to start development on an Angular project is here. 9 | 10 | Angular Material starter template has been built with the official style guide in mind, which means it promotes a clean folder structure and separation of concerns. The material template is fully responsive and contains the fundamental building blocks of a scalable Angular application: 11 | 12 | Authentication module with login, logout and password reset components 13 | Responsive Admin dashboard with sidebar 14 | Account area with change password component 15 | All Angular Material components 16 | In addition to Angular, other well-known open-source libraries such as rxjs, moment and ngx-logger are also included. 17 | 18 | This application template came as a result of several applications that I have developed over the past few years. 19 | 20 | Having mostly used Angular Material component, I wanted to create a starter template to save time for greenfield projects. I developed it based on user feedback and it is a powerful Angular admin dashboard, which allows you to build products like admin panels, content management systems (CMS) and customer relationship management (CRM) software. 21 | 22 | ## Starter Template Features 23 | 24 | Clean folder structure 25 | Core module 26 | Shared module 27 | Example feature modules 28 | Lazy-loaded feature modules 29 | Global error-handling 30 | Error logging with ngx-logger (logging to browser & remote API) 31 | HTTP Interceptors to inject JWT-tokens Authentication and role guards (for Role-based access) 32 | Shows spinner for all HTTP requests 33 | Angular flex layout 34 | Browser Support 35 | 36 | At present, the template aims to support the last two versions of the following browsers: 37 | 38 | Chrome 39 | Firefox 40 | Microsoft Edge 41 | Safari 42 | Opera 43 | 44 | 45 | ## Development server 46 | 47 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 48 | 49 | ## Code scaffolding 50 | 51 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 52 | 53 | ## Build 54 | 55 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 56 | 57 | ## Running unit tests 58 | 59 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 60 | 61 | ## Running end-to-end tests 62 | 63 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 64 | 65 | ## Further help 66 | 67 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 68 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-material-template": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:application": { 10 | "strict": true 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/angular-material-template", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "assets": [ 26 | "src/favicon.ico", 27 | "src/assets" 28 | ], 29 | "styles": [ 30 | "src/styles.css" 31 | ], 32 | "scripts": [] 33 | }, 34 | "configurations": { 35 | "production": { 36 | "budgets": [ 37 | { 38 | "type": "initial", 39 | "maximumWarning": "500kb", 40 | "maximumError": "2mb" 41 | }, 42 | { 43 | "type": "anyComponentStyle", 44 | "maximumWarning": "2kb", 45 | "maximumError": "4kb" 46 | } 47 | ], 48 | "fileReplacements": [ 49 | { 50 | "replace": "src/environments/environment.ts", 51 | "with": "src/environments/environment.prod.ts" 52 | } 53 | ], 54 | "outputHashing": "all" 55 | }, 56 | "development": { 57 | "buildOptimizer": false, 58 | "optimization": false, 59 | "vendorChunk": true, 60 | "extractLicenses": false, 61 | "sourceMap": true, 62 | "namedChunks": true 63 | } 64 | }, 65 | "defaultConfiguration": "production" 66 | }, 67 | "serve": { 68 | "builder": "@angular-devkit/build-angular:dev-server", 69 | "configurations": { 70 | "production": { 71 | "browserTarget": "angular-material-template:build:production" 72 | }, 73 | "development": { 74 | "browserTarget": "angular-material-template:build:development" 75 | } 76 | }, 77 | "defaultConfiguration": "development" 78 | }, 79 | "extract-i18n": { 80 | "builder": "@angular-devkit/build-angular:extract-i18n", 81 | "options": { 82 | "browserTarget": "angular-material-template:build" 83 | } 84 | }, 85 | "test": { 86 | "builder": "@angular-devkit/build-angular:karma", 87 | "options": { 88 | "main": "src/test.ts", 89 | "polyfills": "src/polyfills.ts", 90 | "tsConfig": "tsconfig.spec.json", 91 | "karmaConfig": "karma.conf.js", 92 | "assets": [ 93 | "src/favicon.ico", 94 | "src/assets" 95 | ], 96 | "styles": [ 97 | "src/styles.css" 98 | ], 99 | "scripts": [] 100 | } 101 | }, 102 | "lint": { 103 | "builder": "@angular-eslint/builder:lint", 104 | "options": { 105 | "lintFilePatterns": [ 106 | "src/**/*.ts", 107 | "src/**/*.html" 108 | ] 109 | } 110 | } 111 | } 112 | } 113 | }, 114 | "cli": { 115 | "schematicCollections": [ 116 | "@angular-eslint/schematics" 117 | ] 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /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'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/angular-material-template'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-material-template", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "lint": "ng lint", 10 | "test": "ng test", 11 | "build-production": "ng build --configuration=production --base-href=/angular-material-template/" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "^14.2.1", 16 | "@angular/common": "^14.2.1", 17 | "@angular/compiler": "^14.2.1", 18 | "@angular/core": "^14.2.1", 19 | "@angular/cdk": "^14.2.1", 20 | "@angular/flex-layout": "^14.0.0-beta.40", 21 | "@angular/forms": "^14.2.1", 22 | "@angular/material": "^14.2.1", 23 | "@angular/material-moment-adapter": "^14.2.1", 24 | "@angular/platform-browser": "^14.2.1", 25 | "@angular/platform-browser-dynamic": "^14.2.1", 26 | "@angular/router": "^14.2.1", 27 | "jwt-decode": "^3.1.2", 28 | "ngx-logger": "^5.0.7", 29 | "rxjs": "~7.5.0", 30 | "tslib": "^2.3.0", 31 | "zone.js": "~0.11.4" 32 | }, 33 | "devDependencies": { 34 | "@angular-devkit/build-angular": "^14.2.2", 35 | "@angular-eslint/builder": "14.1.1", 36 | "@angular-eslint/eslint-plugin": "14.1.1", 37 | "@angular-eslint/eslint-plugin-template": "14.1.1", 38 | "@angular-eslint/schematics": "14.1.1", 39 | "@angular-eslint/template-parser": "14.1.1", 40 | "@angular/cli": "^14.2.2", 41 | "@angular/compiler-cli": "^14.2.1", 42 | "@types/jasmine": "~3.10.0", 43 | "@types/node": "^12.11.1", 44 | "@typescript-eslint/eslint-plugin": "^5.36.2", 45 | "@typescript-eslint/parser": "^5.36.2", 46 | "eslint": "^8.23.0", 47 | "jasmine-core": "~4.0.0", 48 | "karma": "~6.3.0", 49 | "karma-chrome-launcher": "~3.1.0", 50 | "karma-coverage": "~2.1.0", 51 | "karma-jasmine": "~4.0.0", 52 | "karma-jasmine-html-reporter": "~1.7.0", 53 | "typescript": "~4.8.3" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/.DS_Store -------------------------------------------------------------------------------- /src/app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/app/.DS_Store -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { AuthGuard } from './core/guards/auth.guard'; 5 | 6 | const appRoutes: Routes = [ 7 | { 8 | path: 'auth', 9 | loadChildren: () => import('./features/auth/auth.module').then(m => m.AuthModule), 10 | }, 11 | { 12 | path: 'dashboard', 13 | loadChildren: () => import('./features/dashboard/dashboard.module').then(m => m.DashboardModule), 14 | canActivate: [AuthGuard] 15 | }, 16 | { 17 | path: 'customers', 18 | loadChildren: () => import('./features/customers/customers.module').then(m => m.CustomersModule), 19 | canActivate: [AuthGuard] 20 | }, 21 | { 22 | path: 'users', 23 | loadChildren: () => import('./features/users/users.module').then(m => m.UsersModule), 24 | canActivate: [AuthGuard] 25 | }, 26 | { 27 | path: 'account', 28 | loadChildren: () => import('./features/account/account.module').then(m => m.AccountModule), 29 | canActivate: [AuthGuard] 30 | }, 31 | { 32 | path: 'icons', 33 | loadChildren: () => import('./features/icons/icons.module').then(m => m.IconsModule), 34 | canActivate: [AuthGuard] 35 | }, 36 | { 37 | path: 'typography', 38 | loadChildren: () => import('./features/typography/typography.module').then(m => m.TypographyModule), 39 | canActivate: [AuthGuard] 40 | }, 41 | { 42 | path: 'about', 43 | loadChildren: () => import('./features/about/about.module').then(m => m.AboutModule), 44 | canActivate: [AuthGuard] 45 | }, 46 | { 47 | path: '**', 48 | redirectTo: 'dashboard', 49 | pathMatch: 'full' 50 | } 51 | ]; 52 | 53 | @NgModule({ 54 | imports: [ 55 | RouterModule.forRoot(appRoutes) 56 | ], 57 | exports: [RouterModule], 58 | providers: [] 59 | }) 60 | export class AppRoutingModule { } 61 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | template: `` 6 | }) 7 | export class AppComponent {} 8 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | 5 | import { AppComponent } from './app.component'; 6 | import { CoreModule } from './core/core.module'; 7 | import { SharedModule } from './shared/shared.module'; 8 | import { CustomMaterialModule } from './custom-material/custom-material.module'; 9 | import { AppRoutingModule } from './app-routing.module'; 10 | import { LoggerModule } from 'ngx-logger'; 11 | import { environment } from '../environments/environment'; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | AppComponent 16 | ], 17 | imports: [ 18 | BrowserModule, 19 | BrowserAnimationsModule, 20 | CoreModule, 21 | SharedModule, 22 | CustomMaterialModule.forRoot(), 23 | AppRoutingModule, 24 | LoggerModule.forRoot({ 25 | serverLoggingUrl: `http://my-api/logs`, 26 | level: environment.logLevel, 27 | serverLogLevel: environment.serverLogLevel 28 | }) 29 | ], 30 | bootstrap: [AppComponent] 31 | }) 32 | export class AppModule { } 33 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, Optional, SkipSelf, ErrorHandler } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 4 | import { MediaMatcher } from '@angular/cdk/layout'; 5 | import { NGXLogger } from 'ngx-logger'; 6 | 7 | import { AuthInterceptor } from './interceptors/auth.interceptor'; 8 | import { SpinnerInterceptor } from './interceptors/spinner.interceptor'; 9 | import { AuthGuard } from './guards/auth.guard'; 10 | import { throwIfAlreadyLoaded } from './guards/module-import.guard'; 11 | import { GlobalErrorHandler } from './services/globar-error.handler'; 12 | import { AdminGuard } from './guards/admin.guard'; 13 | 14 | @NgModule({ 15 | imports: [ 16 | CommonModule, 17 | HttpClientModule 18 | ], 19 | declarations: [ 20 | ], 21 | providers: [ 22 | AuthGuard, 23 | AdminGuard, 24 | MediaMatcher, 25 | { 26 | provide: HTTP_INTERCEPTORS, 27 | useClass: SpinnerInterceptor, 28 | multi: true 29 | }, 30 | { 31 | provide: HTTP_INTERCEPTORS, 32 | useClass: AuthInterceptor, 33 | multi: true 34 | }, 35 | { 36 | provide: ErrorHandler, 37 | useClass: GlobalErrorHandler 38 | }, 39 | { provide: NGXLogger, useClass: NGXLogger }, 40 | { provide: 'LOCALSTORAGE', useValue: window.localStorage } 41 | ], 42 | exports: [ 43 | ] 44 | }) 45 | export class CoreModule { 46 | constructor(@Optional() @SkipSelf() parentModule: CoreModule) { 47 | throwIfAlreadyLoaded(parentModule, 'CoreModule'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/core/guards/admin.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { AdminGuard } from './admin.guard'; 2 | 3 | describe('AdminGuard', () => { 4 | 5 | let router; 6 | let authService; 7 | 8 | beforeEach(() => { 9 | router = jasmine.createSpyObj(['navigate']); 10 | authService = jasmine.createSpyObj(['getCurrentUser']); 11 | }); 12 | 13 | it('create an instance', () => { 14 | const guard = new AdminGuard(router, authService); 15 | expect(guard).toBeTruthy(); 16 | }); 17 | 18 | it('returns true if user is admin', () => { 19 | const user = { 'isAdmin': true }; 20 | authService.getCurrentUser.and.returnValue(user); 21 | const guard = new AdminGuard(router, authService); 22 | 23 | const result = guard.canActivate(); 24 | 25 | expect(result).toBe(true); 26 | }); 27 | 28 | it('returns false if user does not exist', () => { 29 | authService.getCurrentUser.and.returnValue(null); 30 | const guard = new AdminGuard(router, authService); 31 | 32 | const result = guard.canActivate(); 33 | 34 | expect(result).toBe(false); 35 | }); 36 | 37 | it('returns false if user is not admin', () => { 38 | const user = { 'isAdmin': false }; 39 | authService.getCurrentUser.and.returnValue(user); 40 | const guard = new AdminGuard(router, authService); 41 | 42 | const result = guard.canActivate(); 43 | 44 | expect(result).toBe(false); 45 | }); 46 | 47 | it('redirects to root if user is not an admin', () => { 48 | const user = { 'isAdmin': false }; 49 | authService.getCurrentUser.and.returnValue(user); 50 | const guard = new AdminGuard(router, authService); 51 | 52 | guard.canActivate(); 53 | 54 | expect(router.navigate).toHaveBeenCalledWith(['/']); 55 | }); 56 | 57 | it('redirects to root if user does not exist', () => { 58 | authService.getCurrentUser.and.returnValue(null); 59 | const guard = new AdminGuard(router, authService); 60 | 61 | guard.canActivate(); 62 | 63 | expect(router.navigate).toHaveBeenCalledWith(['/']); 64 | }); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /src/app/core/guards/admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanActivate } from '@angular/router'; 3 | import { AuthenticationService } from '../services/auth.service'; 4 | 5 | @Injectable() 6 | export class AdminGuard implements CanActivate { 7 | 8 | constructor(private router: Router, 9 | private authService: AuthenticationService) { } 10 | 11 | canActivate() { 12 | const user = this.authService.getCurrentUser(); 13 | 14 | if (user && user.isAdmin) { 15 | return true; 16 | 17 | } else { 18 | this.router.navigate(['/']); 19 | return false; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/core/guards/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthGuard } from './auth.guard'; 2 | import * as moment from 'moment'; 3 | 4 | describe('AuthGuard', () => { 5 | 6 | let router; 7 | let authService; 8 | let notificationService; 9 | 10 | beforeEach(() => { 11 | router = jasmine.createSpyObj(['navigate']); 12 | authService = jasmine.createSpyObj(['getCurrentUser']); 13 | notificationService = jasmine.createSpyObj(['openSnackBar']); 14 | }); 15 | 16 | it('create an instance', () => { 17 | const guard = new AuthGuard(router, notificationService, authService); 18 | expect(guard).toBeTruthy(); 19 | }); 20 | 21 | it('returns false if user is null', () => { 22 | authService.getCurrentUser.and.returnValue(null); 23 | const guard = new AuthGuard(router, notificationService, authService); 24 | 25 | const result = guard.canActivate(); 26 | 27 | expect(result).toBe(false); 28 | }); 29 | 30 | it('redirects to login if user is null', () => { 31 | authService.getCurrentUser.and.returnValue(null); 32 | const guard = new AuthGuard(router, notificationService, authService); 33 | 34 | guard.canActivate(); 35 | 36 | expect(router.navigate).toHaveBeenCalledWith(['auth/login']); 37 | }); 38 | 39 | it('does not display expired notification if user is null', () => { 40 | authService.getCurrentUser.and.returnValue(null); 41 | const guard = new AuthGuard(router, notificationService, authService); 42 | 43 | guard.canActivate(); 44 | 45 | expect(notificationService.openSnackBar).toHaveBeenCalledTimes(0); 46 | }); 47 | 48 | it('redirects to login if user session has expired', () => { 49 | const user = { expiration: moment().add(-1, 'minutes') }; 50 | authService.getCurrentUser.and.returnValue(user); 51 | const guard = new AuthGuard(router, notificationService, authService); 52 | 53 | guard.canActivate(); 54 | 55 | expect(router.navigate).toHaveBeenCalledWith(['auth/login']); 56 | }); 57 | 58 | it('displays notification if user session has expired', () => { 59 | const user = { expiration: moment().add(-1, 'seconds') }; 60 | authService.getCurrentUser.and.returnValue(user); 61 | const guard = new AuthGuard(router, notificationService, authService); 62 | 63 | guard.canActivate(); 64 | 65 | expect(notificationService.openSnackBar) 66 | .toHaveBeenCalledWith('Your session has expired'); 67 | }); 68 | 69 | it('returns true if user session is valid', () => { 70 | const user = { expiration: moment().add(1, 'minutes') }; 71 | authService.getCurrentUser.and.returnValue(user); 72 | const guard = new AuthGuard(router, notificationService, authService); 73 | 74 | const result = guard.canActivate(); 75 | 76 | expect(result).toBe(true); 77 | }); 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /src/app/core/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanActivate } from '@angular/router'; 3 | import * as moment from 'moment'; 4 | 5 | import { AuthenticationService } from '../services/auth.service'; 6 | import { NotificationService } from '../services/notification.service'; 7 | 8 | @Injectable() 9 | export class AuthGuard implements CanActivate { 10 | 11 | constructor(private router: Router, 12 | private notificationService: NotificationService, 13 | private authService: AuthenticationService) { } 14 | 15 | canActivate() { 16 | const user = this.authService.getCurrentUser(); 17 | 18 | if (user && user.expiration) { 19 | 20 | if (moment() < moment(user.expiration)) { 21 | return true; 22 | } else { 23 | this.notificationService.openSnackBar('Your session has expired'); 24 | this.router.navigate(['auth/login']); 25 | return false; 26 | } 27 | } 28 | 29 | this.router.navigate(['auth/login']); 30 | return false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/core/guards/module-import.guard.ts: -------------------------------------------------------------------------------- 1 | export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) { 2 | if (parentModule) { 3 | throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/app/core/interceptors/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Router } from '@angular/router'; 2 | import { Observable } from 'rxjs'; 3 | import { Injectable } from '@angular/core'; 4 | import { HttpInterceptor, HttpErrorResponse } from '@angular/common/http'; 5 | import { HttpRequest } from '@angular/common/http'; 6 | import { HttpHandler } from '@angular/common/http'; 7 | import { HttpEvent } from '@angular/common/http'; 8 | import { tap } from 'rxjs/operators'; 9 | 10 | import { AuthenticationService } from '../services/auth.service'; 11 | import { MatDialog } from '@angular/material/dialog'; 12 | 13 | @Injectable() 14 | export class AuthInterceptor implements HttpInterceptor { 15 | 16 | constructor(private authService: AuthenticationService, 17 | private router: Router, 18 | private dialog: MatDialog) { } 19 | 20 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 21 | 22 | const user = this.authService.getCurrentUser(); 23 | 24 | if (user && user.token) { 25 | 26 | const cloned = req.clone({ 27 | headers: req.headers.set('Authorization', 28 | 'Bearer ' + user.token) 29 | }); 30 | 31 | return next.handle(cloned).pipe(tap(() => { }, (err: any) => { 32 | if (err instanceof HttpErrorResponse) { 33 | if (err.status === 401) { 34 | this.dialog.closeAll(); 35 | this.router.navigate(['/auth/login']); 36 | } 37 | } 38 | })); 39 | 40 | } else { 41 | return next.handle(req); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/core/interceptors/spinner.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Router } from '@angular/router'; 2 | import { Observable } from 'rxjs'; 3 | import { Injectable } from '@angular/core'; 4 | import { HttpInterceptor, HttpResponse } from '@angular/common/http'; 5 | import { HttpRequest } from '@angular/common/http'; 6 | import { HttpHandler } from '@angular/common/http'; 7 | import { HttpEvent } from '@angular/common/http'; 8 | import { tap } from 'rxjs/operators'; 9 | 10 | import { SpinnerService } from './../services/spinner.service'; 11 | 12 | @Injectable() 13 | export class SpinnerInterceptor implements HttpInterceptor { 14 | 15 | constructor(private spinnerService: SpinnerService) { } 16 | 17 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 18 | 19 | this.spinnerService.show(); 20 | 21 | return next 22 | .handle(req) 23 | .pipe( 24 | tap((event: HttpEvent) => { 25 | if (event instanceof HttpResponse) { 26 | this.spinnerService.hide(); 27 | } 28 | }, (error) => { 29 | this.spinnerService.hide(); 30 | }) 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/core/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { delay, map } from 'rxjs/operators'; 4 | import * as jwt_decode from 'jwt-decode'; 5 | import * as moment from 'moment'; 6 | 7 | import { environment } from '../../../environments/environment'; 8 | import { of, EMPTY } from 'rxjs'; 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class AuthenticationService { 14 | 15 | constructor(private http: HttpClient, 16 | @Inject('LOCALSTORAGE') private localStorage: Storage) { 17 | } 18 | 19 | login(email: string, password: string) { 20 | return of(true) 21 | .pipe(delay(1000), 22 | map((/*response*/) => { 23 | // set token property 24 | // const decodedToken = jwt_decode(response['token']); 25 | 26 | // store email and jwt token in local storage to keep user logged in between page refreshes 27 | this.localStorage.setItem('currentUser', JSON.stringify({ 28 | token: 'aisdnaksjdn,axmnczm', 29 | isAdmin: true, 30 | email: 'john.doe@gmail.com', 31 | id: '12312323232', 32 | alias: 'john.doe@gmail.com'.split('@')[0], 33 | expiration: moment().add(1, 'days').toDate(), 34 | fullName: 'John Doe' 35 | })); 36 | 37 | return true; 38 | })); 39 | } 40 | 41 | logout(): void { 42 | // clear token remove user from local storage to log user out 43 | this.localStorage.removeItem('currentUser'); 44 | } 45 | 46 | getCurrentUser(): any { 47 | // TODO: Enable after implementation 48 | // return JSON.parse(this.localStorage.getItem('currentUser')); 49 | return { 50 | token: 'aisdnaksjdn,axmnczm', 51 | isAdmin: true, 52 | email: 'john.doe@gmail.com', 53 | id: '12312323232', 54 | alias: 'john.doe@gmail.com'.split('@')[0], 55 | expiration: moment().add(1, 'days').toDate(), 56 | fullName: 'John Doe' 57 | }; 58 | } 59 | 60 | passwordResetRequest(email: string) { 61 | return of(true).pipe(delay(1000)); 62 | } 63 | 64 | changePassword(email: string, currentPwd: string, newPwd: string) { 65 | return of(true).pipe(delay(1000)); 66 | } 67 | 68 | passwordReset(email: string, token: string, password: string, confirmPassword: string): any { 69 | return of(true).pipe(delay(1000)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/core/services/globar-error.handler.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler, Injectable, Injector } from '@angular/core'; 2 | import { NGXLogger } from 'ngx-logger'; 3 | 4 | @Injectable() 5 | export class GlobalErrorHandler implements ErrorHandler { 6 | 7 | 8 | constructor(private injector: Injector) { } 9 | 10 | handleError(error: Error) { 11 | // Obtain dependencies at the time of the error 12 | // This is because the GlobalErrorHandler is registered first 13 | // which prevents constructor dependency injection 14 | const logger = this.injector.get(NGXLogger); 15 | 16 | const err = { 17 | message: error.message ? error.message : error.toString(), 18 | stack: error.stack ? error.stack : '' 19 | }; 20 | 21 | // Log the error 22 | logger.error(err); 23 | 24 | // Re-throw the error 25 | throw error; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/core/services/notification.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MatSnackBar } from '@angular/material/snack-bar'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class NotificationService { 8 | 9 | constructor(private snackBar: MatSnackBar) { } 10 | 11 | public openSnackBar(message: string) { 12 | this.snackBar.open(message, '', { 13 | duration: 5000 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/core/services/spinner.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { SpinnerConsumer } from '../../shared/mocks/spinner-consumer'; 2 | import { SpinnerService } from './spinner.service'; 3 | 4 | describe('BusyIndicatorService', () => { 5 | let component: SpinnerService; 6 | let consumer1: SpinnerConsumer; 7 | let consumer2: SpinnerConsumer; 8 | 9 | beforeEach(() => { 10 | component = new SpinnerService(); 11 | consumer1 = new SpinnerConsumer(component); 12 | consumer2 = new SpinnerConsumer(component); 13 | }); 14 | 15 | it('should be created', () => { 16 | expect(component).toBeTruthy(); 17 | }); 18 | 19 | it('should initialise visibility to false', () => { 20 | component.visibility.subscribe((value: boolean) => { 21 | expect(value).toBe(false); 22 | }); 23 | }); 24 | 25 | it('should broadcast visibility to all consumers', () => { 26 | expect(consumer1.isBusy).toBe(false); 27 | expect(consumer2.isBusy).toBe(false); 28 | }); 29 | 30 | it('should broadcast visibility to all consumers when the value changes', () => { 31 | component.visibility.next(true); 32 | 33 | expect(consumer1.isBusy).toBe(true); 34 | expect(consumer2.isBusy).toBe(true); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/app/core/services/spinner.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class SpinnerService { 8 | 9 | visibility = new BehaviorSubject(false); 10 | 11 | constructor() { 12 | } 13 | 14 | show() { 15 | this.visibility.next(true); 16 | } 17 | 18 | hide() { 19 | this.visibility.next(false); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/custom-material/custom-material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, LOCALE_ID } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { MatMomentDateModule } from '@angular/material-moment-adapter'; 4 | import { MatCheckboxModule } from '@angular/material/checkbox'; 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatInputModule } from '@angular/material/input'; 7 | import { MatAutocompleteModule } from '@angular/material/autocomplete'; 8 | import { MatDatepickerModule } from '@angular/material/datepicker'; 9 | import { MatFormFieldModule } from '@angular/material/form-field'; 10 | import { MatRadioModule } from '@angular/material/radio'; 11 | import { MatSelectModule } from '@angular/material/select'; 12 | import { MatSliderModule } from '@angular/material/slider'; 13 | import { MatSlideToggleModule } from '@angular/material/slide-toggle'; 14 | import { MatMenuModule } from '@angular/material/menu'; 15 | import { MatSidenavModule } from '@angular/material/sidenav'; 16 | import { MatBadgeModule } from '@angular/material/badge'; 17 | import { MatToolbarModule } from '@angular/material/toolbar'; 18 | import { MatListModule } from '@angular/material/list'; 19 | import { MatGridListModule } from '@angular/material/grid-list'; 20 | import { MatCardModule } from '@angular/material/card'; 21 | import { MatStepperModule } from '@angular/material/stepper'; 22 | import { MatTabsModule } from '@angular/material/tabs'; 23 | import { MatExpansionModule } from '@angular/material/expansion'; 24 | import { MatButtonToggleModule } from '@angular/material/button-toggle'; 25 | import { MatChipsModule } from '@angular/material/chips'; 26 | import { MatIconModule } from '@angular/material/icon'; 27 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 28 | import { MatProgressBarModule } from '@angular/material/progress-bar'; 29 | import { MatDialogModule } from '@angular/material/dialog'; 30 | import { MatTooltipModule } from '@angular/material/tooltip'; 31 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 32 | import { MatTableModule } from '@angular/material/table'; 33 | import { MatSortModule } from '@angular/material/sort'; 34 | import { MatPaginatorModule } from '@angular/material/paginator'; 35 | import { SelectCheckAllComponent } from './select-check-all/select-check-all.component'; 36 | import { DragDropModule } from '@angular/cdk/drag-drop'; 37 | import { MAT_DATE_FORMATS } from '@angular/material/core'; 38 | 39 | export const MY_FORMATS = { 40 | parse: { 41 | dateInput: 'DD MMM YYYY', 42 | }, 43 | display: { 44 | dateInput: 'DD MMM YYYY', 45 | monthYearLabel: 'MMM YYYY', 46 | dateA11yLabel: 'LL', 47 | monthYearA11yLabel: 'MMMM YYYY' 48 | } 49 | }; 50 | 51 | @NgModule({ 52 | imports: [ 53 | CommonModule, 54 | MatMomentDateModule, 55 | MatSidenavModule, MatIconModule, MatToolbarModule, MatButtonModule, 56 | MatListModule, MatCardModule, MatProgressBarModule, MatInputModule, 57 | MatSnackBarModule, MatProgressSpinnerModule, MatDatepickerModule, 58 | MatAutocompleteModule, MatTableModule, MatDialogModule, MatTabsModule, 59 | MatTooltipModule, MatSelectModule, MatPaginatorModule, MatChipsModule, 60 | MatButtonToggleModule, MatSlideToggleModule, MatBadgeModule, MatCheckboxModule, 61 | MatExpansionModule, DragDropModule, MatSortModule 62 | ], 63 | exports: [ 64 | CommonModule, 65 | MatSidenavModule, MatIconModule, MatToolbarModule, MatButtonModule, 66 | MatListModule, MatCardModule, MatProgressBarModule, MatInputModule, 67 | MatSnackBarModule, MatMenuModule, MatProgressSpinnerModule, MatDatepickerModule, 68 | MatAutocompleteModule, MatTableModule, MatDialogModule, MatTabsModule, 69 | MatTooltipModule, MatSelectModule, MatPaginatorModule, MatChipsModule, 70 | MatButtonToggleModule, MatSlideToggleModule, MatBadgeModule, MatCheckboxModule, 71 | MatExpansionModule, SelectCheckAllComponent, DragDropModule, MatSortModule 72 | ], 73 | providers: [ 74 | { 75 | provide: MAT_DATE_FORMATS, 76 | useValue: MY_FORMATS 77 | }, 78 | { provide: LOCALE_ID, useValue: 'en-gb' } 79 | ], 80 | declarations: [SelectCheckAllComponent] 81 | }) 82 | export class CustomMaterialModule { 83 | static forRoot() { 84 | return { 85 | ngModule: CustomMaterialModule, 86 | providers: [ 87 | ] 88 | }; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/app/custom-material/select-check-all/select-check-all.component.css: -------------------------------------------------------------------------------- 1 | app-select-check-all .mat-checkbox-layout, 2 | app-select-check-all .mat-checkbox-label { 3 | width:100% !important; 4 | } -------------------------------------------------------------------------------- /src/app/custom-material/select-check-all/select-check-all.component.html: -------------------------------------------------------------------------------- 1 | 3 | {{text}} 4 | -------------------------------------------------------------------------------- /src/app/custom-material/select-check-all/select-check-all.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewEncapsulation } from '@angular/core'; 2 | import { UntypedFormControl } from '@angular/forms'; 3 | import { MatCheckboxChange } from '@angular/material/checkbox'; 4 | 5 | @Component({ 6 | selector: 'app-select-check-all', 7 | templateUrl: './select-check-all.component.html', 8 | styleUrls: ['./select-check-all.component.css'], 9 | encapsulation: ViewEncapsulation.None 10 | }) 11 | export class SelectCheckAllComponent { 12 | @Input() 13 | model: UntypedFormControl = new UntypedFormControl; 14 | @Input() values = []; 15 | @Input() text = 'Select All'; 16 | 17 | constructor() { } 18 | 19 | isChecked(): boolean { 20 | return this.model.value && this.values.length 21 | && this.model.value.length === this.values.length; 22 | } 23 | 24 | isIndeterminate(): boolean { 25 | return this.model.value && this.values.length && this.model.value.length 26 | && this.model.value.length < this.values.length; 27 | } 28 | 29 | toggleSelection(change: MatCheckboxChange): void { 30 | if (change.checked) { 31 | this.model.setValue(this.values); 32 | } else { 33 | this.model.setValue([]); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/features/about/about-page/about-page.component.css: -------------------------------------------------------------------------------- 1 | mat-icon { 2 | color: rgb(200, 0, 0); 3 | } -------------------------------------------------------------------------------- /src/app/features/about/about-page/about-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |

About

7 | 8 |

9 | Built on top of Angular & Angular Material, angular-material-template provides a simple template that you can use for your next project. 13 |

14 | 15 |

16 | Support the project by starring it on 18 | GitHub 19 | . 20 |

21 | 22 |

23 | Made with favorite by onthecode. 25 |

26 | 27 | 28 |
29 |
30 | 31 |
32 |
-------------------------------------------------------------------------------- /src/app/features/about/about-page/about-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AboutPageComponent } from './about-page.component'; 4 | 5 | describe('AboutHomeComponent', () => { 6 | let component: AboutPageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AboutPageComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AboutPageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/about/about-page/about-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-about-page', 5 | templateUrl: './about-page.component.html', 6 | styleUrls: ['./about-page.component.css'] 7 | }) 8 | export class AboutPageComponent { 9 | 10 | constructor() { } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/app/features/about/about-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { LayoutComponent } from '../../shared/layout/layout.component'; 5 | import { AboutPageComponent } from './about-page/about-page.component'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: '', 10 | component: LayoutComponent, 11 | children: [ 12 | { path: '', component: AboutPageComponent }, 13 | ] 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forChild(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class AboutRoutingModule { } 22 | -------------------------------------------------------------------------------- /src/app/features/about/about.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { AboutRoutingModule } from './about-routing.module'; 5 | import { AboutPageComponent } from './about-page/about-page.component'; 6 | import { SharedModule } from '../../shared/shared.module'; 7 | 8 | @NgModule({ 9 | declarations: [AboutPageComponent], 10 | imports: [ 11 | CommonModule, 12 | SharedModule, 13 | AboutRoutingModule 14 | ] 15 | }) 16 | export class AboutModule { } 17 | -------------------------------------------------------------------------------- /src/app/features/account/account-page/account-page.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/app/features/account/account-page/account-page.component.css -------------------------------------------------------------------------------- /src/app/features/account/account-page/account-page.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 |

My Profile

8 | 9 |
10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 |
31 |
32 |
33 | 34 | 35 |
-------------------------------------------------------------------------------- /src/app/features/account/account-page/account-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Title } from '@angular/platform-browser'; 3 | 4 | @Component({ 5 | selector: 'app-account-page', 6 | templateUrl: './account-page.component.html', 7 | styleUrls: ['./account-page.component.css'] 8 | }) 9 | export class AccountPageComponent implements OnInit { 10 | 11 | constructor(private titleService: Title) { } 12 | 13 | ngOnInit() { 14 | this.titleService.setTitle('angular-material-template - Account'); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/features/account/account-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { LayoutComponent } from 'src/app/shared/layout/layout.component'; 4 | 5 | import { AccountPageComponent } from './account-page/account-page.component'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: '', 10 | component: LayoutComponent, 11 | children: [ 12 | { path: 'profile', component: AccountPageComponent }, 13 | ] 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forChild(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class AccountRoutingModule { } 22 | -------------------------------------------------------------------------------- /src/app/features/account/account.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { AccountRoutingModule } from './account-routing.module'; 5 | import { AccountPageComponent } from './account-page/account-page.component'; 6 | import { ChangePasswordComponent } from './change-password/change-password.component'; 7 | import { ProfileDetailsComponent } from './profile-details/profile-details.component'; 8 | import { SharedModule } from 'src/app/shared/shared.module'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | CommonModule, 13 | SharedModule, 14 | AccountRoutingModule 15 | ], 16 | declarations: [AccountPageComponent, ChangePasswordComponent, ProfileDetailsComponent], 17 | exports: [AccountPageComponent] 18 | }) 19 | export class AccountModule { } 20 | -------------------------------------------------------------------------------- /src/app/features/account/change-password/change-password.component.css: -------------------------------------------------------------------------------- 1 | .password-rules .mat-divider { 2 | position: unset !important; 3 | } 4 | .container{ 5 | padding-top: 20px; 6 | } -------------------------------------------------------------------------------- /src/app/features/account/change-password/change-password.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Use the form below to change your password.

3 | 4 |
5 | 6 |
7 | 8 | 9 | 11 | 12 | {{hideCurrentPassword ? 'visibility' : 'visibility_off'}} 13 | 14 | 15 | 16 | Please enter a your current password 17 | 18 | 19 | 20 | 21 | 22 | 23 | {{hideNewPassword ? 'visibility' : 'visibility_off'}} 24 | 25 | 26 | 27 | Please enter a new password 28 | 29 | 30 | 31 | 32 | 34 | 35 | {{hideNewPassword ? 'visibility' : 'visibility_off'}} 36 | 37 | 38 | 39 | Please confirm your new password 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 |
48 | 49 |
50 | 51 | -------------------------------------------------------------------------------- /src/app/features/account/change-password/change-password.component.ts: -------------------------------------------------------------------------------- 1 | import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { NGXLogger } from 'ngx-logger'; 4 | import { AuthenticationService } from 'src/app/core/services/auth.service'; 5 | import { NotificationService } from 'src/app/core/services/notification.service'; 6 | import { SpinnerService } from 'src/app/core/services/spinner.service'; 7 | 8 | 9 | @Component({ 10 | selector: 'app-change-password', 11 | templateUrl: './change-password.component.html', 12 | styleUrls: ['./change-password.component.css'] 13 | }) 14 | export class ChangePasswordComponent implements OnInit { 15 | 16 | form!: UntypedFormGroup; 17 | hideCurrentPassword: boolean; 18 | hideNewPassword: boolean; 19 | currentPassword!: string; 20 | newPassword!: string; 21 | newPasswordConfirm!: string; 22 | disableSubmit!: boolean; 23 | 24 | constructor(private authService: AuthenticationService, 25 | private logger: NGXLogger, 26 | private spinnerService: SpinnerService, 27 | private notificationService: NotificationService) { 28 | 29 | this.hideCurrentPassword = true; 30 | this.hideNewPassword = true; 31 | } 32 | 33 | ngOnInit() { 34 | this.form = new UntypedFormGroup({ 35 | currentPassword: new UntypedFormControl('', Validators.required), 36 | newPassword: new UntypedFormControl('', Validators.required), 37 | newPasswordConfirm: new UntypedFormControl('', Validators.required), 38 | }); 39 | 40 | this.form.get('currentPassword')?.valueChanges 41 | .subscribe(val => { this.currentPassword = val; }); 42 | 43 | this.form.get('newPassword')?.valueChanges 44 | .subscribe(val => { this.newPassword = val; }); 45 | 46 | this.form.get('newPasswordConfirm')?.valueChanges 47 | .subscribe(val => { this.newPasswordConfirm = val; }); 48 | 49 | this.spinnerService.visibility.subscribe((value) => { 50 | this.disableSubmit = value; 51 | }); 52 | } 53 | 54 | changePassword() { 55 | 56 | if (this.newPassword !== this.newPasswordConfirm) { 57 | this.notificationService.openSnackBar('New passwords do not match.'); 58 | return; 59 | } 60 | 61 | const email = this.authService.getCurrentUser().email; 62 | 63 | this.authService.changePassword(email, this.currentPassword, this.newPassword) 64 | .subscribe( 65 | data => { 66 | this.logger.info(`User ${email} changed password.`); 67 | this.form.reset(); 68 | this.notificationService.openSnackBar('Your password has been changed.'); 69 | }, 70 | error => { 71 | this.notificationService.openSnackBar(error.error); 72 | } 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/app/features/account/profile-details/profile-details.component.css: -------------------------------------------------------------------------------- 1 | .profile-card { 2 | text-align: center; 3 | } -------------------------------------------------------------------------------- /src/app/features/account/profile-details/profile-details.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

5 | {{fullName}} 6 |

7 | 8 | 11 | 12 | 15 |
-------------------------------------------------------------------------------- /src/app/features/account/profile-details/profile-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AuthenticationService } from 'src/app/core/services/auth.service'; 3 | 4 | @Component({ 5 | selector: 'app-profile-details', 6 | templateUrl: './profile-details.component.html', 7 | styleUrls: ['./profile-details.component.css'] 8 | }) 9 | export class ProfileDetailsComponent implements OnInit { 10 | 11 | fullName: string = ""; 12 | email: string = ""; 13 | alias: string = ""; 14 | 15 | constructor(private authService: AuthenticationService) { } 16 | 17 | ngOnInit() { 18 | this.fullName = this.authService.getCurrentUser().fullName; 19 | this.email = this.authService.getCurrentUser().email; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/features/auth/auth-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { LoginComponent } from './login/login.component'; 5 | import { PasswordResetRequestComponent } from './password-reset-request/password-reset-request.component'; 6 | import { PasswordResetComponent } from './password-reset/password-reset.component'; 7 | 8 | const routes: Routes = [ 9 | { path: 'login', component: LoginComponent }, 10 | { path: 'password-reset-request', component: PasswordResetRequestComponent }, 11 | { path: 'password-reset', component: PasswordResetComponent } 12 | ]; 13 | 14 | @NgModule({ 15 | imports: [RouterModule.forChild(routes)], 16 | exports: [RouterModule] 17 | }) 18 | export class AuthRoutingModule { } 19 | -------------------------------------------------------------------------------- /src/app/features/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { AuthRoutingModule } from './auth-routing.module'; 5 | import { LoginComponent } from './login/login.component'; 6 | import { PasswordResetRequestComponent } from './password-reset-request/password-reset-request.component'; 7 | import { PasswordResetComponent } from './password-reset/password-reset.component'; 8 | import { SharedModule } from 'src/app/shared/shared.module'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | CommonModule, 13 | SharedModule, 14 | AuthRoutingModule 15 | ], 16 | declarations: [LoginComponent, PasswordResetRequestComponent, PasswordResetComponent] 17 | }) 18 | export class AuthModule { } 19 | -------------------------------------------------------------------------------- /src/app/features/auth/login/login.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/app/features/auth/login/login.component.css -------------------------------------------------------------------------------- /src/app/features/auth/login/login.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/features/auth/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { UntypedFormControl, Validators, UntypedFormGroup } from '@angular/forms'; 4 | import { Title } from '@angular/platform-browser'; 5 | import { AuthenticationService } from 'src/app/core/services/auth.service'; 6 | import { NotificationService } from 'src/app/core/services/notification.service'; 7 | 8 | @Component({ 9 | selector: 'app-login', 10 | templateUrl: './login.component.html', 11 | styleUrls: ['./login.component.css'] 12 | }) 13 | export class LoginComponent implements OnInit { 14 | 15 | loginForm!: UntypedFormGroup; 16 | loading!: boolean; 17 | 18 | constructor(private router: Router, 19 | private titleService: Title, 20 | private notificationService: NotificationService, 21 | private authenticationService: AuthenticationService) { 22 | } 23 | 24 | ngOnInit() { 25 | this.titleService.setTitle('angular-material-template - Login'); 26 | this.authenticationService.logout(); 27 | this.createForm(); 28 | } 29 | 30 | private createForm() { 31 | const savedUserEmail = localStorage.getItem('savedUserEmail'); 32 | 33 | this.loginForm = new UntypedFormGroup({ 34 | email: new UntypedFormControl(savedUserEmail, [Validators.required, Validators.email]), 35 | password: new UntypedFormControl('', Validators.required), 36 | rememberMe: new UntypedFormControl(savedUserEmail !== null) 37 | }); 38 | } 39 | 40 | login() { 41 | const email = this.loginForm.get('email')?.value; 42 | const password = this.loginForm.get('password')?.value; 43 | const rememberMe = this.loginForm.get('rememberMe')?.value; 44 | 45 | this.loading = true; 46 | this.authenticationService 47 | .login(email.toLowerCase(), password) 48 | .subscribe( 49 | data => { 50 | if (rememberMe) { 51 | localStorage.setItem('savedUserEmail', email); 52 | } else { 53 | localStorage.removeItem('savedUserEmail'); 54 | } 55 | this.router.navigate(['/']); 56 | }, 57 | error => { 58 | this.notificationService.openSnackBar(error.error); 59 | this.loading = false; 60 | } 61 | ); 62 | } 63 | 64 | resetPassword() { 65 | this.router.navigate(['/auth/password-reset-request']); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/features/auth/password-reset-request/password-reset-request.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/app/features/auth/password-reset-request/password-reset-request.component.css -------------------------------------------------------------------------------- /src/app/features/auth/password-reset-request/password-reset-request.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/features/auth/password-reset-request/password-reset-request.component.ts: -------------------------------------------------------------------------------- 1 | import { Router } from '@angular/router'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'; 4 | import { Title } from '@angular/platform-browser'; 5 | 6 | import { NotificationService } from 'src/app/core/services/notification.service'; 7 | import { AuthenticationService } from 'src/app/core/services/auth.service'; 8 | 9 | @Component({ 10 | selector: 'app-password-reset-request', 11 | templateUrl: './password-reset-request.component.html', 12 | styleUrls: ['./password-reset-request.component.css'] 13 | }) 14 | export class PasswordResetRequestComponent implements OnInit { 15 | 16 | private email!: string; 17 | form!: UntypedFormGroup; 18 | loading!: boolean; 19 | 20 | constructor(private authService: AuthenticationService, 21 | private notificationService: NotificationService, 22 | private titleService: Title, 23 | private router: Router) { } 24 | 25 | ngOnInit() { 26 | this.titleService.setTitle('angular-material-template - Password Reset Request'); 27 | 28 | this.form = new UntypedFormGroup({ 29 | email: new UntypedFormControl('', [Validators.required, Validators.email]) 30 | }); 31 | 32 | this.form.get('email')?.valueChanges 33 | .subscribe((val: string) => { this.email = val.toLowerCase(); }); 34 | } 35 | 36 | resetPassword() { 37 | this.loading = true; 38 | this.authService.passwordResetRequest(this.email) 39 | .subscribe( 40 | results => { 41 | this.router.navigate(['/auth/login']); 42 | this.notificationService.openSnackBar('Password verification mail has been sent to your email address.'); 43 | }, 44 | error => { 45 | this.loading = false; 46 | this.notificationService.openSnackBar(error.error); 47 | } 48 | ); 49 | } 50 | 51 | cancel() { 52 | this.router.navigate(['/']); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/features/auth/password-reset/password-reset.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/app/features/auth/password-reset/password-reset.component.css -------------------------------------------------------------------------------- /src/app/features/auth/password-reset/password-reset.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/features/auth/password-reset/password-reset.component.ts: -------------------------------------------------------------------------------- 1 | import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'; 2 | import { ActivatedRoute, Router, ParamMap } from '@angular/router'; 3 | import { Component, OnInit } from '@angular/core'; 4 | import { Title } from '@angular/platform-browser'; 5 | import { AuthenticationService } from 'src/app/core/services/auth.service'; 6 | import { NotificationService } from 'src/app/core/services/notification.service'; 7 | 8 | @Component({ 9 | selector: 'app-password-reset', 10 | templateUrl: './password-reset.component.html', 11 | styleUrls: ['./password-reset.component.css'] 12 | }) 13 | export class PasswordResetComponent implements OnInit { 14 | 15 | private token!: string; 16 | email!: string; 17 | form!: UntypedFormGroup; 18 | loading!: boolean; 19 | hideNewPassword: boolean; 20 | hideNewPasswordConfirm: boolean; 21 | 22 | constructor(private activeRoute: ActivatedRoute, 23 | private router: Router, 24 | private authService: AuthenticationService, 25 | private notificationService: NotificationService, 26 | private titleService: Title) { 27 | 28 | this.titleService.setTitle('angular-material-template - Password Reset'); 29 | this.hideNewPassword = true; 30 | this.hideNewPasswordConfirm = true; 31 | } 32 | 33 | ngOnInit() { 34 | this.activeRoute.queryParamMap.subscribe((params: ParamMap) => { 35 | this.token = params.get('token') + ''; 36 | this.email = params.get('email') + ''; 37 | 38 | if (!this.token || !this.email) { 39 | this.router.navigate(['/']); 40 | } 41 | }); 42 | 43 | this.form = new UntypedFormGroup({ 44 | newPassword: new UntypedFormControl('', Validators.required), 45 | newPasswordConfirm: new UntypedFormControl('', Validators.required) 46 | }); 47 | } 48 | 49 | resetPassword() { 50 | 51 | const password = this.form.get('newPassword')?.value; 52 | const passwordConfirm = this.form.get('newPasswordConfirm')?.value; 53 | 54 | if (password !== passwordConfirm) { 55 | this.notificationService.openSnackBar('Passwords do not match'); 56 | return; 57 | } 58 | 59 | this.loading = true; 60 | 61 | this.authService.passwordReset(this.email, this.token, password, passwordConfirm) 62 | .subscribe( 63 | () => { 64 | this.notificationService.openSnackBar('Your password has been changed.'); 65 | this.router.navigate(['/auth/login']); 66 | }, 67 | (error: any) => { 68 | this.notificationService.openSnackBar(error.error); 69 | this.loading = false; 70 | } 71 | ); 72 | } 73 | 74 | cancel() { 75 | this.router.navigate(['/']); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/features/customers/customer-list/customer-list.component.css: -------------------------------------------------------------------------------- 1 | table { 2 | width: 100%; 3 | } 4 | 5 | th.mat-sort-header-sorted { 6 | color: black; 7 | } -------------------------------------------------------------------------------- /src/app/features/customers/customer-list/customer-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |

Customers

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
No. {{element.position}} Name {{element.name}} Weight {{element.weight}} Symbol {{element.symbol}}
37 | 38 |
39 |
40 | 41 |
42 |
-------------------------------------------------------------------------------- /src/app/features/customers/customer-list/customer-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { MatSort } from '@angular/material/sort'; 3 | import { MatTableDataSource } from '@angular/material/table'; 4 | import { NGXLogger } from 'ngx-logger'; 5 | import { Title } from '@angular/platform-browser'; 6 | import { NotificationService } from 'src/app/core/services/notification.service'; 7 | 8 | export interface PeriodicElement { 9 | name: string; 10 | position: number; 11 | weight: number; 12 | symbol: string; 13 | } 14 | 15 | const ELEMENT_DATA: PeriodicElement[] = [ 16 | { position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H' }, 17 | { position: 2, name: 'Helium', weight: 4.0026, symbol: 'He' }, 18 | { position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li' }, 19 | { position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be' }, 20 | { position: 5, name: 'Boron', weight: 10.811, symbol: 'B' }, 21 | { position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C' }, 22 | { position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N' }, 23 | { position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O' }, 24 | { position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F' }, 25 | { position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne' }, 26 | ]; 27 | 28 | @Component({ 29 | selector: 'app-customer-list', 30 | templateUrl: './customer-list.component.html', 31 | styleUrls: ['./customer-list.component.css'] 32 | }) 33 | export class CustomerListComponent implements OnInit { 34 | displayedColumns: string[] = ['position', 'name', 'weight', 'symbol']; 35 | dataSource = new MatTableDataSource(ELEMENT_DATA); 36 | 37 | @ViewChild(MatSort, { static: true }) 38 | sort: MatSort = new MatSort; 39 | 40 | constructor( 41 | private logger: NGXLogger, 42 | private notificationService: NotificationService, 43 | private titleService: Title 44 | ) { } 45 | 46 | ngOnInit() { 47 | this.titleService.setTitle('angular-material-template - Customers'); 48 | this.logger.log('Customers loaded'); 49 | this.notificationService.openSnackBar('Customers loaded'); 50 | this.dataSource.sort = this.sort; 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/features/customers/customers-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { LayoutComponent } from 'src/app/shared/layout/layout.component'; 4 | 5 | import { CustomerListComponent } from './customer-list/customer-list.component'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: '', 10 | component: LayoutComponent, 11 | children: [ 12 | { path: '', component: CustomerListComponent }, 13 | ] 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forChild(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class CustomersRoutingModule { } 22 | -------------------------------------------------------------------------------- /src/app/features/customers/customers.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { CustomersRoutingModule } from './customers-routing.module'; 4 | import { SharedModule } from 'src/app/shared/shared.module'; 5 | import { CustomerListComponent } from './customer-list/customer-list.component'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | CustomersRoutingModule, 11 | SharedModule 12 | ], 13 | declarations: [ 14 | CustomerListComponent 15 | ] 16 | }) 17 | export class CustomersModule { } 18 | -------------------------------------------------------------------------------- /src/app/features/dashboard/dashboard-home/dashboard-home.component.css: -------------------------------------------------------------------------------- 1 | .single-cards { 2 | margin: 20px 0; 3 | } 4 | 5 | .single-card .mat-card-avatar { 6 | width: 50px; 7 | height: 50px; 8 | } 9 | 10 | .single-card .mat-icon { 11 | font-size: 55px; 12 | } 13 | 14 | .projects-card>mat-card-content { 15 | max-height: 400px; 16 | overflow: auto; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/features/dashboard/dashboard-home/dashboard-home.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |

Welcome back, {{currentUser.fullName}}!

7 | 8 |
9 | 10 |
11 |
12 | dashboard 13 |

This is the dashboard.

14 |
15 | 16 |
17 |
18 |
-------------------------------------------------------------------------------- /src/app/features/dashboard/dashboard-home/dashboard-home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NotificationService } from 'src/app/core/services/notification.service'; 3 | import { Title } from '@angular/platform-browser'; 4 | import { NGXLogger } from 'ngx-logger'; 5 | import { AuthenticationService } from 'src/app/core/services/auth.service'; 6 | 7 | @Component({ 8 | selector: 'app-dashboard-home', 9 | templateUrl: './dashboard-home.component.html', 10 | styleUrls: ['./dashboard-home.component.css'] 11 | }) 12 | export class DashboardHomeComponent implements OnInit { 13 | currentUser: any; 14 | 15 | constructor(private notificationService: NotificationService, 16 | private authService: AuthenticationService, 17 | private titleService: Title, 18 | private logger: NGXLogger) { 19 | } 20 | 21 | ngOnInit() { 22 | this.currentUser = this.authService.getCurrentUser(); 23 | this.titleService.setTitle('angular-material-template - Dashboard'); 24 | this.logger.log('Dashboard loaded'); 25 | 26 | setTimeout(() => { 27 | this.notificationService.openSnackBar('Welcome!'); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/features/dashboard/dashboard-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { LayoutComponent } from 'src/app/shared/layout/layout.component'; 4 | 5 | import { DashboardHomeComponent } from './dashboard-home/dashboard-home.component'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: '', 10 | component: LayoutComponent, 11 | children: [ 12 | { path: '', component: DashboardHomeComponent }, 13 | ] 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forChild(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class DashboardRoutingModule { } 22 | -------------------------------------------------------------------------------- /src/app/features/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 { DashboardHomeComponent } from './dashboard-home/dashboard-home.component'; 6 | import { SharedModule } from 'src/app/shared/shared.module'; 7 | 8 | @NgModule({ 9 | declarations: [DashboardHomeComponent], 10 | imports: [ 11 | CommonModule, 12 | DashboardRoutingModule, 13 | SharedModule 14 | ] 15 | }) 16 | export class DashboardModule { } 17 | -------------------------------------------------------------------------------- /src/app/features/icons/icons-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { LayoutComponent } from 'src/app/shared/layout/layout.component'; 4 | import { IconsComponent } from './icons/icons.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: LayoutComponent, 10 | children: [ 11 | { path: '', component: IconsComponent }, 12 | ] 13 | } 14 | ]; 15 | 16 | @NgModule({ 17 | imports: [RouterModule.forChild(routes)], 18 | exports: [RouterModule] 19 | }) 20 | export class IconsRoutingModule { } 21 | -------------------------------------------------------------------------------- /src/app/features/icons/icons.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { IconsRoutingModule } from './icons-routing.module'; 5 | import { IconsComponent } from './icons/icons.component'; 6 | import { SharedModule } from 'src/app/shared/shared.module'; 7 | 8 | 9 | @NgModule({ 10 | declarations: [IconsComponent], 11 | imports: [ 12 | CommonModule, 13 | SharedModule, 14 | IconsRoutingModule 15 | ] 16 | }) 17 | export class IconsModule { } 18 | -------------------------------------------------------------------------------- /src/app/features/icons/icons/icons.component.css: -------------------------------------------------------------------------------- 1 | iframe { 2 | width: 100%; 3 | height: 650px; 4 | } -------------------------------------------------------------------------------- /src/app/features/icons/icons/icons.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |

Icons

7 | 8 | 11 | 12 |
13 |
14 | 15 |
16 |
-------------------------------------------------------------------------------- /src/app/features/icons/icons/icons.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { IconsComponent } from './icons.component'; 4 | 5 | describe('IconsComponent', () => { 6 | let component: IconsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ IconsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(IconsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/icons/icons/icons.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-icons', 5 | templateUrl: './icons.component.html', 6 | styleUrls: ['./icons.component.css'] 7 | }) 8 | export class IconsComponent { 9 | 10 | constructor() { } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/features/typography/typography-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { LayoutComponent } from 'src/app/shared/layout/layout.component'; 4 | import { TypographyComponent } from './typography/typography.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: LayoutComponent, 10 | children: [ 11 | { path: '', component: TypographyComponent }, 12 | ] 13 | } 14 | ]; 15 | @NgModule({ 16 | imports: [RouterModule.forChild(routes)], 17 | exports: [RouterModule] 18 | }) 19 | export class TypographyRoutingModule { } 20 | -------------------------------------------------------------------------------- /src/app/features/typography/typography.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { TypographyRoutingModule } from './typography-routing.module'; 5 | import { TypographyComponent } from './typography/typography.component'; 6 | import { SharedModule } from 'src/app/shared/shared.module'; 7 | 8 | 9 | @NgModule({ 10 | declarations: [TypographyComponent], 11 | imports: [ 12 | CommonModule, 13 | SharedModule, 14 | TypographyRoutingModule 15 | ] 16 | }) 17 | export class TypographyModule { } 18 | -------------------------------------------------------------------------------- /src/app/features/typography/typography/typography.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/app/features/typography/typography/typography.component.css -------------------------------------------------------------------------------- /src/app/features/typography/typography/typography.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |

Typography

7 | 8 | 9 | Header 1 10 |

11 | The Life of Material Dashboard 12 |

13 | 14 | Header 2 15 |

16 | The Life of Material Dashboard 17 |

18 | 19 | Header 3 20 |

21 | The Life of Material Dashboard 22 |

23 | 24 | Header 4 25 |

26 | The Life of Material Dashboard 27 |

28 | 29 | Header 5 30 |
31 | The Life of Material Dashboard 32 |
33 | 34 | Header 6 35 |
36 | The Life of Material Dashboard 37 |
38 | 39 | Paragraph 40 |

41 | I will be the leader of a company that ends up being worth billions of dollars, because I got 42 | the answers. I understand culture. I am the nucleus. I think that’s a responsibility that I 43 | have, to push possibilities, to show people, this is the level that things could be at. 44 |

45 | 46 | Quote 47 |
48 |

49 | I will be the leader of a company that ends up being worth billions of dollars, because I 50 | got the answers. I understand culture. I am the nucleus. I think that’s a responsibility 51 | that I have, to push possibilities, to show people, this is the level that things could be 52 | at. 53 |

54 | 55 | Kanye West, Musician 56 | 57 |
58 | 59 |
60 |
61 | 62 |
63 |
-------------------------------------------------------------------------------- /src/app/features/typography/typography/typography.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TypographyComponent } from './typography.component'; 4 | 5 | describe('TypographyComponent', () => { 6 | let component: TypographyComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ TypographyComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TypographyComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/typography/typography/typography.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-typography', 5 | templateUrl: './typography.component.html', 6 | styleUrls: ['./typography.component.css'] 7 | }) 8 | export class TypographyComponent { 9 | 10 | constructor() { } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/features/users/user-list/user-list.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/app/features/users/user-list/user-list.component.css -------------------------------------------------------------------------------- /src/app/features/users/user-list/user-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |

Users

7 | 8 | 9 |
10 |
11 | people_outline 12 |

No users exist.

13 |
14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 |
-------------------------------------------------------------------------------- /src/app/features/users/user-list/user-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Title } from '@angular/platform-browser'; 3 | 4 | import { NGXLogger } from 'ngx-logger'; 5 | import { NotificationService } from 'src/app/core/services/notification.service'; 6 | 7 | @Component({ 8 | selector: 'app-user-list', 9 | templateUrl: './user-list.component.html', 10 | styleUrls: ['./user-list.component.css'] 11 | }) 12 | export class UserListComponent implements OnInit { 13 | 14 | constructor( 15 | private logger: NGXLogger, 16 | private notificationService: NotificationService, 17 | private titleService: Title 18 | ) { } 19 | 20 | ngOnInit() { 21 | this.titleService.setTitle('angular-material-template - Users'); 22 | this.logger.log('Users loaded'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/features/users/users-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { LayoutComponent } from 'src/app/shared/layout/layout.component'; 4 | 5 | import { UserListComponent } from './user-list/user-list.component'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: '', 10 | component: LayoutComponent, 11 | children: [ 12 | { path: '', component: UserListComponent }, 13 | ] 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forChild(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class UsersRoutingModule { } 22 | -------------------------------------------------------------------------------- /src/app/features/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { UsersRoutingModule } from './users-routing.module'; 5 | import { UserListComponent } from './user-list/user-list.component'; 6 | import { SharedModule } from 'src/app/shared/shared.module'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, 11 | SharedModule, 12 | UsersRoutingModule 13 | ], 14 | declarations: [UserListComponent] 15 | }) 16 | export class UsersModule { } 17 | -------------------------------------------------------------------------------- /src/app/shared/confirm-dialog/confirm-dialog.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/app/shared/confirm-dialog/confirm-dialog.component.css -------------------------------------------------------------------------------- /src/app/shared/confirm-dialog/confirm-dialog.component.html: -------------------------------------------------------------------------------- 1 |

2 | {{title}} 3 |

4 | 5 |
6 |

{{message}}

7 |
8 | 9 |
10 | 11 | 12 |
-------------------------------------------------------------------------------- /src/app/shared/confirm-dialog/confirm-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'app-confirm-dialog', 6 | templateUrl: './confirm-dialog.component.html', 7 | styleUrls: ['./confirm-dialog.component.css'] 8 | }) 9 | export class ConfirmDialogComponent { 10 | title: string; 11 | message: string; 12 | 13 | constructor(public dialogRef: MatDialogRef, 14 | @Inject(MAT_DIALOG_DATA) public data: ConfirmDialogModel) { 15 | this.title = data.title; 16 | this.message = data.message; 17 | } 18 | 19 | onConfirm(): void { 20 | this.dialogRef.close(true); 21 | } 22 | 23 | onDismiss(): void { 24 | this.dialogRef.close(false); 25 | } 26 | } 27 | 28 | /** 29 | * Class to represent confirm dialog model. 30 | * 31 | * It has been kept here to keep it as part of shared component. 32 | */ 33 | export class ConfirmDialogModel { 34 | 35 | constructor(public title: string, public message: string) { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/shared/content-placeholder-animation/content-placeholder-animation.component.css: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 220px; 3 | border-radius: 5px; 4 | width: 100%; 5 | } 6 | 7 | .line { 8 | height: 12px; 9 | margin: 10px; 10 | animation: pulse 1s infinite ease-in-out; 11 | -webkit-animation: pulse 1s infinite ease-in-out; 12 | } 13 | 14 | .container div:nth-child(1) { 15 | width: 20%; 16 | } 17 | 18 | .container div:nth-child(2) { 19 | width: 25%; 20 | } 21 | 22 | .container div:nth-child(3) { 23 | width: 15%; 24 | } 25 | 26 | .container div:nth-child(4) { 27 | width: 40%; 28 | } 29 | 30 | .container div:nth-child(5) { 31 | width: 30%; 32 | } 33 | 34 | .container div:nth-child(6) { 35 | width: 25%; 36 | } 37 | 38 | .container div:nth-child(7) { 39 | width: 35%; 40 | } 41 | 42 | .container div:nth-child(8) { 43 | width: 15%; 44 | } 45 | 46 | .container div:nth-child(9) { 47 | width: 25%; 48 | } 49 | 50 | .container div:nth-child(10) { 51 | width: 10%; 52 | } 53 | 54 | @keyframes pulse { 55 | 0% { 56 | background-color: rgba(165, 165, 165, .1); 57 | } 58 | 50% { 59 | background-color: rgba(165, 165, 165, .3); 60 | } 61 | 100% { 62 | background-color: rgba(165, 165, 165, .1); 63 | } 64 | } 65 | 66 | @-webkit-keyframes pulse { 67 | 0% { 68 | background-color: rgba(165, 165, 165, .1); 69 | } 70 | 50% { 71 | background-color: rgba(165, 165, 165, .3); 72 | } 73 | 100% { 74 | background-color: rgba(165, 165, 165, .1); 75 | } 76 | } -------------------------------------------------------------------------------- /src/app/shared/content-placeholder-animation/content-placeholder-animation.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
-------------------------------------------------------------------------------- /src/app/shared/content-placeholder-animation/content-placeholder-animation.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-content-placeholder-animation', 5 | templateUrl: './content-placeholder-animation.component.html', 6 | styleUrls: ['./content-placeholder-animation.component.css'] 7 | }) 8 | export class ContentPlaceholderAnimationComponent { 9 | 10 | constructor() { } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/layout/layout.component.css: -------------------------------------------------------------------------------- 1 | .navbar-spacer { 2 | flex: 1 1 auto; 3 | } 4 | 5 | .navbar { 6 | z-index: 2; 7 | } 8 | 9 | .navbar-brand { 10 | text-decoration: none; 11 | color: white; 12 | } 13 | 14 | .navbar-container { 15 | display: flex; 16 | flex-direction: column; 17 | position: absolute; 18 | top: 0; 19 | bottom: 0; 20 | left: 0; 21 | right: 0; 22 | } 23 | 24 | .navbar-is-mobile .navbar { 25 | position: fixed; 26 | /* Make sure the toolbar will stay on top of the content as it scrolls past. */ 27 | z-index: 2; 28 | } 29 | 30 | .navbar-sidenav-container { 31 | /* When the sidenav is not fixed, stretch the sidenav container to fill the available space. This 32 | causes `` to act as our scrolling element for desktop layouts. */ 33 | flex: 1; 34 | } 35 | 36 | .navbar-is-mobile .navbar-sidenav-container { 37 | /* When the sidenav is fixed, don't constrain the height of the sidenav container. This allows the 38 | `` to be our scrolling element for mobile layouts. */ 39 | flex: 1 0 auto; 40 | } 41 | 42 | /*Set sidenav width*/ 43 | 44 | mat-sidenav { 45 | min-width: 180px !important; 46 | border-right: 1px solid #eee; 47 | box-shadow: 6px 0 6px rgba(0, 0, 0, .1); 48 | /* background-color:rgb(63, 81, 181); */ 49 | } 50 | 51 | /* Set height of wrapper to stop content from moving up & down */ 52 | 53 | .progress-bar-container { 54 | height: 5px; 55 | } 56 | 57 | a.mat-list-item.active { 58 | background: rgba(0, 0, 0, .04); 59 | } 60 | 61 | #push-bottom { 62 | position: absolute; 63 | bottom: 0; 64 | } -------------------------------------------------------------------------------- /src/app/shared/layout/layout.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/shared/layout/layout.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectorRef, OnDestroy, AfterViewInit } from '@angular/core'; 2 | import { MediaMatcher } from '@angular/cdk/layout'; 3 | import { timer } from 'rxjs'; 4 | import { Subscription } from 'rxjs'; 5 | 6 | import { AuthenticationService } from 'src/app/core/services/auth.service'; 7 | import { SpinnerService } from '../../core/services/spinner.service'; 8 | import { AuthGuard } from 'src/app/core/guards/auth.guard'; 9 | 10 | @Component({ 11 | selector: 'app-layout', 12 | templateUrl: './layout.component.html', 13 | styleUrls: ['./layout.component.css'] 14 | }) 15 | export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit { 16 | 17 | private _mobileQueryListener: () => void; 18 | mobileQuery: MediaQueryList; 19 | showSpinner: boolean = false; 20 | userName: string = ""; 21 | isAdmin: boolean = false; 22 | 23 | private autoLogoutSubscription: Subscription = new Subscription; 24 | 25 | constructor(private changeDetectorRef: ChangeDetectorRef, 26 | private media: MediaMatcher, 27 | public spinnerService: SpinnerService, 28 | private authService: AuthenticationService, 29 | private authGuard: AuthGuard) { 30 | 31 | this.mobileQuery = this.media.matchMedia('(max-width: 1000px)'); 32 | this._mobileQueryListener = () => changeDetectorRef.detectChanges(); 33 | // tslint:disable-next-line: deprecation 34 | this.mobileQuery.addListener(this._mobileQueryListener); 35 | } 36 | 37 | ngOnInit(): void { 38 | const user = this.authService.getCurrentUser(); 39 | 40 | this.isAdmin = user.isAdmin; 41 | this.userName = user.fullName; 42 | 43 | // Auto log-out subscription 44 | const timer$ = timer(2000, 5000); 45 | this.autoLogoutSubscription = timer$.subscribe(() => { 46 | this.authGuard.canActivate(); 47 | }); 48 | } 49 | 50 | ngOnDestroy(): void { 51 | // tslint:disable-next-line: deprecation 52 | this.mobileQuery.removeListener(this._mobileQueryListener); 53 | this.autoLogoutSubscription.unsubscribe(); 54 | } 55 | 56 | ngAfterViewInit(): void { 57 | this.changeDetectorRef.detectChanges(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/shared/mocks/spinner-consumer.ts: -------------------------------------------------------------------------------- 1 | import { SpinnerService } from '../../core/services/spinner.service'; 2 | 3 | export class SpinnerConsumer { 4 | isBusy = false; 5 | 6 | constructor(private spinnerService: SpinnerService) { 7 | this.spinnerService.visibility.subscribe((value: boolean) => { 8 | this.isBusy = value; 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shared/pipes/limit-to.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { LimitToPipe } from './limit-to.pipe'; 2 | 3 | describe('LimitToPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new LimitToPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | 9 | it('returns same value when length is shorter than specified', () => { 10 | const pipe = new LimitToPipe(); 11 | const result = pipe.transform('some text', '20'); 12 | expect(result).toBe('some text'); 13 | }); 14 | 15 | it('returns limited value when length is longer than specified', () => { 16 | const pipe = new LimitToPipe(); 17 | const result = pipe.transform('some text', '3'); 18 | expect(result).toBe('som..'); 19 | }); 20 | 21 | it('returns empty string when value is empty', () => { 22 | const pipe = new LimitToPipe(); 23 | const result = pipe.transform('', '3'); 24 | expect(result).toBe(''); 25 | }); 26 | 27 | it('returns empty string when value is null', () => { 28 | const pipe = new LimitToPipe(); 29 | const result = pipe.transform(null, '3'); 30 | expect(result).toBe(''); 31 | }); 32 | 33 | it('returns empty string when value is undefined', () => { 34 | const pipe = new LimitToPipe(); 35 | const result = pipe.transform(undefined, '3'); 36 | expect(result).toBe(''); 37 | }); 38 | 39 | it('returns empty string when limit to is 0', () => { 40 | const pipe = new LimitToPipe(); 41 | const result = pipe.transform(undefined, '0'); 42 | expect(result).toBe(''); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/app/shared/pipes/limit-to.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'limitTo' 5 | }) 6 | export class LimitToPipe implements PipeTransform { 7 | 8 | transform(value: string, limitTo: string): string { 9 | 10 | if (value === undefined || value === null) { 11 | return ''; 12 | } 13 | 14 | const limit = limitTo ? parseInt(limitTo, 10) : 10; 15 | const trail = '..'; 16 | 17 | return value.length > limit ? value.substring(0, limit) + trail : value; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/app/shared/pipes/local-date.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { LocalDatePipe } from './local-date.pipe'; 2 | 3 | describe('LocalDatePipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new LocalDatePipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | 9 | it('returns valid date given utc', () => { 10 | const date = new Date('2018-05-04T08:17:57.8979116Z'); 11 | 12 | const pipe = new LocalDatePipe(); 13 | const result = pipe.transform(date, 'DD MMM YYYY HH:mm'); 14 | expect(result).toBe('04 May 2018 09:17'); 15 | }); 16 | 17 | it('returns empty string when date is null', () => { 18 | const date = null; 19 | 20 | const pipe = new LocalDatePipe(); 21 | const result = pipe.transform(date, 'DD MMM YYYY HH:mm'); 22 | expect(result).toBe(''); 23 | }); 24 | 25 | it('returns empty string when format is null', () => { 26 | const date = new Date('2018-05-04T08:17:57.8979116Z'); 27 | 28 | const pipe = new LocalDatePipe(); 29 | const result = pipe.transform(date, null); 30 | expect(result).toBe(''); 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /src/app/shared/pipes/local-date.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import * as moment from 'moment'; 3 | 4 | @Pipe({ 5 | name: 'localDate' 6 | }) 7 | export class LocalDatePipe implements PipeTransform { 8 | 9 | transform(value: Date, args: string): string { 10 | if (!value || !args) { 11 | return ''; 12 | } 13 | return moment.utc(value).local().format(args); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/app/shared/pipes/yes-no.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { YesNoPipe } from './yes-no.pipe'; 2 | 3 | describe('YesNoPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new YesNoPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | 9 | it('returns Yes given true', () => { 10 | const pipe = new YesNoPipe(); 11 | 12 | const result = pipe.transform(true); 13 | 14 | expect(result).toBe('Yes'); 15 | }); 16 | 17 | it('returns No given false', () => { 18 | const pipe = new YesNoPipe(); 19 | 20 | const result = pipe.transform(false); 21 | 22 | expect(result).toBe('No'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/shared/pipes/yes-no.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'yesNo' 5 | }) 6 | export class YesNoPipe implements PipeTransform { 7 | 8 | transform(value: boolean): string { 9 | return value ? 'Yes' : 'No'; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { FlexLayoutModule } from '@angular/flex-layout'; 4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 5 | 6 | import { CustomMaterialModule } from '../custom-material/custom-material.module'; 7 | import { LimitToPipe } from './pipes/limit-to.pipe'; 8 | import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component'; 9 | import { ContentPlaceholderAnimationComponent } from './content-placeholder-animation/content-placeholder-animation.component'; 10 | import { LocalDatePipe } from './pipes/local-date.pipe'; 11 | import { YesNoPipe } from './pipes/yes-no.pipe'; 12 | import { LayoutComponent } from './layout/layout.component'; 13 | 14 | @NgModule({ 15 | imports: [ 16 | RouterModule, 17 | CustomMaterialModule, 18 | FormsModule, 19 | ReactiveFormsModule, 20 | FlexLayoutModule, 21 | ], 22 | declarations: [ 23 | ConfirmDialogComponent, 24 | ContentPlaceholderAnimationComponent, 25 | LimitToPipe, 26 | LocalDatePipe, 27 | YesNoPipe, 28 | LayoutComponent 29 | ], 30 | exports: [ 31 | FormsModule, 32 | ReactiveFormsModule, 33 | FlexLayoutModule, 34 | CustomMaterialModule, 35 | LimitToPipe, 36 | ConfirmDialogComponent, 37 | ContentPlaceholderAnimationComponent, 38 | LocalDatePipe, 39 | YesNoPipe 40 | ] 41 | }) 42 | export class SharedModule { } 43 | -------------------------------------------------------------------------------- /src/app/shared/validators/autocompleteSelectionValidator.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl } from '@angular/forms'; 2 | 3 | export function AutocompleteSelectionValidator(control: AbstractControl) { 4 | const selection = control.value; 5 | 6 | if (selection && typeof selection === 'string') { 7 | return { incorrect: true }; 8 | } 9 | 10 | return null; 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/favicon/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/android-icon-144x144.png -------------------------------------------------------------------------------- /src/assets/favicon/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/android-icon-192x192.png -------------------------------------------------------------------------------- /src/assets/favicon/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/android-icon-36x36.png -------------------------------------------------------------------------------- /src/assets/favicon/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/android-icon-48x48.png -------------------------------------------------------------------------------- /src/assets/favicon/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/android-icon-72x72.png -------------------------------------------------------------------------------- /src/assets/favicon/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/android-icon-96x96.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/apple-icon-114x114.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/apple-icon-120x120.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/apple-icon-144x144.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/apple-icon-152x152.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/apple-icon-180x180.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/apple-icon-57x57.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/apple-icon-60x60.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/apple-icon-72x72.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/apple-icon-76x76.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/apple-icon-precomposed.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/apple-icon.png -------------------------------------------------------------------------------- /src/assets/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /src/assets/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/favicon.ico -------------------------------------------------------------------------------- /src/assets/favicon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /src/assets/favicon/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/ms-icon-144x144.png -------------------------------------------------------------------------------- /src/assets/favicon/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/ms-icon-150x150.png -------------------------------------------------------------------------------- /src/assets/favicon/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/ms-icon-310x310.png -------------------------------------------------------------------------------- /src/assets/favicon/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/favicon/ms-icon-70x70.png -------------------------------------------------------------------------------- /src/assets/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umutesen/angular-material-template/8d54e5febbd7b19503aefa7b23067da3d39c1891/src/assets/images/user.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | import { NgxLoggerLevel } from 'ngx-logger'; 2 | 3 | export const environment = { 4 | production: true, 5 | logLevel: NgxLoggerLevel.OFF, 6 | serverLogLevel: NgxLoggerLevel.ERROR 7 | }; 8 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | import { NgxLoggerLevel } from 'ngx-logger'; 2 | 3 | // The file contents for the current environment will overwrite these during build. 4 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 5 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 6 | // The list of which env maps to which file can be found in `.angular-cli.json`. 7 | 8 | export const environment = { 9 | production: false, 10 | logLevel: NgxLoggerLevel.TRACE, 11 | serverLogLevel: NgxLoggerLevel.OFF 12 | }; 13 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | angular-material-template 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /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.error(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 recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import "~@angular/material/prebuilt-themes/indigo-pink.css"; 2 | 3 | body { 4 | margin: 0; 5 | font-family: Roboto, "Helvetica Neue", sans-serif; 6 | font-size: 14px; 7 | } 8 | 9 | .full-width { 10 | width: 100%; 11 | } 12 | 13 | .container { 14 | margin-top: 10px; 15 | margin-bottom: 10px; 16 | } 17 | 18 | .spinner { 19 | left: 49%; 20 | position: fixed !important; 21 | top: 15%; 22 | z-index: 9999; 23 | } 24 | 25 | .text-center { 26 | text-align: center; 27 | } 28 | 29 | .text-right { 30 | text-align: right; 31 | } 32 | 33 | .login-container { 34 | height: 80vh; 35 | } 36 | 37 | .login-actions { 38 | margin-left: unset !important; 39 | } 40 | 41 | .mat-list-item, 42 | .mat-menu-item { 43 | font-size: 14px !important; 44 | } 45 | 46 | .command-wrapper button { 47 | margin-left: 10px; 48 | margin-bottom: 5px; 49 | } 50 | 51 | .mat-dialog-title { 52 | margin: 0 !important; 53 | } 54 | 55 | .mat-column-actions { 56 | flex: 0 0 60px; 57 | } 58 | 59 | .mat-table .mat-row:hover { 60 | background-color: rgba(0, 0, 0, 0.075); 61 | } 62 | 63 | .mat-table td.mat-cell a:hover { 64 | text-decoration: underline; 65 | } 66 | 67 | .mat-table td.mat-cell:first-child, 68 | .mat-table td.mat-footer-cell:first-child, 69 | .mat-table th.mat-header-cell:first-child { 70 | padding-left: 20px !important; 71 | } 72 | 73 | .mat-table td.mat-cell:last-child, 74 | .mat-table td.mat-footer-cell:last-child, 75 | .mat-table th.mat-header-cell:last-child { 76 | padding-right: 10px !important; 77 | } 78 | 79 | .mat-table .mat-cell a { 80 | color: inherit; 81 | text-decoration: none; 82 | } 83 | 84 | .no-records { 85 | padding-top: 20px; 86 | padding-bottom: 20px; 87 | } 88 | 89 | .refresh-button { 90 | margin-top: 8px !important; 91 | } 92 | 93 | .no-records .mat-icon { 94 | width: 130px; 95 | height: 130px; 96 | font-size: 130px; 97 | color: #c7c7c7; 98 | } 99 | 100 | p .mat-icon { 101 | vertical-align: middle; 102 | } 103 | 104 | .mat-slide-toggle.full-width { 105 | margin-top: 15px; 106 | margin-bottom: 15px; 107 | } 108 | 109 | .mat-tooltip { 110 | font-size: 14px; 111 | } 112 | 113 | .fixed-fab { 114 | position: fixed !important; 115 | right: 2%; 116 | bottom: 3%; 117 | } 118 | 119 | /* Highlight on hover */ 120 | .submitted-timesheet-day:not(.mat-calendar-body-disabled):hover>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected), 121 | /* Highlight */ 122 | .submitted-timesheet-day>div:not(.mat-calendar-body-selected) { 123 | border-radius: 999px; 124 | background-color: rgba(14, 152, 5, 0.25); 125 | } 126 | 127 | .table-wrapper { 128 | overflow: auto; 129 | height: calc(100vh - 270px); 130 | max-height: 450px; 131 | /* padding: 10px 5px; */ 132 | } 133 | 134 | .animate { 135 | animation-name: show; 136 | animation-duration: 1s; 137 | animation-fill-mode: forwards; 138 | } 139 | 140 | @keyframes show { 141 | from { 142 | opacity: 0; 143 | } 144 | 145 | to { 146 | opacity: 1; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /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/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: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | (id: string): T; 13 | keys(): string[]; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting(), 21 | ); 22 | 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().map(context); 27 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "es2020", 20 | "module": "es2020", 21 | "lib": [ 22 | "es2020", 23 | "dom" 24 | ] 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------