├── .browserslistrc ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml └── dependabot.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── README.md ├── angular.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.module.ts │ ├── core │ │ ├── components │ │ │ ├── app │ │ │ │ ├── app.component.spec.ts │ │ │ │ └── app.component.ts │ │ │ └── network-status │ │ │ │ └── network-status.component.ts │ │ ├── guards │ │ │ ├── auth.guard.ts │ │ │ ├── guest.guard.ts │ │ │ ├── role.guard.ts │ │ │ ├── user-data.guard.ts │ │ │ └── user.roles.ts │ │ ├── interfaces │ │ │ ├── notification.interface.ts │ │ │ └── session.interface.ts │ │ ├── routes │ │ │ ├── app-routing.module.ts │ │ │ └── app.routing.ts │ │ └── store │ │ │ ├── app.store.ts │ │ │ ├── notification │ │ │ ├── notification.actions.ts │ │ │ ├── notification.reducer.ts │ │ │ └── notification.selectors.ts │ │ │ ├── router.selectors.ts │ │ │ └── session │ │ │ ├── session.actions.ts │ │ │ ├── session.reducer.ts │ │ │ └── session.selectors.ts │ ├── modules │ │ ├── authentication │ │ │ ├── authentication.module.ts │ │ │ ├── interceptors │ │ │ │ └── auth.interceptor.ts │ │ │ ├── interfaces │ │ │ │ ├── credentials.interface.ts │ │ │ │ └── state.interface.ts │ │ │ ├── pages │ │ │ │ ├── forgot-password │ │ │ │ │ ├── forgot-password.component.html │ │ │ │ │ └── forgot-password.component.ts │ │ │ │ ├── login │ │ │ │ │ ├── login.component.html │ │ │ │ │ └── login.component.ts │ │ │ │ └── reset-password │ │ │ │ │ ├── reset-password.component.html │ │ │ │ │ └── reset-password.component.ts │ │ │ ├── routes │ │ │ │ └── authenticate.routes.ts │ │ │ ├── services │ │ │ │ ├── auth.service.ts │ │ │ │ └── local-storage.service.ts │ │ │ └── store │ │ │ │ ├── auth.actions.ts │ │ │ │ ├── auth.effects.ts │ │ │ │ ├── auth.reducer.ts │ │ │ │ └── auth.selectors.ts │ │ ├── dashboard │ │ │ ├── dashboard.module.ts │ │ │ ├── navigation │ │ │ │ └── admin.menu.ts │ │ │ ├── pages │ │ │ │ └── dashboard │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ └── dashboard.component.ts │ │ │ └── routes │ │ │ │ └── dashboard.routes.ts │ │ └── user │ │ │ └── interfaces │ │ │ └── user.interface.ts │ └── shared │ │ ├── components │ │ ├── alert │ │ │ ├── alert.module.ts │ │ │ ├── error.component.ts │ │ │ └── success.component.ts │ │ ├── buttons │ │ │ ├── button.module.ts │ │ │ ├── default.component.ts │ │ │ ├── link.component.ts │ │ │ └── primary.component.ts │ │ ├── headings │ │ │ ├── heading.module.ts │ │ │ ├── page-heading-with-action.component.ts │ │ │ └── page-heading.component.ts │ │ ├── inputs │ │ │ ├── inputs.module.ts │ │ │ └── overlaping-label │ │ │ │ ├── overlaping-label.component.html │ │ │ │ └── overlaping-label.component.ts │ │ ├── notifications │ │ │ ├── notifications.module.ts │ │ │ └── simple-notification.component.ts │ │ ├── skeletons │ │ │ ├── backdrop-loader.component.ts │ │ │ ├── skeleton.component.ts │ │ │ └── skeleton.module.ts │ │ ├── snippets │ │ │ ├── decrease.component.ts │ │ │ ├── increase.component.ts │ │ │ └── snippet.module.ts │ │ └── textarea │ │ │ ├── simple │ │ │ ├── simple.component.html │ │ │ └── simple.component.ts │ │ │ └── textarea.module.ts │ │ ├── directives │ │ └── click-outside.directive.ts │ │ ├── helpers │ │ └── meta-data.ts │ │ ├── interceptors │ │ ├── errors.interceptor.ts │ │ └── http-loading.interceptor.ts │ │ ├── interfaces │ │ ├── menu.ts │ │ ├── response.interface.ts │ │ ├── state.interfaces.ts │ │ └── values.interface.ts │ │ ├── pipes │ │ ├── status-color.pipe.ts │ │ └── status-value.pipe.ts │ │ ├── rules │ │ ├── password.rule.ts │ │ └── whitespace.rule.ts │ │ ├── services │ │ └── seo.service.ts │ │ ├── shared.module.ts │ │ └── themes │ │ ├── components │ │ ├── header │ │ │ ├── header.component.html │ │ │ └── header.component.ts │ │ ├── logo │ │ │ └── logo.component.ts │ │ └── sidebar │ │ │ ├── sidebar.component.html │ │ │ ├── sidebar.component.scss │ │ │ └── sidebar.component.ts │ │ ├── layouts │ │ ├── auth │ │ │ ├── auth.component.html │ │ │ └── auth.component.ts │ │ └── cpanel │ │ │ ├── cpanel.component.html │ │ │ └── cpanel.component.ts │ │ ├── pages │ │ └── not-found │ │ │ ├── not-found.component.html │ │ │ └── not-found.component.ts │ │ ├── services │ │ ├── loading.service.ts │ │ └── sidebar.service.ts │ │ └── theme.module.ts ├── assets │ ├── .gitkeep │ ├── favicons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-384x384.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-150x150.png │ │ ├── safari-pinned-tab.svg │ │ └── site.webmanifest │ ├── images │ │ ├── .gitkeep │ │ └── wallpaper.jpg │ ├── json │ │ └── .gitkeep │ └── svg │ │ └── .gitkeep ├── env.d.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── locales │ ├── messages.en-US.xlf │ └── messages.xlf ├── main.ts ├── polyfills.ts ├── sass │ ├── _base.scss │ ├── _material.scss │ └── _plugin.scss ├── styles.scss └── test.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NG_APP_NAME="Admin Cpanel" 2 | NG_APP_DEBUG=true 3 | NG_APP_VERSION="v1.0.0" 4 | NG_APP_BASE_URL=https://cpanel.mon-site.com 5 | NG_APP_API_URL=https://mon-site.test/api 6 | NG_APP_API_VERSION=v2 7 | NG_APP_SENTRY_DSN= 8 | NG_APP_SENTRY_TRACES_SAMPLE_RATE=1.0 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | dist 4 | e2e/** 5 | karma.conf.js 6 | commitlint.config.js -------------------------------------------------------------------------------- /.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 | "plugin:prettier/recommended" 21 | ], 22 | "rules": { 23 | "@angular-eslint/directive-selector": [ 24 | "error", 25 | { 26 | "type": "attribute", 27 | "prefix": "cosna", 28 | "style": "camelCase" 29 | } 30 | ], 31 | "@angular-eslint/component-selector": [ 32 | "error", 33 | { 34 | "type": "element", 35 | "prefix": "cosna", 36 | "style": "kebab-case" 37 | } 38 | ] 39 | } 40 | }, 41 | { 42 | "files": [ 43 | "*.html" 44 | ], 45 | "extends": [ 46 | "plugin:@angular-eslint/template/recommended" 47 | ], 48 | "rules": {} 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://laravel.cm/sponsors 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # Maintain dependencies for npm 9 | - package-ecosystem: 'npm' 10 | directory: '/' 11 | schedule: 12 | interval: 'weekly' 13 | day: 'monday' 14 | 15 | # Maintain dependencies for GitHub Actions 16 | - package-ecosystem: 'github-actions' 17 | directory: '/' 18 | schedule: 19 | interval: 'weekly' 20 | day: 'monday' 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | /.vscode 17 | .project 18 | .classpath 19 | .c9/ 20 | *.launch 21 | .settings/ 22 | *.sublime-workspace 23 | 24 | # Visual Studio Code 25 | .vscode/* 26 | !.vscode/settings.json 27 | !.vscode/tasks.json 28 | !.vscode/launch.json 29 | !.vscode/extensions.json 30 | .history/* 31 | 32 | # Miscellaneous 33 | /.angular/cache 34 | .sass-cache/ 35 | /connect.lock 36 | /coverage 37 | /libpeerconnection.log 38 | testem.log 39 | /typings 40 | 41 | # System files 42 | .DS_Store 43 | Thumbs.db 44 | 45 | # Config 46 | .env 47 | .env.local 48 | .env.test 49 | .env.prod 50 | 51 | # Docker Construction 52 | Dockerfile 53 | docker-compose* 54 | 55 | # Continious integration 56 | # .gitlab-ci.yml 57 | .gitlab-ci.dockerv_version.yml -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | # Config 45 | .env 46 | .env.local -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "semi": true, 6 | "bracketSpacing": true, 7 | "arrowParens": "avoid", 8 | "trailingComma": "es5", 9 | "bracketSameLine": true, 10 | "printWidth": 80 11 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "pwa-chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Admin Cpanel 2 | 3 | 🚀 Admin panel boilerplate to quickly scaffold any large scale enterprise application for Angular. Built with the Tailwind, fully customizable and developer-first. 4 | 5 | > This project use Angular 13+ 6 | 7 | ### ⏳ Installation 8 | 9 | Clone the repo locally: 10 | ```bash 11 | git clone https://github.com/laravelcm/angular-admin-panel.git cpanel.angular && cd cpanel.angular 12 | ``` 13 | 14 | Install NPM dependencies: 15 | ```bash 16 | npm i 17 | ``` 18 | 19 | Setup configuration: 20 | ```bash 21 | cp .env.example .env 22 | ``` 23 | 24 | ### 🖐 Requirements 25 | 26 | **Node:** 27 | 28 | - NodeJS >= 14 <= 18 29 | - NPM >= 6.x 30 | 31 | ### Features 32 | 33 | - **Modern Admin Panel:** Elegant, entirely customizable and a fully extensible admin panel. 34 | - **Secure by default:** Roles, Guard and more. 35 | - **Authentication Module:** All authentication features enabled (Login, Reset Password, Forgot Password, Logout). 36 | - **Shared module:** All reusabled customize components. 37 | - **Theme module:** Theme management with Auth and Cpanel Layout. -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": "28bf0e25-7262-4cd2-8008-6af8c18cc349" 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "admin-cpanel": { 10 | "projectType": "application", 11 | "schematics": { 12 | "@schematics/angular:application": { 13 | "style": "scss", 14 | "skipTests": true, 15 | "strict": true 16 | }, 17 | "@schematics/angular:component": { 18 | "style": "scss", 19 | "skipTests": true 20 | } 21 | }, 22 | "i18n": { 23 | "sourceLocale": "fr-FR", 24 | "locales": { 25 | "en": { 26 | "translation": "src/locales/messages.en-US.xlf", 27 | "baseHref": "/en/" 28 | } 29 | } 30 | }, 31 | "root": "", 32 | "sourceRoot": "src", 33 | "prefix": "admin", 34 | "architect": { 35 | "build": { 36 | "builder": "@ngx-env/builder:browser", 37 | "options": { 38 | "outputPath": "dist/admin-cpanel", 39 | "index": "src/index.html", 40 | "main": "src/main.ts", 41 | "polyfills": "src/polyfills.ts", 42 | "tsConfig": "tsconfig.app.json", 43 | "inlineStyleLanguage": "scss", 44 | "assets": [ 45 | "src/assets" 46 | ], 47 | "styles": [ 48 | "src/styles.scss" 49 | ], 50 | "scripts": [] 51 | }, 52 | "configurations": { 53 | "production": { 54 | "budgets": [ 55 | { 56 | "type": "initial", 57 | "maximumWarning": "500kb", 58 | "maximumError": "1mb" 59 | }, 60 | { 61 | "type": "anyComponentStyle", 62 | "maximumWarning": "2kb", 63 | "maximumError": "4kb" 64 | } 65 | ], 66 | "fileReplacements": [ 67 | { 68 | "replace": "src/environments/environment.ts", 69 | "with": "src/environments/environment.prod.ts" 70 | } 71 | ], 72 | "outputHashing": "all" 73 | }, 74 | "development": { 75 | "buildOptimizer": false, 76 | "optimization": false, 77 | "vendorChunk": true, 78 | "extractLicenses": false, 79 | "sourceMap": true, 80 | "namedChunks": true 81 | }, 82 | "en": { 83 | "localize": ["en"] 84 | } 85 | }, 86 | "defaultConfiguration": "production" 87 | }, 88 | "serve": { 89 | "builder": "@ngx-env/builder:dev-server", 90 | "configurations": { 91 | "production": { 92 | "browserTarget": "admin-cpanel:build:production" 93 | }, 94 | "development": { 95 | "browserTarget": "admin-cpanel:build:development" 96 | }, 97 | "en": { 98 | "browserTarget": "admin-cpanel:build:development,en" 99 | } 100 | }, 101 | "defaultConfiguration": "development" 102 | }, 103 | "extract-i18n": { 104 | "builder": "@ngx-env/builder:extract-i18n", 105 | "options": { 106 | "browserTarget": "admin-cpanel:build" 107 | } 108 | }, 109 | "test": { 110 | "builder": "@ngx-env/builder:karma", 111 | "options": { 112 | "main": "src/test.ts", 113 | "polyfills": "src/polyfills.ts", 114 | "tsConfig": "tsconfig.spec.json", 115 | "karmaConfig": "karma.conf.js", 116 | "assets": [ 117 | "src/assets" 118 | ], 119 | "styles": [ 120 | "src/styles.scss" 121 | ], 122 | "scripts": [] 123 | } 124 | } 125 | } 126 | } 127 | }, 128 | "defaultProject": "admin-cpanel" 129 | } -------------------------------------------------------------------------------- /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/ngrx-app'), 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": "admin-cpanel", 3 | "version": "1.1.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "start:en": "ng serve --configuration=en -- --port 4201", 8 | "extract": "ng extract-i18n --output-path src/locales", 9 | "build": "ng build", 10 | "watch": "ng build --watch --configuration development", 11 | "test": "ng test", 12 | "lint": "ng lint --fix" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "~13.3.0", 17 | "@angular/cdk": "^13.3.9", 18 | "@angular/common": "~16.2.2", 19 | "@angular/compiler": "~13.3.0", 20 | "@angular/core": "~16.1.8", 21 | "@angular/forms": "~13.3.0", 22 | "@angular/material": "^13.3.9", 23 | "@angular/platform-browser": "~13.3.0", 24 | "@angular/platform-browser-dynamic": "~16.1.8", 25 | "@angular/router": "~13.3.0", 26 | "@ngrx/effects": "^16.1.0", 27 | "@ngrx/router-store": "^16.2.0", 28 | "@ngrx/store": "^16.1.0", 29 | "@ngrx/store-devtools": "^16.2.0", 30 | "@sentry/angular": "^7.12.1", 31 | "@sentry/tracing": "^7.12.1", 32 | "@tailwindcss/aspect-ratio": "^0.4.1", 33 | "@tailwindcss/forms": "^0.5.4", 34 | "@tailwindcss/line-clamp": "^0.4.1", 35 | "@tailwindcss/typography": "^0.5.6", 36 | "ngx-pagination": "^6.0.2", 37 | "ngx-permissions": "^13.0.1", 38 | "rxjs": "~7.8.1", 39 | "tslib": "^2.3.0", 40 | "zone.js": "~0.13.1" 41 | }, 42 | "devDependencies": { 43 | "@angular-devkit/build-angular": "~13.3.5", 44 | "@angular-eslint/builder": "13.5.0", 45 | "@angular-eslint/eslint-plugin": "16.1.1", 46 | "@angular-eslint/eslint-plugin-template": "16.1.0", 47 | "@angular-eslint/schematics": "16.1.0", 48 | "@angular-eslint/template-parser": "16.1.1", 49 | "@angular/cli": "~16.2.0", 50 | "@angular/compiler-cli": "~13.3.0", 51 | "@angular/localize": "^13.3.11", 52 | "@ngx-env/builder": "^2.2.0", 53 | "@schematics/angular": "^14.2.2", 54 | "@types/jasmine": "~3.10.0", 55 | "@types/node": "^20.4.5", 56 | "@typescript-eslint/eslint-plugin": "6.4.0", 57 | "@typescript-eslint/parser": "6.4.1", 58 | "autoprefixer": "^10.4.8", 59 | "eslint": "^8.17.0", 60 | "eslint-config-prettier": "^8.8.0", 61 | "eslint-plugin-prettier": "^4.2.1", 62 | "husky": "^8.0.3", 63 | "jasmine-core": "~5.1.0", 64 | "karma": "~6.3.0", 65 | "karma-chrome-launcher": "~3.2.0", 66 | "karma-coverage": "~2.1.0", 67 | "karma-jasmine": "~5.1.0", 68 | "karma-jasmine-html-reporter": "~1.7.0", 69 | "postcss": "^8.4.24", 70 | "prettier": "^3.0.2", 71 | "prettier-eslint": "^15.0.1", 72 | "tailwindcss": "^3.3.2", 73 | "typescript": "~4.6.2" 74 | }, 75 | "husky": { 76 | "hooks": { 77 | "pre-commit": "npm run lint" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APP_INITIALIZER, 3 | ErrorHandler, 4 | LOCALE_ID, 5 | NgModule, 6 | } from '@angular/core'; 7 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 8 | import { BrowserModule } from '@angular/platform-browser'; 9 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 10 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 11 | import { StoreModule } from '@ngrx/store'; 12 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 13 | import { EffectsModule } from '@ngrx/effects'; 14 | import { StoreRouterConnectingModule } from '@ngrx/router-store'; 15 | import * as Sentry from '@sentry/angular'; 16 | import { Router } from '@angular/router'; 17 | import localeFr from '@angular/common/locales/fr'; 18 | import { registerLocaleData } from '@angular/common'; 19 | import { NgxPermissionsModule } from 'ngx-permissions'; 20 | 21 | import { environment } from 'environments/environment'; 22 | import { AppRoutingModule } from './core/routes/app-routing.module'; 23 | import { ROOT_REDUCERS } from './core/store/app.store'; 24 | import { AuthInterceptor } from './modules/authentication/interceptors/auth.interceptor'; 25 | import { HttpLoadingInterceptor } from './shared/interceptors/http-loading.interceptor'; 26 | import { AuthenticationModule } from './modules/authentication/authentication.module'; 27 | import { ErrorsInterceptor } from './shared/interceptors/errors.interceptor'; 28 | import { AppComponent } from './core/components/app/app.component'; 29 | import { NetworkStatusComponent } from './core/components/network-status/network-status.component'; 30 | import { SharedModule } from './shared/shared.module'; 31 | 32 | registerLocaleData(localeFr); 33 | 34 | @NgModule({ 35 | declarations: [AppComponent, NetworkStatusComponent], 36 | imports: [ 37 | AppRoutingModule, 38 | AuthenticationModule, 39 | BrowserAnimationsModule, 40 | BrowserModule, 41 | EffectsModule.forRoot(), 42 | SharedModule, 43 | FormsModule, 44 | HttpClientModule, 45 | NgxPermissionsModule.forRoot(), 46 | ReactiveFormsModule, 47 | StoreModule.forRoot(ROOT_REDUCERS), 48 | StoreDevtoolsModule.instrument({ 49 | maxAge: 25, 50 | logOnly: environment.production, 51 | }), 52 | StoreRouterConnectingModule.forRoot(), 53 | ], 54 | providers: [ 55 | { 56 | provide: ErrorHandler, 57 | useValue: Sentry.createErrorHandler({ 58 | showDialog: true, 59 | }), 60 | }, 61 | { 62 | provide: Sentry.TraceService, 63 | deps: [Router], 64 | }, 65 | { 66 | provide: APP_INITIALIZER, 67 | useFactory: () => () => {}, 68 | deps: [Sentry.TraceService], 69 | multi: true, 70 | }, 71 | { 72 | provide: HTTP_INTERCEPTORS, 73 | useClass: AuthInterceptor, 74 | multi: true, 75 | }, 76 | { 77 | provide: HTTP_INTERCEPTORS, 78 | useClass: ErrorsInterceptor, 79 | multi: true, 80 | }, 81 | { 82 | provide: HTTP_INTERCEPTORS, 83 | useClass: HttpLoadingInterceptor, 84 | multi: true, 85 | }, 86 | { 87 | provide: LOCALE_ID, 88 | useValue: 'fr-FR', 89 | }, 90 | ], 91 | bootstrap: [AppComponent], 92 | }) 93 | export class AppModule {} 94 | -------------------------------------------------------------------------------- /src/app/core/components/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'ngrx-app'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('ngrx-app'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement as HTMLElement; 33 | expect(compiled.querySelector('.content span')?.textContent).toContain('ngrx-app app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/core/components/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { Notification } from '@app/core/interfaces/notification.interface'; 6 | import { resetNotificationStatusAction } from '@app/core/store/notification/notification.actions'; 7 | import { selectNotification } from '@app/core/store/notification/notification.selectors'; 8 | 9 | @Component({ 10 | selector: 'admin-root', 11 | template: ` 12 | 13 | 14 | 21 | `, 22 | }) 23 | export class AppComponent implements OnInit { 24 | mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 25 | 26 | isOpen!: boolean; 27 | duration: number = 5000; 28 | 29 | notification$: Observable = 30 | this.store.select(selectNotification); 31 | 32 | constructor(private store: Store) {} 33 | 34 | ngOnInit(): void { 35 | this.notification$.subscribe(notification => { 36 | if (notification) { 37 | this.isOpen = true; 38 | setTimeout(() => { 39 | this.isOpen = false; 40 | }, this.duration); 41 | } 42 | }); 43 | 44 | document.documentElement.setAttribute('data-theme', this.updateTheme()); 45 | 46 | new MutationObserver(([{ oldValue }]) => { 47 | let newValue = document.documentElement.getAttribute('data-theme')!; 48 | if (newValue !== oldValue) { 49 | try { 50 | window.localStorage.setItem('theme', newValue); 51 | } catch {} 52 | this.updateThemeWithoutTransitions(newValue); 53 | } 54 | }).observe(document.documentElement, { 55 | attributeFilter: ['data-theme'], 56 | attributeOldValue: true, 57 | }); 58 | } 59 | 60 | close(value: boolean) { 61 | console.log('close', value); 62 | 63 | this.store.dispatch(resetNotificationStatusAction()); 64 | this.isOpen = false; 65 | } 66 | 67 | updateTheme(savedTheme: string | null = null): string { 68 | let theme = 'system'; 69 | try { 70 | if (!savedTheme) { 71 | savedTheme = window.localStorage.getItem('theme'); 72 | } 73 | if (savedTheme === 'dark') { 74 | theme = 'dark'; 75 | document.documentElement.classList.add('dark'); 76 | } else if (savedTheme === 'light') { 77 | theme = 'light'; 78 | document.documentElement.classList.remove('dark'); 79 | } else if (this.mediaQuery.matches) { 80 | document.documentElement.classList.add('dark'); 81 | } else { 82 | document.documentElement.classList.remove('dark'); 83 | } 84 | } catch { 85 | theme = 'light'; 86 | document.documentElement.classList.remove('dark'); 87 | } 88 | return theme; 89 | } 90 | 91 | updateThemeWithoutTransitions(savedTheme: string | null = null): void { 92 | this.updateTheme(savedTheme); 93 | document.documentElement.classList.add('[&_*]:!transition-none'); 94 | window.setTimeout(() => { 95 | document.documentElement.classList.remove('[&_*]:!transition-none'); 96 | }, 0); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/app/core/components/network-status/network-status.component.ts: -------------------------------------------------------------------------------- 1 | import { animate, style, transition, trigger } from '@angular/animations'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { fromEvent, Observable, Subscription } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'network-status', 7 | template: ` 8 |
13 |
14 |
16 |
17 |
18 |
19 | 42 |
43 |
46 |

47 | {{ networkStatus }} 48 |

49 |

50 | {{ networkStatusMessage }} 51 |

52 |
53 |
54 |
55 |
56 |
57 |
58 | `, 59 | animations: [ 60 | trigger('showHideNotification', [ 61 | transition('void => *', [ 62 | style({ transform: 'translateX(0.5rem)', opacity: 0 }), 63 | animate(300, style({ transform: 'translateX(0)', opacity: 1 })), 64 | ]), 65 | transition('* => void', [animate(100, style({ opacity: 0 }))]), 66 | ]), 67 | ], 68 | }) 69 | export class NetworkStatusComponent implements OnInit { 70 | networkStatusMessage!: string; 71 | networkStatus!: string; 72 | subscriptions: Subscription[] = []; 73 | showNetworkStatus!: boolean; 74 | 75 | onlineEvent$!: Observable; 76 | offlineEvent$!: Observable; 77 | 78 | ngOnInit() { 79 | this.networkStatusChecker(); 80 | } 81 | 82 | toggleNetworkStatus(): void { 83 | this.showNetworkStatus = true; 84 | setTimeout(() => { 85 | this.showNetworkStatus = false; 86 | }, 5000); 87 | } 88 | 89 | networkStatusChecker(): void { 90 | this.onlineEvent$ = fromEvent(window, 'online'); 91 | this.offlineEvent$ = fromEvent(window, 'offline'); 92 | 93 | this.subscriptions.push( 94 | this.onlineEvent$.subscribe(() => { 95 | this.networkStatus = 'online'; 96 | this.networkStatusMessage = $localize`Vous êtes de nouveau en ligne.`; 97 | this.toggleNetworkStatus(); 98 | }) 99 | ); 100 | 101 | this.subscriptions.push( 102 | this.offlineEvent$.subscribe(() => { 103 | this.networkStatus = 'offline'; 104 | this.networkStatusMessage = $localize`Vous n'êtes pas connecté à l'Internet`; 105 | this.toggleNetworkStatus(); 106 | }) 107 | ); 108 | } 109 | 110 | ngOnDestroy(): void { 111 | this.subscriptions.forEach(subscription => subscription.unsubscribe()); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/app/core/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, Router } from '@angular/router'; 3 | 4 | import { LocalStorageService } from '@app/modules/authentication/services/local-storage.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class AuthGuard implements CanActivate { 10 | constructor( 11 | private router: Router, 12 | private localStorageService: LocalStorageService 13 | ) {} 14 | 15 | canActivate(): boolean { 16 | if (!this.localStorageService.getAccessToken()) { 17 | this.router.navigateByUrl('/auth/login'); 18 | 19 | return false; 20 | } 21 | 22 | return true; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/core/guards/guest.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, Router } from '@angular/router'; 3 | 4 | import { LocalStorageService } from '@app/modules/authentication/services/local-storage.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class GuestGuard implements CanActivate { 10 | constructor( 11 | private router: Router, 12 | private localStorageService: LocalStorageService 13 | ) {} 14 | 15 | canActivate(): boolean { 16 | if (!this.localStorageService.tokenExpired()) { 17 | this.router.navigateByUrl('/dashboard'); 18 | 19 | return false; 20 | } 21 | 22 | return true; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/core/guards/role.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate } from '@angular/router'; 3 | import { NgxPermissionsService } from 'ngx-permissions'; 4 | 5 | import { LocalStorageService } from '@app/modules/authentication/services/local-storage.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class RoleGuard implements CanActivate { 11 | constructor( 12 | private localStorageService: LocalStorageService, 13 | private ngxPermissionsService: NgxPermissionsService 14 | ) {} 15 | 16 | canActivate(): boolean { 17 | const roles = this.localStorageService.getLocalStorage('roles'); 18 | 19 | if (roles) { 20 | const USER_ROLES = JSON.parse(roles); 21 | this.ngxPermissionsService.loadPermissions(USER_ROLES); 22 | 23 | return true; 24 | } 25 | 26 | return false; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/core/guards/user-data.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate } from '@angular/router'; 3 | import { Store } from '@ngrx/store'; 4 | import { first, map, Observable } from 'rxjs'; 5 | 6 | import { selectCurrentUser } from '@app/modules/authentication/store/auth.selectors'; 7 | import { getCurrentUserAction } from '@app/modules/authentication/store/auth.actions'; 8 | import { User } from '@app/modules/user/interfaces/user.interface'; 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class UserDataGuard implements CanActivate { 14 | constructor(private store: Store) {} 15 | 16 | canActivate(): Observable { 17 | return this.store.select(selectCurrentUser).pipe( 18 | first(), 19 | map((user: User | null) => { 20 | if (!user) { 21 | this.store.dispatch(getCurrentUserAction()); 22 | } 23 | return true; 24 | }) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/core/guards/user.roles.ts: -------------------------------------------------------------------------------- 1 | export const ADMIN_ROLE = 'super_admin'; 2 | export const MANAGER_ROLE = 'manager'; 3 | export const DEVELOPER_ROLE = 'developer'; 4 | -------------------------------------------------------------------------------- /src/app/core/interfaces/notification.interface.ts: -------------------------------------------------------------------------------- 1 | export interface NotificationState { 2 | notification: Notification | null; 3 | } 4 | 5 | export interface Notification { 6 | title?: string; 7 | message: string; 8 | type: Type; 9 | duration?: number; 10 | } 11 | 12 | export type Type = 'success' | 'error' | 'info' | 'warning'; 13 | -------------------------------------------------------------------------------- /src/app/core/interfaces/session.interface.ts: -------------------------------------------------------------------------------- 1 | export interface SessionState { 2 | formErrors: IValidationError | null; 3 | } 4 | 5 | export type IValidationError = { 6 | message: string; 7 | errors: { 8 | [key: string]: string[]; 9 | }; 10 | status_code: number; 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/core/routes/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { PreloadAllModules, RouterModule } from '@angular/router'; 3 | 4 | import { ROUTES } from './app.routing'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forRoot(ROUTES, { preloadingStrategy: PreloadAllModules }), 9 | ], 10 | exports: [RouterModule], 11 | }) 12 | export class AppRoutingModule {} 13 | -------------------------------------------------------------------------------- /src/app/core/routes/app.routing.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | import { NotFoundComponent } from '@app/shared/themes/pages/not-found/not-found.component'; 4 | 5 | export const ROUTES: Routes = [ 6 | { path: '', redirectTo: '/auth/login', pathMatch: 'full' }, 7 | { 8 | path: 'auth', 9 | loadChildren: () => 10 | import('@modules/authentication/authentication.module').then( 11 | m => m.AuthenticationModule 12 | ), 13 | }, 14 | { 15 | path: 'dashboard', 16 | loadChildren: () => 17 | import('@modules/dashboard/dashboard.module').then( 18 | m => m.DashboardModule 19 | ), 20 | }, 21 | { path: '**', pathMatch: 'full', component: NotFoundComponent }, 22 | ]; 23 | -------------------------------------------------------------------------------- /src/app/core/store/app.store.ts: -------------------------------------------------------------------------------- 1 | import { routerReducer, RouterState } from '@ngrx/router-store'; 2 | import { Action, ActionReducerMap } from '@ngrx/store'; 3 | 4 | import { NotificationState } from '../interfaces/notification.interface'; 5 | import { SessionState } from '../interfaces/session.interface'; 6 | import { 7 | notificationFeatureKey, 8 | notificationReducer, 9 | } from './notification/notification.reducer'; 10 | import { sessionFeatureKey, sessionReducer } from './session/session.reducer'; 11 | 12 | export interface AppState { 13 | router: RouterState; 14 | [sessionFeatureKey]: SessionState; 15 | [notificationFeatureKey]: NotificationState; 16 | } 17 | 18 | export const ROOT_REDUCERS: ActionReducerMap = { 19 | router: routerReducer, 20 | [sessionFeatureKey]: sessionReducer, 21 | [notificationFeatureKey]: notificationReducer, 22 | }; 23 | -------------------------------------------------------------------------------- /src/app/core/store/notification/notification.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, props } from '@ngrx/store'; 2 | 3 | import { Notification } from '@app/core/interfaces/notification.interface'; 4 | 5 | export const getNotificationStatusAction = createAction( 6 | '[Notification] Get Notification', 7 | props<{ notification: Notification }>() 8 | ); 9 | 10 | export const resetNotificationStatusAction = createAction( 11 | '[Notification] Reset Notification' 12 | ); 13 | -------------------------------------------------------------------------------- /src/app/core/store/notification/notification.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer, on } from '@ngrx/store'; 2 | 3 | import { 4 | Notification, 5 | NotificationState, 6 | } from '@app/core/interfaces/notification.interface'; 7 | import * as NotificationActions from './notification.actions'; 8 | 9 | export const notificationFeatureKey = 'notification'; 10 | 11 | export const notificationState: NotificationState = { 12 | notification: null, 13 | }; 14 | 15 | export const notificationReducer = createReducer( 16 | notificationState, 17 | on( 18 | NotificationActions.getNotificationStatusAction, 19 | ( 20 | state: NotificationState, 21 | { notification }: { notification: Notification } 22 | ): NotificationState => { 23 | return { 24 | ...state, 25 | notification, 26 | }; 27 | } 28 | ), 29 | on( 30 | NotificationActions.resetNotificationStatusAction, 31 | (state: NotificationState): NotificationState => { 32 | return { 33 | ...state, 34 | notification: null, 35 | }; 36 | } 37 | ) 38 | ); 39 | -------------------------------------------------------------------------------- /src/app/core/store/notification/notification.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | 3 | import { NotificationState } from '@app/core/interfaces/notification.interface'; 4 | import { notificationFeatureKey } from './notification.reducer'; 5 | 6 | export const notificationFeatureSelector = 7 | createFeatureSelector(notificationFeatureKey); 8 | 9 | export const selectNotification = createSelector( 10 | notificationFeatureSelector, 11 | (state: NotificationState) => state.notification 12 | ); 13 | -------------------------------------------------------------------------------- /src/app/core/store/router.selectors.ts: -------------------------------------------------------------------------------- 1 | import { getSelectors } from '@ngrx/router-store'; 2 | 3 | export const { selectRouteParams, selectQueryParams } = getSelectors(); -------------------------------------------------------------------------------- /src/app/core/store/session/session.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, props } from '@ngrx/store'; 2 | 3 | import { IValidationError } from '@app/core/interfaces/session.interface'; 4 | 5 | export const fetchFormsErrorAction = createAction( 6 | '[Session] Get Forms Errors', 7 | props<{ formErrors: IValidationError }>() 8 | ); 9 | 10 | export const clearErrorsAction = createAction('[Session] Clear Forms Errors'); 11 | -------------------------------------------------------------------------------- /src/app/core/store/session/session.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer, on } from '@ngrx/store'; 2 | 3 | import { SessionState } from '@app/core/interfaces/session.interface'; 4 | import * as SessionActions from './session.actions'; 5 | 6 | export const sessionFeatureKey = 'session'; 7 | 8 | export const sessionState: SessionState = { 9 | formErrors: null, 10 | }; 11 | 12 | export const sessionReducer = createReducer( 13 | sessionState, 14 | on( 15 | SessionActions.fetchFormsErrorAction, 16 | (state: SessionState, { formErrors }): SessionState => { 17 | return { 18 | ...state, 19 | formErrors, 20 | }; 21 | } 22 | ), 23 | on(SessionActions.clearErrorsAction, (state: SessionState): SessionState => { 24 | return { 25 | ...state, 26 | formErrors: null, 27 | }; 28 | }) 29 | ); 30 | -------------------------------------------------------------------------------- /src/app/core/store/session/session.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | 3 | import { SessionState } from '@app/core/interfaces/session.interface'; 4 | import { sessionFeatureKey } from './session.reducer'; 5 | 6 | export const sessionFeatureSelector = 7 | createFeatureSelector(sessionFeatureKey); 8 | 9 | export const selectFormErrors = createSelector( 10 | sessionFeatureSelector, 11 | (state: SessionState) => state.formErrors 12 | ); 13 | -------------------------------------------------------------------------------- /src/app/modules/authentication/authentication.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 3 | import { RouterModule } from '@angular/router'; 4 | import { StoreModule } from '@ngrx/store'; 5 | import { EffectsModule } from '@ngrx/effects'; 6 | 7 | import { SharedModule } from '@shared/shared.module'; 8 | import { AUTH_ROUTES } from './routes/authenticate.routes'; 9 | 10 | import { ForgotPasswordComponent } from './pages/forgot-password/forgot-password.component'; 11 | import { LoginComponent } from './pages/login/login.component'; 12 | import { ResetPasswordComponent } from './pages/reset-password/reset-password.component'; 13 | import { authReducer, authFeatureKey } from './store/auth.reducer'; 14 | import { AuthEffects } from './store/auth.effects'; 15 | 16 | @NgModule({ 17 | declarations: [ 18 | LoginComponent, 19 | ForgotPasswordComponent, 20 | ResetPasswordComponent, 21 | ], 22 | imports: [ 23 | EffectsModule.forFeature([AuthEffects]), 24 | FormsModule, 25 | ReactiveFormsModule, 26 | RouterModule.forChild(AUTH_ROUTES), 27 | SharedModule, 28 | StoreModule.forFeature(authFeatureKey, authReducer), 29 | ], 30 | exports: [], 31 | providers: [], 32 | }) 33 | export class AuthenticationModule {} 34 | -------------------------------------------------------------------------------- /src/app/modules/authentication/interceptors/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | HttpEvent, 4 | HttpInterceptor, 5 | HttpHandler, 6 | HttpRequest, 7 | } from '@angular/common/http'; 8 | import { Observable } from 'rxjs'; 9 | 10 | import { environment } from 'environments/environment'; 11 | import { LocalStorageService } from '../services/local-storage.service'; 12 | 13 | @Injectable() 14 | export class AuthInterceptor implements HttpInterceptor { 15 | constructor(private localStorageService: LocalStorageService) {} 16 | 17 | intercept( 18 | request: HttpRequest, 19 | next: HttpHandler 20 | ): Observable> { 21 | const isApiUrl = request.url.startsWith(environment.apiUrl); 22 | const accessToken = this.localStorageService.getAccessToken(); 23 | 24 | if (accessToken && isApiUrl) { 25 | request = request.clone({ 26 | setHeaders: { Authorization: `Bearer ${accessToken}` }, 27 | }); 28 | } 29 | 30 | return next.handle(request); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/modules/authentication/interfaces/credentials.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Credentials { 2 | email: string; 3 | password: string; 4 | } 5 | 6 | export interface ResetPasswordCredentials { 7 | email: string; 8 | password: string; 9 | password_confirmation: string; 10 | token: string; 11 | } -------------------------------------------------------------------------------- /src/app/modules/authentication/interfaces/state.interface.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@app/modules/user/interfaces/user.interface'; 2 | import { DefaultState } from '@app/shared/interfaces/state.interfaces'; 3 | 4 | export interface AuthState extends DefaultState { 5 | isLoggedIn: boolean; 6 | user: User | null; 7 | roles: string[]; 8 | permissions: string[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/modules/authentication/pages/forgot-password/forgot-password.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Mot de passe oublié?

4 |

5 | Saisissez votre adresse e-mail et nous vous enverrons un lien pour réinitialiser votre mot de passe. 6 |

7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 23 |
24 | 25 |
26 | 27 | Envoyer le lien de réinitialisation 28 | 29 |
30 |
31 | 32 |

33 | 34 | 35 | 36 | 37 | Retournez à la page de connexion 38 | 39 |

-------------------------------------------------------------------------------- /src/app/modules/authentication/pages/forgot-password/forgot-password.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Store } from '@ngrx/store'; 4 | import { Observable } from 'rxjs'; 5 | 6 | import { forgotPasswordAction } from '../../store/auth.actions'; 7 | import { 8 | selectError, 9 | selectLoading, 10 | selectMessage, 11 | } from '../../store/auth.selectors'; 12 | 13 | @Component({ 14 | templateUrl: './forgot-password.component.html', 15 | }) 16 | export class ForgotPasswordComponent { 17 | public form: FormGroup = this.fb.group({ 18 | email: ['', [Validators.required, Validators.email]], 19 | }); 20 | 21 | public error$: Observable = this.store.select(selectError); 22 | 23 | public message$: Observable = this.store.select(selectMessage); 24 | 25 | public loading$: Observable = this.store.select(selectLoading); 26 | 27 | constructor(private fb: FormBuilder, private store: Store) {} 28 | 29 | public submit() { 30 | if (this.form.valid) { 31 | this.store.dispatch(forgotPasswordAction(this.form.getRawValue())); 32 | 33 | this.form.reset(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/modules/authentication/pages/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Connectez-vous

4 |

5 | Admin Panel - Administration de votre plateforme. 6 |

7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 24 |
25 | 26 | 27 |
28 | 35 |
36 | 37 |
38 | 42 | 43 | 46 |
47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | Se connecter 56 | 57 |
58 |
59 | -------------------------------------------------------------------------------- /src/app/modules/authentication/pages/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Store } from '@ngrx/store'; 4 | import { Observable } from 'rxjs'; 5 | 6 | import { authenticateAction } from '../../store/auth.actions'; 7 | import { 8 | selectError, 9 | selectLoading, 10 | selectMessage, 11 | } from '../../store/auth.selectors'; 12 | 13 | @Component({ 14 | templateUrl: './login.component.html', 15 | }) 16 | export class LoginComponent { 17 | public form: FormGroup = this.fb.group({ 18 | email: ['', [Validators.required, Validators.email]], 19 | password: ['', Validators.required], 20 | }); 21 | 22 | public error$: Observable = this.store.select(selectError); 23 | 24 | public message$: Observable = this.store.select(selectMessage); 25 | 26 | public loading$: Observable = this.store.select(selectLoading); 27 | 28 | constructor(private fb: FormBuilder, private store: Store) {} 29 | 30 | public submit() { 31 | if (this.form.valid) { 32 | this.store.dispatch( 33 | authenticateAction({ credentials: this.form.getRawValue() }) 34 | ); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/modules/authentication/pages/reset-password/reset-password.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Réinitialisation Mot de passe

4 |

5 | Veuillez renseignez votre nouveau mot de passe pour vous connectez. 6 |

7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 24 |
25 | 26 | 27 |
28 | 35 |
36 | 37 | 38 |
39 | 46 |
47 | 48 |
49 | 50 | Réinitialiser Mot de passe 51 | 52 |
53 | 54 |
-------------------------------------------------------------------------------- /src/app/modules/authentication/pages/reset-password/reset-password.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Store } from '@ngrx/store'; 4 | import { map, Observable } from 'rxjs'; 5 | 6 | import { PasswordRules } from '@app/shared/rules/password.rule'; 7 | import { 8 | selectError, 9 | selectLoading, 10 | selectMessage, 11 | selectResetPasswordToken, 12 | } from '../../store/auth.selectors'; 13 | import { resetPasswordAction } from '../../store/auth.actions'; 14 | 15 | @Component({ 16 | templateUrl: './reset-password.component.html', 17 | }) 18 | export class ResetPasswordComponent { 19 | public form: FormGroup = this.formBuilder.group( 20 | { 21 | email: ['', [Validators.required, Validators.email]], 22 | password: ['', [Validators.required, Validators.minLength(2)]], 23 | password_confirmation: [ 24 | '', 25 | [Validators.required, Validators.minLength(2)], 26 | ], 27 | }, 28 | { validators: [PasswordRules.match('password', 'password_confirmation')] } 29 | ); 30 | 31 | public error$: Observable = this.store.select(selectError); 32 | public message$: Observable = this.store.select(selectMessage); 33 | public loading$: Observable = this.store.select(selectLoading); 34 | public token$: Observable = this.store.select( 35 | selectResetPasswordToken 36 | ); 37 | 38 | constructor(private formBuilder: FormBuilder, private store: Store) {} 39 | 40 | public submit() { 41 | if (this.form.valid) { 42 | this.token$.pipe(map(token => token)).subscribe(value => { 43 | let credentials = { ...this.form.getRawValue(), token: value }; 44 | this.store.dispatch(resetPasswordAction({ credentials })); 45 | }); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/modules/authentication/routes/authenticate.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | import { GuestGuard } from '@core/guards/guest.guard'; 4 | import { AuthComponent } from '@app/shared/themes/layouts/auth/auth.component'; 5 | 6 | import { ForgotPasswordComponent } from '../pages/forgot-password/forgot-password.component'; 7 | import { LoginComponent } from '../pages/login/login.component'; 8 | import { ResetPasswordComponent } from '../pages/reset-password/reset-password.component'; 9 | 10 | export const AUTH_ROUTES: Routes = [ 11 | { path: '', redirectTo: '/auth/login', pathMatch: 'full' }, 12 | { 13 | path: '', 14 | canActivate: [GuestGuard], 15 | component: AuthComponent, 16 | children: [ 17 | { path: 'login', component: LoginComponent }, 18 | { path: 'forgot-password', component: ForgotPasswordComponent }, 19 | { path: 'reset-password', component: ResetPasswordComponent }, 20 | ], 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /src/app/modules/authentication/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { environment } from 'environments/environment'; 6 | import { 7 | AuthResponse, 8 | User, 9 | } from '@app/modules/user/interfaces/user.interface'; 10 | import { 11 | Credentials, 12 | ResetPasswordCredentials, 13 | } from '../interfaces/credentials.interface'; 14 | 15 | @Injectable({ 16 | providedIn: 'root', 17 | }) 18 | export class AuthService { 19 | constructor(private http: HttpClient) {} 20 | 21 | public authenticate(credentials: Credentials): Observable { 22 | return this.http.post( 23 | `${environment.apiUrl}/login`, 24 | credentials 25 | ); 26 | } 27 | 28 | public forgotPassword(email: string): Observable { 29 | return this.http.post(`${environment.apiUrl}/forgot-password`, { email }); 30 | } 31 | 32 | public resetPassword(credentials: ResetPasswordCredentials): Observable { 33 | return this.http.post(`${environment.apiUrl}/reset-password`, credentials); 34 | } 35 | 36 | public getCurrentUser(): Observable { 37 | return this.http.get(`${environment.apiUrl}/user/me`); 38 | } 39 | 40 | public getUserRolesPermissions(): Observable<{ 41 | roles: string[]; 42 | permissions: string[]; 43 | }> { 44 | return this.http.get<{ roles: string[]; permissions: string[] }>( 45 | `${environment.apiUrl}/user/roles` 46 | ); 47 | } 48 | 49 | public logout(): Observable { 50 | return this.http.post(`${environment.apiUrl}/logout`, {}); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/modules/authentication/services/local-storage.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root', 5 | }) 6 | export class LocalStorageService { 7 | constructor() {} 8 | 9 | public setAccessToken(accessToken: string, expiredIn: number): void { 10 | localStorage.setItem('accessToken', accessToken); 11 | localStorage.setItem('expiredIn', expiredIn.toString()); 12 | } 13 | 14 | public getAccessToken(): string | null { 15 | return localStorage.getItem('accessToken'); 16 | } 17 | 18 | public removeAccessToken(): void { 19 | localStorage.clear(); 20 | } 21 | 22 | public tokenExpired(): boolean { 23 | return ( 24 | !this.getAccessToken() && 25 | parseInt(localStorage.getItem('expiredIn') || '0') <= 0 26 | ); 27 | } 28 | 29 | public setLocalStorage(key: string, value: any): void { 30 | localStorage.setItem(key, value); 31 | } 32 | 33 | public getLocalStorage(key: string): string | null { 34 | return localStorage.getItem(key); 35 | } 36 | 37 | payload(token: string): any { 38 | const tokenPayload = token.split('.')[1]; 39 | return JSON.parse(atob(tokenPayload)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/modules/authentication/store/auth.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, props } from '@ngrx/store'; 2 | import { 3 | Credentials, 4 | ResetPasswordCredentials, 5 | } from '../interfaces/credentials.interface'; 6 | 7 | import { User } from '@app/modules/user/interfaces/user.interface'; 8 | 9 | export const authenticateAction = createAction( 10 | '[Auth] Authenticate', 11 | props<{ credentials: Credentials }>() 12 | ); 13 | 14 | export const fetchAuthenticateSuccessAction = createAction( 15 | '[Auth] Authenticate Success', 16 | props<{ user: User; roles: string[]; permissions: string[] }>() 17 | ); 18 | 19 | export const fetchAuthenticateFailureAction = createAction( 20 | '[Auth] Authenticate Failure', 21 | props<{ error: string }>() 22 | ); 23 | 24 | export const forgotPasswordAction = createAction( 25 | '[Auth] Forgot Password', 26 | props<{ email: string }>() 27 | ); 28 | 29 | export const fetchForgotPasswordSuccessAction = createAction( 30 | '[Auth] Forgot Password Success', 31 | props<{ message: string }>() 32 | ); 33 | 34 | export const fetchForgotPasswordFailureAction = createAction( 35 | '[Auth] Forgot Password Failure', 36 | props<{ error: string }>() 37 | ); 38 | 39 | export const resetPasswordAction = createAction( 40 | '[Auth] Reset Password', 41 | props<{ credentials: ResetPasswordCredentials }>() 42 | ); 43 | 44 | export const fetchResetPasswordSuccessAction = createAction( 45 | '[Auth] Reset Password Success', 46 | props<{ message: string }>() 47 | ); 48 | 49 | export const fetchResetPasswordFailureAction = createAction( 50 | '[Auth] Reset Password Failure', 51 | props<{ error: string }>() 52 | ); 53 | 54 | export const getCurrentUserAction = createAction('[Auth] Get Current User'); 55 | 56 | export const fetchCurrentUserSuccessAction = createAction( 57 | '[Auth] Get Current User Success', 58 | props<{ user: User | null }>() 59 | ); 60 | 61 | export const getUserRolesAndPermissionsAction = createAction( 62 | '[Auth] Get User Roles & Permissions' 63 | ); 64 | 65 | export const fetchUserRolesAndPermissionsSuccessAction = createAction( 66 | '[Auth] Get User Roles & Permissions Success', 67 | props<{ roles: string[]; permissions: string[] }>() 68 | ); 69 | 70 | export const logoutAction = createAction('[Auth] Logout'); 71 | 72 | export const fetchLogoutSuccessAction = createAction('[Auth] Logout Success'); 73 | -------------------------------------------------------------------------------- /src/app/modules/authentication/store/auth.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Actions, createEffect, ofType } from '@ngrx/effects'; 4 | import { EMPTY, of } from 'rxjs'; 5 | import { catchError, map, switchMap } from 'rxjs/operators'; 6 | import { Store } from '@ngrx/store'; 7 | 8 | import * as AuthActions from './auth.actions'; 9 | import { AuthService } from '../services/auth.service'; 10 | import { 11 | Credentials, 12 | ResetPasswordCredentials, 13 | } from '../interfaces/credentials.interface'; 14 | import { AuthResponse } from '@app/modules/user/interfaces/user.interface'; 15 | import { LocalStorageService } from '../services/local-storage.service'; 16 | import { getNotificationStatusAction } from '@app/core/store/notification/notification.actions'; 17 | import { Notification } from '@app/core/interfaces/notification.interface'; 18 | 19 | @Injectable() 20 | export class AuthEffects { 21 | constructor( 22 | private actions$: Actions, 23 | private store: Store, 24 | private authService: AuthService, 25 | private localStorageService: LocalStorageService, 26 | private router: Router 27 | ) {} 28 | 29 | authenticateEffect = createEffect(() => 30 | this.actions$.pipe( 31 | ofType(AuthActions.authenticateAction), 32 | switchMap(({ credentials }: { credentials: Credentials }) => 33 | this.authService.authenticate(credentials).pipe( 34 | map((authResponse: AuthResponse) => { 35 | this.localStorageService.setAccessToken( 36 | authResponse.data.access_token, 37 | authResponse.data.expires_in 38 | ); 39 | this.localStorageService.setLocalStorage( 40 | 'user', 41 | JSON.stringify(authResponse.data.user) 42 | ); 43 | this.localStorageService.setLocalStorage( 44 | 'roles', 45 | JSON.stringify(authResponse.data.roles) 46 | ); 47 | 48 | this.router.navigateByUrl('/dashboard'); 49 | 50 | const notification: Notification = { 51 | message: $localize`Vous êtes desormais connecté!`, 52 | type: 'success', 53 | }; 54 | 55 | this.store.dispatch(getNotificationStatusAction({ notification })); 56 | 57 | return AuthActions.fetchAuthenticateSuccessAction({ 58 | user: authResponse.data.user, 59 | roles: authResponse.data.roles, 60 | permissions: authResponse.data.permissions, 61 | }); 62 | }), 63 | catchError(error => { 64 | return of( 65 | AuthActions.fetchAuthenticateFailureAction({ 66 | error: 67 | error.error?.message ?? $localize`Une erreur est survenue`, 68 | }) 69 | ); 70 | }) 71 | ) 72 | ) 73 | ) 74 | ); 75 | 76 | forgotPasswordEffect = createEffect(() => 77 | this.actions$.pipe( 78 | ofType(AuthActions.forgotPasswordAction), 79 | switchMap(({ email }: { email: string }) => 80 | this.authService.forgotPassword(email).pipe( 81 | map(({ message }: { message: string }) => 82 | AuthActions.fetchForgotPasswordSuccessAction({ message }) 83 | ), 84 | catchError(error => { 85 | return of( 86 | AuthActions.fetchForgotPasswordFailureAction({ 87 | error: 88 | error.error?.message ?? $localize`Une erreur est survenue`, 89 | }) 90 | ); 91 | }) 92 | ) 93 | ) 94 | ) 95 | ); 96 | 97 | resetPasswordEffect = createEffect(() => 98 | this.actions$.pipe( 99 | ofType(AuthActions.resetPasswordAction), 100 | switchMap(({ credentials }: { credentials: ResetPasswordCredentials }) => 101 | this.authService.resetPassword(credentials).pipe( 102 | map(({ message }: { message: string }) => 103 | AuthActions.fetchResetPasswordSuccessAction({ message }) 104 | ), 105 | catchError(error => { 106 | return of( 107 | AuthActions.fetchResetPasswordFailureAction({ 108 | error: 109 | error.error?.message ?? $localize`Une erreur est survenue`, 110 | }) 111 | ); 112 | }) 113 | ) 114 | ) 115 | ) 116 | ); 117 | 118 | getCurrentUserEffect = createEffect(() => 119 | this.actions$.pipe( 120 | ofType(AuthActions.getCurrentUserAction), 121 | switchMap(() => 122 | this.authService.getCurrentUser().pipe( 123 | map(({ data }: any) => 124 | AuthActions.fetchCurrentUserSuccessAction({ user: data.user }) 125 | ), 126 | catchError(() => EMPTY) 127 | ) 128 | ) 129 | ) 130 | ); 131 | 132 | getUserRolesAndPermissionsEffect = createEffect(() => 133 | this.actions$.pipe( 134 | ofType(AuthActions.getUserRolesAndPermissionsAction), 135 | switchMap(() => 136 | this.authService.getUserRolesPermissions().pipe( 137 | map( 138 | ({ 139 | roles, 140 | permissions, 141 | }: { 142 | roles: string[]; 143 | permissions: string[]; 144 | }) => 145 | AuthActions.fetchUserRolesAndPermissionsSuccessAction({ 146 | roles, 147 | permissions, 148 | }) 149 | ), 150 | catchError(() => EMPTY) 151 | ) 152 | ) 153 | ) 154 | ); 155 | 156 | logoutEffect = createEffect(() => 157 | this.actions$.pipe( 158 | ofType(AuthActions.logoutAction), 159 | switchMap(() => 160 | this.authService.logout().pipe( 161 | map(() => { 162 | const notification: Notification = { 163 | message: $localize`Au revoir et à bientôt!`, 164 | type: 'success', 165 | }; 166 | 167 | this.store.dispatch(getNotificationStatusAction({ notification })); 168 | 169 | this.localStorageService.removeAccessToken(); 170 | this.router.navigateByUrl('/auth/login'); 171 | return AuthActions.fetchLogoutSuccessAction(); 172 | }), 173 | catchError(() => EMPTY) 174 | ) 175 | ) 176 | ) 177 | ); 178 | } 179 | -------------------------------------------------------------------------------- /src/app/modules/authentication/store/auth.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer, on } from '@ngrx/store'; 2 | 3 | import * as AuthActions from './auth.actions'; 4 | import { AuthState } from '../interfaces/state.interface'; 5 | import { User } from '@app/modules/user/interfaces/user.interface'; 6 | 7 | export const authState: AuthState = { 8 | isLoggedIn: false, 9 | user: null, 10 | roles: [], 11 | permissions: [], 12 | error: null, 13 | message: null, 14 | loading: false, 15 | }; 16 | 17 | export const authFeatureKey = 'auth'; 18 | 19 | export const authReducer = createReducer( 20 | authState, 21 | on( 22 | AuthActions.authenticateAction, 23 | AuthActions.forgotPasswordAction, 24 | AuthActions.resetPasswordAction, 25 | AuthActions.logoutAction, 26 | (state: AuthState): AuthState => { 27 | return { 28 | ...state, 29 | loading: true, 30 | }; 31 | } 32 | ), 33 | on( 34 | AuthActions.fetchAuthenticateSuccessAction, 35 | ( 36 | state: AuthState, 37 | { 38 | user, 39 | roles, 40 | permissions, 41 | }: { user: User | null; roles: string[]; permissions: string[] } 42 | ): AuthState => { 43 | return { 44 | ...state, 45 | user, 46 | roles, 47 | permissions, 48 | isLoggedIn: user ? true : false, 49 | loading: false, 50 | error: null, 51 | message: null, 52 | }; 53 | } 54 | ), 55 | on( 56 | AuthActions.fetchCurrentUserSuccessAction, 57 | (state: AuthState, { user }: { user: User | null }): AuthState => { 58 | return { 59 | ...state, 60 | user, 61 | loading: false, 62 | message: null, 63 | error: null, 64 | }; 65 | } 66 | ), 67 | on( 68 | AuthActions.fetchAuthenticateFailureAction, 69 | AuthActions.fetchForgotPasswordFailureAction, 70 | AuthActions.fetchResetPasswordFailureAction, 71 | (state: AuthState, { error }: { error: string }): AuthState => { 72 | return { 73 | ...state, 74 | loading: false, 75 | error, 76 | message: null, 77 | }; 78 | } 79 | ), 80 | on( 81 | AuthActions.fetchForgotPasswordSuccessAction, 82 | (state: AuthState, { message }: { message: string }): AuthState => { 83 | return { 84 | ...state, 85 | loading: false, 86 | error: null, 87 | message, 88 | }; 89 | } 90 | ), 91 | on( 92 | AuthActions.fetchResetPasswordSuccessAction, 93 | (state: AuthState, { message }: { message: string }): AuthState => { 94 | return { 95 | ...state, 96 | loading: false, 97 | error: null, 98 | message, 99 | }; 100 | } 101 | ), 102 | on(AuthActions.fetchLogoutSuccessAction, (state: AuthState): AuthState => { 103 | return { 104 | ...state, 105 | isLoggedIn: false, 106 | loading: false, 107 | user: null, 108 | roles: [], 109 | permissions: [], 110 | error: null, 111 | message: null, 112 | }; 113 | }), 114 | on( 115 | AuthActions.fetchUserRolesAndPermissionsSuccessAction, 116 | ( 117 | state: AuthState, 118 | { roles, permissions }: { roles: string[]; permissions: string[] } 119 | ): AuthState => { 120 | return { 121 | ...state, 122 | permissions, 123 | roles, 124 | message: null, 125 | loading: false, 126 | }; 127 | } 128 | ) 129 | ); 130 | -------------------------------------------------------------------------------- /src/app/modules/authentication/store/auth.selectors.ts: -------------------------------------------------------------------------------- 1 | import { Params } from '@angular/router'; 2 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 3 | 4 | import { selectQueryParams } from '@app/core/store/router.selectors'; 5 | import { AuthState } from '../interfaces/state.interface'; 6 | import { authFeatureKey } from './auth.reducer'; 7 | 8 | const authSelectorFeature = createFeatureSelector(authFeatureKey); 9 | 10 | export const selectCurrentUser = createSelector( 11 | authSelectorFeature, 12 | (state: AuthState) => state.user 13 | ); 14 | 15 | export const selectUserRoles = createSelector( 16 | authSelectorFeature, 17 | (state: AuthState) => state.roles 18 | ); 19 | 20 | export const selectUserPermissions = createSelector( 21 | authSelectorFeature, 22 | (state: AuthState) => state.permissions 23 | ); 24 | 25 | export const selectRolesAndPermissions = createSelector( 26 | authSelectorFeature, 27 | (state: AuthState) => { 28 | return { 29 | roles: state.roles, 30 | permissions: state.permissions, 31 | }; 32 | } 33 | ); 34 | 35 | export const selectError = createSelector( 36 | authSelectorFeature, 37 | (state: AuthState) => state.error 38 | ); 39 | 40 | export const selectIsLoggedIn = createSelector( 41 | authSelectorFeature, 42 | (state: AuthState) => state.isLoggedIn 43 | ); 44 | 45 | export const selectMessage = createSelector( 46 | authSelectorFeature, 47 | (state: AuthState) => state.message 48 | ); 49 | 50 | export const selectLoading = createSelector( 51 | authSelectorFeature, 52 | (state: AuthState) => state.loading 53 | ); 54 | 55 | export const selectResetPasswordToken = createSelector( 56 | selectQueryParams, 57 | (params: Params) => params['token'] 58 | ); 59 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { SharedModule } from '@app/shared/shared.module'; 5 | 6 | import { DashboardComponent } from './pages/dashboard/dashboard.component'; 7 | import { dashboardRoutes } from './routes/dashboard.routes'; 8 | 9 | @NgModule({ 10 | declarations: [ 11 | DashboardComponent 12 | ], 13 | imports: [ 14 | SharedModule, 15 | RouterModule.forChild(dashboardRoutes), 16 | ], 17 | exports: [], 18 | providers: [], 19 | }) 20 | export class DashboardModule { } 21 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/navigation/admin.menu.ts: -------------------------------------------------------------------------------- 1 | import { ADMIN_ROLE } from '@app/core/guards/user.roles'; 2 | import { Menu } from '@app/shared/interfaces/menu'; 3 | 4 | export const adminMenu: Menu[] = [ 5 | { 6 | group: $localize`Aperçu`, 7 | items: [ 8 | { 9 | title: $localize`Tableau de bord`, 10 | svgPath: [ 11 | 'M12 16v5m6 0-3.951-3.293c-.73-.607-1.094-.91-1.5-1.027a2 2 0 0 0-1.098 0c-.406.116-.77.42-1.5 1.027L6 21m2-10v1m4-3v3m4-5v5m6-9H2m1 0h18v8.2c0 1.68 0 2.52-.327 3.162a3 3 0 0 1-1.311 1.311C18.72 16 17.88 16 16.2 16H7.8c-1.68 0-2.52 0-3.162-.327a3 3 0 0 1-1.311-1.311C3 13.72 3 12.88 3 11.2V3Z', 12 | ], 13 | link: '/dashboard', 14 | roles: [ADMIN_ROLE], 15 | }, 16 | { 17 | title: $localize`Statistiques`, 18 | svgPath: [ 19 | 'M10.5 6a7.5 7.5 0 107.5 7.5h-7.5V6z', 20 | 'M13.5 10.5H21A7.5 7.5 0 0013.5 3v7.5z', 21 | ], 22 | link: '/stats', 23 | roles: [ADMIN_ROLE], 24 | }, 25 | ], 26 | }, 27 | { 28 | group: $localize`Management`, 29 | items: [ 30 | { 31 | title: $localize`Utilisateurs`, 32 | svgPath: [ 33 | 'M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z', 34 | ], 35 | link: '/users', 36 | roles: [ADMIN_ROLE], 37 | }, 38 | { 39 | title: $localize`Roles & Permissions`, 40 | svgPath: [ 41 | 'M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z', 42 | ], 43 | link: '/promotions', 44 | roles: [ADMIN_ROLE], 45 | }, 46 | ], 47 | }, 48 | { 49 | group: $localize`Opérations`, 50 | items: [ 51 | { 52 | title: $localize`Paramètres`, 53 | svgPath: [ 54 | 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z', 55 | 'M18.727 14.727a1.5 1.5 0 0 0 .3 1.655l.055.054a1.816 1.816 0 0 1 0 2.573 1.818 1.818 0 0 1-2.573 0l-.055-.055a1.5 1.5 0 0 0-1.654-.3 1.5 1.5 0 0 0-.91 1.373v.155a1.818 1.818 0 1 1-3.636 0V20.1a1.5 1.5 0 0 0-.981-1.373 1.5 1.5 0 0 0-1.655.3l-.054.055a1.818 1.818 0 0 1-3.106-1.287 1.818 1.818 0 0 1 .533-1.286l.054-.055a1.5 1.5 0 0 0 .3-1.654 1.5 1.5 0 0 0-1.372-.91h-.155a1.818 1.818 0 1 1 0-3.636H3.9a1.5 1.5 0 0 0 1.373-.981 1.5 1.5 0 0 0-.3-1.655l-.055-.054A1.818 1.818 0 1 1 7.491 4.99l.054.054a1.5 1.5 0 0 0 1.655.3h.073a1.5 1.5 0 0 0 .909-1.372v-.155a1.818 1.818 0 0 1 3.636 0V3.9a1.499 1.499 0 0 0 .91 1.373 1.5 1.5 0 0 0 1.654-.3l.054-.055a1.817 1.817 0 0 1 2.573 0 1.819 1.819 0 0 1 0 2.573l-.055.054a1.5 1.5 0 0 0-.3 1.655v.073a1.5 1.5 0 0 0 1.373.909h.155a1.818 1.818 0 0 1 0 3.636H20.1a1.499 1.499 0 0 0-1.373.91Z', 56 | ], 57 | link: '/settings', 58 | roles: [ADMIN_ROLE], 59 | }, 60 | ], 61 | }, 62 | ]; 63 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/pages/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 | 7 |
8 |
9 | 10 | 13 | 14 |
15 |
16 |

17 | 18 | 19 | 20 | Documentation 21 | 22 |

23 |

24 | Une documentation simple pour rapidement utiliser le boilerplate pour votre administration. 25 |

26 |
27 | 32 |
33 | 34 |
35 |
36 | 37 | 40 | 41 |
42 |
43 |

44 | 45 | 46 | 47 | Bénéfices 48 | 49 |

50 |

51 | Un boilerplate Ready To Use pour créer une administration avec Angular 13 et Laravel. 52 |

53 |
54 | 59 |
60 | 61 |
62 |
63 | 64 | 65 | 66 | 67 | 68 |
69 |
70 |

71 | 72 | 73 | 74 | Technologies 75 | 76 |

77 |

78 | Ce projet tourne sur la version 13 d'Angular et pour le style, Tailwind v3.1 est utilisé. 79 |

80 |
81 | 86 |
87 | 88 |
89 |
90 | 91 | 94 | 95 |
96 |
97 |

98 | 99 | 100 | 101 | Sponsoring 102 | 103 |

104 |

105 | Pour soutenir le developpement de ce projet, vous pouvez faire un don pour encorager la Team. 106 |

107 |
108 | 113 |
114 | 115 |
116 |
117 | 118 | 119 | 120 | 121 | 122 |
123 |
124 |

125 | 126 | 127 | 128 | NgRx - RxJS 129 | 130 |

131 |

132 | Toute l'architecture NgRx et RxJS est inspiré de la documentation officielle de NgRx. 133 |

134 |
135 | 140 |
141 | 142 |
143 |
144 | 145 | 148 | 149 |
150 |
151 |

152 | 153 | 154 | 155 | Formation 156 | 157 |

158 |

159 | Obtenez une réduction de 10€ sur la formation Angular en vous inscrivant sur Dyma.fr. 160 |

161 |
162 | 167 |
168 | 169 |
170 |
171 |
-------------------------------------------------------------------------------- /src/app/modules/dashboard/pages/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: './dashboard.component.html', 5 | }) 6 | export class DashboardComponent {} 7 | -------------------------------------------------------------------------------- /src/app/modules/dashboard/routes/dashboard.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { NgxPermissionsGuard } from 'ngx-permissions'; 3 | 4 | import { AuthGuard } from '@app/core/guards/auth.guard'; 5 | import { RoleGuard } from '@app/core/guards/role.guard'; 6 | import { UserDataGuard } from '@app/core/guards/user-data.guard'; 7 | 8 | import { CpanelComponent } from '@app/shared/themes/layouts/cpanel/cpanel.component'; 9 | import { DashboardComponent } from '../pages/dashboard/dashboard.component'; 10 | 11 | import { ADMIN_ROLE, DEVELOPER_ROLE } from '@app/core/guards/user.roles'; 12 | 13 | export const dashboardRoutes: Routes = [ 14 | { 15 | path: '', 16 | component: CpanelComponent, 17 | canActivate: [AuthGuard, UserDataGuard, RoleGuard], 18 | children: [ 19 | { 20 | path: '', 21 | component: DashboardComponent, 22 | canActivate: [NgxPermissionsGuard], 23 | data: { 24 | permissions: { 25 | only: [ADMIN_ROLE, DEVELOPER_ROLE], 26 | }, 27 | }, 28 | }, 29 | ], 30 | }, 31 | ]; 32 | -------------------------------------------------------------------------------- /src/app/modules/user/interfaces/user.interface.ts: -------------------------------------------------------------------------------- 1 | import { Pagination } from '@app/shared/interfaces/response.interface'; 2 | import { DefaultState } from '@app/shared/interfaces/state.interfaces'; 3 | 4 | export interface UserState extends DefaultState { 5 | pagination: Pagination; 6 | } 7 | 8 | export interface AuthResponse { 9 | message: string; 10 | data: { 11 | user: User; 12 | access_token: string; 13 | token_type: string; 14 | expires_at: FromDate; 15 | expires_in: number; 16 | roles: string[]; 17 | permissions: string[]; 18 | }; 19 | } 20 | 21 | export interface FromDate { 22 | date: Date; 23 | timezone_type: number; 24 | timezone: string; 25 | } 26 | 27 | export interface User { 28 | id: number; 29 | name: string; 30 | email: string; 31 | phoneNumber: string; 32 | accountType: string; 33 | profilePhotoUrl: string; 34 | timezone: string; 35 | emailVerifiedAt: string; 36 | createdAt: Date; 37 | updatedAt: Date; 38 | } 39 | -------------------------------------------------------------------------------- /src/app/shared/components/alert/alert.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { ErrorComponent } from './error.component'; 5 | import { SuccessComponent } from './success.component'; 6 | 7 | const COMPONENTS = [ErrorComponent, SuccessComponent]; 8 | 9 | @NgModule({ 10 | declarations: COMPONENTS, 11 | imports: [CommonModule], 12 | exports: COMPONENTS, 13 | }) 14 | export class AlertModule {} 15 | -------------------------------------------------------------------------------- /src/app/shared/components/alert/error.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'alert-errors', 5 | template: ` 6 |
7 |
8 |
9 | 20 |
21 |
22 |

23 | {{ message }} 24 |

25 |
26 |
    27 |
  • {{ error }}
  • 28 |
29 |
30 |
31 |
32 |
33 | `, 34 | }) 35 | export class ErrorComponent { 36 | @Input() class!: string; 37 | @Input() message!: string; 38 | @Input() errors: string[] = []; 39 | } -------------------------------------------------------------------------------- /src/app/shared/components/alert/success.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'alert-success', 5 | template: ` 6 |
7 |
8 |
9 | 20 |
21 |
22 |

{{ message }}

23 |
24 |
25 |
26 | `, 27 | }) 28 | export class SuccessComponent { 29 | @Input() class!: string; 30 | @Input() message!: string; 31 | } -------------------------------------------------------------------------------- /src/app/shared/components/buttons/button.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { ButtonPrimaryComponent } from './primary.component'; 5 | import { ButtonLinkComponent } from './link.component'; 6 | import { ButtonDefaultComponent } from './default.component'; 7 | import { RouterModule } from '@angular/router'; 8 | 9 | const COMPONENTS = [ 10 | ButtonPrimaryComponent, 11 | ButtonLinkComponent, 12 | ButtonDefaultComponent, 13 | ]; 14 | 15 | @NgModule({ 16 | declarations: COMPONENTS, 17 | imports: [CommonModule, RouterModule], 18 | exports: COMPONENTS, 19 | }) 20 | export class ButtonModule {} 21 | -------------------------------------------------------------------------------- /src/app/shared/components/buttons/default.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | @Component({ 5 | selector: 'button-default', 6 | template: ` 7 | 32 | `, 33 | }) 34 | export class ButtonDefaultComponent { 35 | @Input() type: string = 'button'; 36 | @Input() loading$!: Observable; 37 | @Input() class!: string; 38 | } 39 | -------------------------------------------------------------------------------- /src/app/shared/components/buttons/link.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'button-link', 5 | template: ` 6 | 10 | 11 | 12 | `, 13 | }) 14 | export class ButtonLinkComponent { 15 | @Input() link!: any; 16 | @Input() class!: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/shared/components/buttons/primary.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | @Component({ 5 | selector: 'button-primary', 6 | template: ` 7 | 34 | `, 35 | }) 36 | export class ButtonPrimaryComponent { 37 | @Input() type: string = 'button'; 38 | @Input() loading$!: Observable; 39 | @Input() class!: string; 40 | } 41 | -------------------------------------------------------------------------------- /src/app/shared/components/headings/heading.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { PageHeadingWithActionComponent } from './page-heading-with-action.component'; 5 | import { PageHeadingComponent } from './page-heading.component'; 6 | 7 | const COMPONENTS = [PageHeadingWithActionComponent, PageHeadingComponent]; 8 | 9 | @NgModule({ 10 | declarations: COMPONENTS, 11 | imports: [CommonModule], 12 | exports: COMPONENTS, 13 | }) 14 | export class HeadingModule {} 15 | -------------------------------------------------------------------------------- /src/app/shared/components/headings/page-heading-with-action.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'admin-page-heading-with-action', 5 | template: ` 6 |
7 |
8 |

10 | {{ title }} 11 |

12 |
13 |
14 | 15 |
16 |
17 | `, 18 | }) 19 | export class PageHeadingWithActionComponent { 20 | @Input() title!: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/app/shared/components/headings/page-heading.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'admin-page-heading', 5 | template: ` 6 |
7 |
8 |
9 |

11 | {{ title }} 12 |

13 |

16 | {{ description }} 17 |

18 |
19 |
20 |
21 | `, 22 | }) 23 | export class PageHeadingComponent { 24 | @Input() title!: string; 25 | @Input() description!: string; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/shared/components/inputs/inputs.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { OverlapingLabelComponent } from './overlaping-label/overlaping-label.component'; 6 | 7 | const COMPONENTS = [OverlapingLabelComponent]; 8 | 9 | @NgModule({ 10 | declarations: COMPONENTS, 11 | imports: [CommonModule, FormsModule, ReactiveFormsModule], 12 | exports: COMPONENTS, 13 | }) 14 | export class InputsModule {} 15 | -------------------------------------------------------------------------------- /src/app/shared/components/inputs/overlaping-label/overlaping-label.component.html: -------------------------------------------------------------------------------- 1 |
2 | 8 | 20 |
21 | 22 |

{{ helpText }}

23 |
24 | -------------------------------------------------------------------------------- /src/app/shared/components/inputs/overlaping-label/overlaping-label.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, forwardRef, Input } from '@angular/core'; 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'input-overlaping-label', 6 | templateUrl: './overlaping-label.component.html', 7 | providers: [ 8 | { 9 | provide: NG_VALUE_ACCESSOR, 10 | useExisting: forwardRef(() => OverlapingLabelComponent), 11 | multi: true, 12 | }, 13 | ], 14 | }) 15 | export class OverlapingLabelComponent implements ControlValueAccessor { 16 | value: string = ''; 17 | 18 | @Input() label!: string | null; 19 | @Input() placeholder!: string | null; 20 | @Input() name!: string; 21 | @Input() type: string = 'text'; 22 | @Input() required: boolean = false; 23 | @Input() disabled!: boolean; 24 | @Input() containerClass!: string; 25 | @Input() inputClass!: string; 26 | @Input() helpText!: string | null; 27 | 28 | writeValue(value: string): void { 29 | if (value !== undefined) { 30 | this.value = value; 31 | this.onChange(value); 32 | } 33 | } 34 | 35 | setDisabledState(isDisabled: boolean): void { 36 | this.disabled = isDisabled; 37 | } 38 | 39 | registerOnChange(fn: (value: string) => void): void { 40 | this.onChange = fn; 41 | } 42 | 43 | registerOnTouched(fn: () => void): void { 44 | this.onTouched = fn; 45 | } 46 | 47 | onChange = (value: string) => {}; 48 | 49 | onTouched = () => {}; 50 | } 51 | -------------------------------------------------------------------------------- /src/app/shared/components/notifications/notifications.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { SimpleNotificationComponent } from './simple-notification.component'; 5 | 6 | const COMPONENTS = [SimpleNotificationComponent]; 7 | 8 | @NgModule({ 9 | declarations: [COMPONENTS], 10 | imports: [CommonModule], 11 | exports: [COMPONENTS], 12 | }) 13 | export class NotificationsModule {} 14 | -------------------------------------------------------------------------------- /src/app/shared/components/notifications/simple-notification.component.ts: -------------------------------------------------------------------------------- 1 | import { animate, style, transition, trigger } from '@angular/animations'; 2 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 3 | 4 | import { Type } from '@app/core/interfaces/notification.interface'; 5 | 6 | @Component({ 7 | selector: 'simple-notification', 8 | template: ` 9 |
13 |
14 |
18 |
19 |
20 |
21 | 35 | 43 | 47 | 48 | 56 | 60 | 61 | 69 | 73 | 74 |
75 |
76 |

79 | {{ title }} 80 |

81 |

84 | {{ message }} 85 |

86 |
87 |
88 | 103 |
104 |
105 |
106 |
107 |
108 |
109 | `, 110 | animations: [ 111 | trigger('showHideNotification', [ 112 | transition('void => *', [ 113 | style({ transform: 'translateX(0.5rem)', opacity: 0 }), 114 | animate(300, style({ transform: 'translateX(0)', opacity: 1 })), 115 | ]), 116 | transition('* => void', [animate(100, style({ opacity: 0 }))]), 117 | ]), 118 | ], 119 | }) 120 | export class SimpleNotificationComponent { 121 | @Input('isOpen') showNotification!: boolean; 122 | @Input() class!: string; 123 | @Input() title!: string | undefined; 124 | @Input() message!: string; 125 | @Input() type: Type = 'success'; 126 | @Input() duration = 5000; 127 | 128 | @Output() toggleShowNotification: EventEmitter = 129 | new EventEmitter(); 130 | 131 | toggle() { 132 | setTimeout(() => { 133 | this.toggleShowNotification.emit(false); 134 | }, 100); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/app/shared/components/skeletons/backdrop-loader.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | @Component({ 5 | selector: 'backdrop-loader', 6 | template: ` 7 |
10 | 11 | 15 | 22 | 26 | 27 | 28 |
29 | `, 30 | }) 31 | export class BackdropLoaderComponent { 32 | @Input() loading$!: Observable; 33 | } 34 | -------------------------------------------------------------------------------- /src/app/shared/components/skeletons/skeleton.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | Input, 5 | ViewEncapsulation, 6 | } from '@angular/core'; 7 | 8 | @Component({ 9 | selector: 'skeleton', 10 | template: ` 11 |
12 | 13 |
14 | `, 15 | changeDetection: ChangeDetectionStrategy.OnPush, 16 | encapsulation: ViewEncapsulation.None, 17 | }) 18 | export class SkeletonComponent { 19 | @Input() styleClass!: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/shared/components/skeletons/skeleton.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { BackdropLoaderComponent } from './backdrop-loader.component'; 5 | import { SkeletonComponent } from './skeleton.component'; 6 | 7 | const COMPONENTS = [BackdropLoaderComponent, SkeletonComponent]; 8 | 9 | @NgModule({ 10 | declarations: COMPONENTS, 11 | imports: [CommonModule], 12 | exports: COMPONENTS, 13 | }) 14 | export class SkeletonModule {} 15 | -------------------------------------------------------------------------------- /src/app/shared/components/snippets/decrease.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'decrease-badge', 5 | template: ` 6 |
8 | 19 | Decreased by 20 | {{ value }} 21 |
22 | `, 23 | }) 24 | export class DecreaseBadgeComponent { 25 | @Input() value!: string | number; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/shared/components/snippets/increase.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'increase-badge', 5 | template: ` 6 |
8 | 19 | Increased by 20 | {{ value }} 21 |
22 | `, 23 | }) 24 | export class IncreaseBadgeComponent { 25 | @Input() value!: number | string; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/shared/components/snippets/snippet.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { DecreaseBadgeComponent } from './decrease.component'; 5 | import { IncreaseBadgeComponent } from './increase.component'; 6 | 7 | const COMPONENTS = [DecreaseBadgeComponent, IncreaseBadgeComponent]; 8 | 9 | @NgModule({ 10 | declarations: COMPONENTS, 11 | imports: [CommonModule], 12 | exports: COMPONENTS, 13 | }) 14 | export class SnippetModule {} 15 | -------------------------------------------------------------------------------- /src/app/shared/components/textarea/simple/simple.component.html: -------------------------------------------------------------------------------- 1 |
3 | 11 | 22 |
23 | 24 |

{{ helpText }}

25 |
26 | -------------------------------------------------------------------------------- /src/app/shared/components/textarea/simple/simple.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, forwardRef, Input } from '@angular/core'; 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'textarea-simple', 6 | templateUrl: './simple.component.html', 7 | providers: [ 8 | { 9 | provide: NG_VALUE_ACCESSOR, 10 | useExisting: forwardRef(() => TextareaSimpleComponent), 11 | multi: true, 12 | }, 13 | ], 14 | }) 15 | export class TextareaSimpleComponent implements ControlValueAccessor { 16 | value: string = ''; 17 | 18 | @Input() placeholder: string = ''; 19 | @Input() label!: string; 20 | @Input() name!: string; 21 | @Input() class!: string; 22 | @Input() disabled!: boolean; 23 | @Input() required!: boolean; 24 | @Input() helpText!: string; 25 | 26 | writeValue(value: string): void { 27 | if (value !== undefined) { 28 | this.value = value; 29 | this.onChange(value); 30 | } 31 | } 32 | 33 | setDisabledState(isDisabled: boolean): void { 34 | this.disabled = isDisabled; 35 | } 36 | 37 | registerOnChange(fn: (value: string) => void): void { 38 | this.onChange = fn; 39 | } 40 | 41 | registerOnTouched(fn: () => void): void { 42 | this.onTouched = fn; 43 | } 44 | 45 | onChange = (value: string) => {}; 46 | 47 | onTouched = () => {}; 48 | } 49 | -------------------------------------------------------------------------------- /src/app/shared/components/textarea/textarea.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { TextareaSimpleComponent } from './simple/simple.component'; 5 | 6 | const COMPONENTS = [TextareaSimpleComponent]; 7 | 8 | @NgModule({ 9 | declarations: COMPONENTS, 10 | imports: [CommonModule], 11 | exports: COMPONENTS, 12 | }) 13 | export class TextareaModule {} 14 | -------------------------------------------------------------------------------- /src/app/shared/directives/click-outside.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | ElementRef, 4 | EventEmitter, 5 | HostListener, 6 | Output, 7 | } from '@angular/core'; 8 | 9 | @Directive({ 10 | selector: '[ClickOutside]', 11 | }) 12 | export class ClickOutsideDirective { 13 | @Output() clickOutside = new EventEmitter(); 14 | 15 | constructor(private elementRef: ElementRef) {} 16 | 17 | @HostListener('document:click', ['$event', '$event.target']) 18 | public onClick(event: MouseEvent, targetElement: HTMLElement): void { 19 | if (!targetElement) { 20 | return; 21 | } 22 | 23 | const clickedInside = this.elementRef.nativeElement.contains(targetElement); 24 | 25 | if (!clickedInside) { 26 | this.clickOutside.emit(event); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/shared/helpers/meta-data.ts: -------------------------------------------------------------------------------- 1 | import { environment } from 'environments/environment'; 2 | 3 | export const siteTitle = environment.appName; 4 | 5 | export interface SeoInterface { 6 | [key: string]: { 7 | title: string; 8 | description?: string; 9 | keywords?: string; 10 | metaTags?: { 11 | [key: string]: string; 12 | }; 13 | }; 14 | } 15 | 16 | export const meta: SeoInterface = { 17 | '/auth/login': { 18 | title: $localize`Connexion | ${siteTitle}`, 19 | description: $localize`Connexion à votre compte`, 20 | keywords: $localize`connexion, compte, admin, cpanel`, 21 | metaTags: { 22 | 'og:url': `${environment.baseUrl}/auth/login`, 23 | }, 24 | }, 25 | '/auth/forgot-password': { 26 | title: $localize`Mot de passe oublié | ${siteTitle}`, 27 | description: $localize`Réinitialisez votre mot de passe`, 28 | keywords: $localize`mot de passe, oublié, admin, cpanel`, 29 | metaTags: { 30 | 'og:url': `${environment.baseUrl}/auth/forgot-password`, 31 | }, 32 | }, 33 | '/dashboard': { 34 | title: $localize`Tableau de bord | ${siteTitle}`, 35 | description: $localize`Tableau de bord`, 36 | keywords: $localize`tableau de bord, admin, cpanel`, 37 | metaTags: { 38 | 'og:url': `${environment.baseUrl}/dashboard`, 39 | }, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /src/app/shared/interceptors/errors.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | HttpRequest, 4 | HttpHandler, 5 | HttpEvent, 6 | HttpInterceptor, 7 | HttpErrorResponse, 8 | } from '@angular/common/http'; 9 | import { Observable, of } from 'rxjs'; 10 | import { catchError } from 'rxjs/operators'; 11 | import { Store } from '@ngrx/store'; 12 | 13 | import { logoutAction } from '@app/modules/authentication/store/auth.actions'; 14 | import { fetchFormsErrorAction } from '@app/core/store/session/session.actions'; 15 | 16 | @Injectable() 17 | export class ErrorsInterceptor implements HttpInterceptor { 18 | constructor(private store: Store) {} 19 | 20 | intercept( 21 | request: HttpRequest, 22 | next: HttpHandler 23 | ): Observable> { 24 | return next.handle(request).pipe( 25 | catchError((response: HttpErrorResponse) => { 26 | if ([403].includes(response.status)) { 27 | this.store.dispatch(logoutAction()); 28 | } 29 | 30 | if ([422].includes(response.status)) { 31 | this.store.dispatch( 32 | fetchFormsErrorAction({ formErrors: response.error }) 33 | ); 34 | } 35 | 36 | const error = response.error.message || response.statusText; 37 | return of(error); 38 | }) 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/shared/interceptors/http-loading.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | HttpRequest, 4 | HttpHandler, 5 | HttpEvent, 6 | HttpInterceptor, 7 | HttpResponse, 8 | } from '@angular/common/http'; 9 | import { BehaviorSubject, catchError, Observable, of, tap } from 'rxjs'; 10 | 11 | import { environment } from 'environments/environment'; 12 | import { LocalStorageService } from '@app/modules/authentication/services/local-storage.service'; 13 | import { LoadingService } from '../themes/services/loading.service'; 14 | 15 | @Injectable() 16 | export class HttpLoadingInterceptor implements HttpInterceptor { 17 | private httpRequestCount$: BehaviorSubject = new BehaviorSubject(0); 18 | 19 | constructor( 20 | private localStorageService: LocalStorageService, 21 | public loadingService: LoadingService 22 | ) { 23 | this.httpRequestCount$.subscribe((i: number) => { 24 | i === 0 25 | ? setTimeout(() => { 26 | this.loadingService.isLoading$.next(false); 27 | }) 28 | : setTimeout(() => { 29 | this.loadingService.isLoading$.next(true); 30 | }); 31 | }); 32 | } 33 | 34 | intercept( 35 | request: HttpRequest, 36 | next: HttpHandler 37 | ): Observable> { 38 | const isApiUrl = request.url.startsWith(environment.apiUrl); 39 | const accessToken = this.localStorageService.getAccessToken(); 40 | 41 | if (accessToken && isApiUrl) { 42 | this.httpRequestCount$.next(this.httpRequestCount$.value + 1); 43 | return next.handle(request).pipe( 44 | tap((httpEvent: HttpEvent) => { 45 | if (httpEvent instanceof HttpResponse) { 46 | this.httpRequestCount$.next(this.httpRequestCount$.value - 1); 47 | } 48 | }), 49 | catchError(error => { 50 | this.httpRequestCount$.next(this.httpRequestCount$.value - 1); 51 | return of(error); 52 | }) 53 | ); 54 | } 55 | 56 | return next.handle(request); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/menu.ts: -------------------------------------------------------------------------------- 1 | export interface Menu { 2 | group: string; 3 | items: MenuItem[]; 4 | } 5 | 6 | export interface MenuItem { 7 | title: string; 8 | svgPath: string[]; 9 | link: string; 10 | roles: string[]; 11 | soon?: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface PaginationResponse { 2 | data: T[]; 3 | pagination: Pagination; 4 | filters: IFilterParams; 5 | } 6 | 7 | export type IFilterParams = { 8 | [key: string]: string | number | boolean; 9 | }; 10 | 11 | export interface Pagination { 12 | total: number; 13 | perPage: number; 14 | currentPage: number; 15 | nextPage: string | null; 16 | prevPage: string | null; 17 | firstPage: string | null; 18 | lastPage: string | null; 19 | from: number; 20 | to: number; 21 | totalPages: number; 22 | } 23 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/state.interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface DefaultState { 2 | loading: boolean; 3 | error: string | null; 4 | message: string | null; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/values.interface.ts: -------------------------------------------------------------------------------- 1 | import { Pagination } from './response.interface'; 2 | 3 | /** 4 | * This pagination Object is the default structure of 5 | * a Custom Laravel Eloquent Pagination Response 6 | * 7 | * @see https://laravel.com/docs/eloquent-resources#pagination 8 | */ 9 | export const pagination: Pagination = { 10 | total: 0, 11 | perPage: 0, 12 | currentPage: 0, 13 | nextPage: null, 14 | prevPage: null, 15 | firstPage: null, 16 | lastPage: null, 17 | from: 0, 18 | to: 0, 19 | totalPages: 0, 20 | }; 21 | 22 | export const status = { 23 | pending: { 24 | label: 'PENDING', 25 | locale: $localize`En attente`, 26 | }, 27 | success: { 28 | label: 'SUCCESS', 29 | locale: $localize`Réussi`, 30 | }, 31 | failed: { 32 | label: 'FAILED', 33 | locale: $localize`Échoué`, 34 | }, 35 | rejected: { 36 | label: 'REJECTED', 37 | locale: $localize`Rejeté`, 38 | }, 39 | canceled: { 40 | label: 'CANCELED', 41 | locale: $localize`Annulé`, 42 | }, 43 | completed: { 44 | label: 'COMPLETED', 45 | locale: $localize`Terminé`, 46 | }, 47 | refunded: { 48 | label: 'REFUNDED', 49 | locale: $localize`Remboursé`, 50 | }, 51 | processing: { 52 | label: 'PROCESSING', 53 | locale: $localize`En cours`, 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /src/app/shared/pipes/status-color.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | import { status } from '@app/shared/interfaces/values.interface'; 4 | 5 | @Pipe({ 6 | name: 'statusColor' 7 | }) 8 | export class StatusColorPipe implements PipeTransform { 9 | transform(value: string): string { 10 | switch (value) { 11 | case status.pending.label: 12 | return 'bg-orange-100 text-orange-800'; 13 | case status.success.label: 14 | return 'bg-green-100 text-green-800'; 15 | case status.failed.label: 16 | return 'bg-red-100 text-red-800'; 17 | case status.rejected.label: 18 | return 'bg-pink-100 text-pink-800'; 19 | case status.canceled.label: 20 | return 'bg-rose-100 text-rose-800'; 21 | default: 22 | return 'bg-slate-100 text-slate-800'; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/shared/pipes/status-value.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | import { status } from '@app/shared/interfaces/values.interface'; 4 | 5 | @Pipe({ 6 | name: 'statusValue', 7 | }) 8 | export class StatusValuePipe implements PipeTransform { 9 | transform(value: string): string { 10 | switch (value) { 11 | case status.pending.label: 12 | return status.pending.locale; 13 | case status.success.label: 14 | return status.success.locale; 15 | case status.failed.label: 16 | return status.failed.locale; 17 | case status.rejected.label: 18 | return status.rejected.locale; 19 | case status.canceled.label: 20 | return status.canceled.locale; 21 | default: 22 | return $localize`Pas disponible`; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/shared/rules/password.rule.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl, ValidatorFn } from '@angular/forms'; 2 | 3 | export class PasswordRules { 4 | static match(controlName: string, checkControlName: string): ValidatorFn { 5 | return (controls: AbstractControl) => { 6 | const control = controls.get(controlName); 7 | const checkControl = controls.get(checkControlName); 8 | if (checkControl?.errors && !checkControl.errors['matching']) { 9 | return null; 10 | } 11 | if (control?.value !== checkControl?.value) { 12 | controls.get(checkControlName)?.setErrors({ matching: true }); 13 | return { matching: true }; 14 | } else { 15 | return null; 16 | } 17 | }; 18 | } 19 | } -------------------------------------------------------------------------------- /src/app/shared/rules/whitespace.rule.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl, ValidatorFn } from '@angular/forms'; 2 | 3 | export class WhiteSpaceRule { 4 | static noWhitespace(controlName: string): ValidatorFn { 5 | return (controls: AbstractControl) => { 6 | const control = controls.get(controlName); 7 | const isWhitespace = (control?.value || '').trim().length === 0; 8 | const isValid = !isWhitespace; 9 | return isValid ? null : { whitespace: true }; 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/services/seo.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnDestroy } from '@angular/core'; 2 | import { Router, NavigationEnd } from '@angular/router'; 3 | import { Subscription } from 'rxjs'; 4 | import { Meta, MetaDefinition, Title } from '@angular/platform-browser'; 5 | 6 | import { meta } from '../helpers/meta-data'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class SeoService implements OnDestroy { 12 | private subscription = new Subscription(); 13 | 14 | constructor( 15 | private router: Router, 16 | private meta: Meta, 17 | private title: Title 18 | ) { 19 | this.subscription.add( 20 | this.router.events.subscribe(event => { 21 | if (event instanceof NavigationEnd) { 22 | const url = event.url; 23 | this.updateTitle(url); 24 | this.updateMeta(url); 25 | } 26 | }) 27 | ); 28 | } 29 | 30 | private updateTitle(url: string): void { 31 | this.title.setTitle(meta[url].title); 32 | } 33 | 34 | private updateMeta(url: string): void { 35 | const oldTagOgTitle = this.meta.getTag('property="og:title"'); 36 | const newTagOgTitle = { 37 | property: 'og:title', 38 | content: meta[url].title, 39 | }; 40 | const oldTagTwitterTitle = this.meta.getTag('name="twitter:title"'); 41 | const newTagTwitterTitle = { 42 | name: 'twitter:title', 43 | content: meta[url].title, 44 | }; 45 | const oldTagDescription = this.meta.getTag('name="description"'); 46 | const newTagDescription = { 47 | name: 'description', 48 | content: meta[url].description, 49 | }; 50 | const oldTagOgDescription = this.meta.getTag('property="og:description"'); 51 | const newTagOgDescription = { 52 | property: 'og:description', 53 | content: meta[url].description, 54 | }; 55 | const oldTagTwitterDescription = this.meta.getTag( 56 | 'property="og:description"' 57 | ); 58 | const newTagTwitterDescription = { 59 | property: 'og:description', 60 | content: meta[url].description, 61 | }; 62 | const oldTagOgImage = this.meta.getTag('property="og:image"'); 63 | const imageTag = 64 | meta[url].metaTags?.['image'] ?? 65 | this.meta.getTag('property="og:image"')!.content; 66 | const newTagOgImage = { 67 | property: 'og:image', 68 | content: imageTag, 69 | }; 70 | const oldTagTwitterImage = this.meta.getTag('name="twitter:image"'); 71 | const newTagTwitterImage = { 72 | name: 'twitter:image', 73 | content: imageTag, 74 | }; 75 | const oldTagOgUrl = this.meta.getTag('property="og:url"'); 76 | const newTagOgUrl = { 77 | property: 'og:url', 78 | content: meta[url].metaTags?.['og:url'], 79 | }; 80 | const oldTagKeywords = this.meta.getTag('name="keywords"'); 81 | const newTagKeywords = { 82 | name: 'keywords', 83 | content: meta[url].keywords, 84 | }; 85 | 86 | // Update description 87 | oldTagDescription 88 | ? this.meta.updateTag(newTagDescription as MetaDefinition) 89 | : this.meta.addTag(newTagDescription as MetaDefinition); 90 | // Update og:description 91 | oldTagOgDescription 92 | ? this.meta.updateTag(newTagOgDescription as MetaDefinition) 93 | : this.meta.addTag(newTagOgDescription as MetaDefinition); 94 | // Update twitter:description 95 | oldTagTwitterDescription 96 | ? this.meta.updateTag(newTagTwitterDescription as MetaDefinition) 97 | : this.meta.addTag(newTagTwitterDescription as MetaDefinition); 98 | // Update og:title 99 | oldTagOgTitle 100 | ? this.meta.updateTag(newTagOgTitle as MetaDefinition) 101 | : this.meta.addTag(newTagOgTitle as MetaDefinition); 102 | // Update twitter:title 103 | oldTagTwitterTitle 104 | ? this.meta.updateTag(newTagTwitterTitle as MetaDefinition) 105 | : this.meta.addTag(newTagTwitterTitle as MetaDefinition); 106 | // Update og:image 107 | oldTagOgImage 108 | ? this.meta.updateTag(newTagOgImage as MetaDefinition) 109 | : this.meta.addTag(newTagOgImage as MetaDefinition); 110 | // Update twitter:image 111 | oldTagTwitterImage 112 | ? this.meta.updateTag(newTagTwitterImage as MetaDefinition) 113 | : this.meta.addTag(newTagTwitterImage as MetaDefinition); 114 | // Update og:url 115 | oldTagOgUrl 116 | ? this.meta.updateTag(newTagOgUrl as MetaDefinition) 117 | : this.meta.addTag(newTagOgUrl as MetaDefinition); 118 | // Update keywords 119 | oldTagKeywords 120 | ? this.meta.updateTag(newTagKeywords as MetaDefinition) 121 | : this.meta.addTag(newTagKeywords as MetaDefinition); 122 | } 123 | 124 | ngOnDestroy() { 125 | this.subscription.unsubscribe(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NgxPermissionsModule } from 'ngx-permissions'; 3 | 4 | import { ThemeModule } from './themes/theme.module'; 5 | import { InputsModule } from './components/inputs/inputs.module'; 6 | import { AlertModule } from './components/alert/alert.module'; 7 | import { ButtonModule } from './components/buttons/button.module'; 8 | import { HeadingModule } from './components/headings/heading.module'; 9 | import { SkeletonModule } from './components/skeletons/skeleton.module'; 10 | import { SnippetModule } from './components/snippets/snippet.module'; 11 | import { TextareaModule } from './components/textarea/textarea.module'; 12 | 13 | import { ClickOutsideDirective } from './directives/click-outside.directive'; 14 | import { StatusColorPipe } from './pipes/status-color.pipe'; 15 | import { StatusValuePipe } from './pipes/status-value.pipe'; 16 | import { NotificationsModule } from './components/notifications/notifications.module'; 17 | 18 | const DECLARATIONS = [ClickOutsideDirective, StatusColorPipe, StatusValuePipe]; 19 | const MODULES = [ 20 | AlertModule, 21 | ThemeModule, 22 | ButtonModule, 23 | InputsModule, 24 | HeadingModule, 25 | SkeletonModule, 26 | SnippetModule, 27 | TextareaModule, 28 | NotificationsModule 29 | ]; 30 | 31 | @NgModule({ 32 | declarations: DECLARATIONS, 33 | imports: [...MODULES], 34 | exports: [...DECLARATIONS, ...MODULES, NgxPermissionsModule], 35 | }) 36 | export class SharedModule {} 37 | -------------------------------------------------------------------------------- /src/app/shared/themes/components/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 13 |
14 |
15 | 16 | 17 | 18 | 27 |
28 | 29 |
30 | 49 |
50 | 51 | 67 | 68 | 84 | 85 |
86 | 87 | 118 |
    126 |
  • 127 | 149 |
    {{ theme.name }}
    150 |
  • 151 |
152 |
153 | 154 | 155 |
156 | 169 | 209 |
210 |
211 |
212 |
213 |
214 |
215 | -------------------------------------------------------------------------------- /src/app/shared/themes/components/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '@ngrx/store'; 2 | import { Observable } from 'rxjs'; 3 | import { 4 | Component, 5 | ElementRef, 6 | EventEmitter, 7 | HostListener, 8 | OnInit, 9 | Output, 10 | ViewChild, 11 | } from '@angular/core'; 12 | import { 13 | animate, 14 | state, 15 | style, 16 | transition, 17 | trigger, 18 | } from '@angular/animations'; 19 | 20 | import { User } from '@app/modules/user/interfaces/user.interface'; 21 | import { 22 | selectCurrentUser, 23 | selectLoading, 24 | } from '@app/modules/authentication/store/auth.selectors'; 25 | import { logoutAction } from '@app/modules/authentication/store/auth.actions'; 26 | 27 | @Component({ 28 | selector: 'admin-header', 29 | templateUrl: './header.component.html', 30 | animations: [ 31 | trigger('openClose', [ 32 | state('open', style({ opacity: 1, transform: 'scale(1, 1)' })), 33 | state('closed', style({ opacity: 0, transform: 'scale(0.95, 0.95)' })), 34 | transition('open => closed', [animate('100ms ease-in')]), 35 | transition('closed => open', [animate('200ms ease-out')]), 36 | ]), 37 | ], 38 | }) 39 | export class HeaderComponent implements OnInit { 40 | mobileMenuOpen!: boolean; 41 | currentTheme!: string; 42 | showDialog: boolean = false; 43 | themes: { name: string; value: string }[] = [ 44 | { name: $localize`Clair`, value: 'light' }, 45 | { name: $localize`Sombre`, value: 'dark' }, 46 | { name: $localize`Système`, value: 'system' }, 47 | ]; 48 | 49 | @ViewChild('menuDropdown') menuDropdown!: ElementRef; 50 | 51 | @Output() private openMobileSidebar: EventEmitter = 52 | new EventEmitter(); 53 | 54 | public user$: Observable = this.store.select(selectCurrentUser); 55 | public loading$: Observable = this.store.select(selectLoading); 56 | 57 | constructor(private store: Store) {} 58 | 59 | ngOnInit(): void { 60 | const selectedTheme = window.localStorage.getItem('theme'); 61 | 62 | if (selectedTheme) { 63 | document.documentElement.setAttribute('data-theme', selectedTheme); 64 | } else { 65 | const theme = this.themes.find( 66 | theme => 67 | theme.value === document.documentElement.getAttribute('data-theme') 68 | ); 69 | window.localStorage.setItem('theme', theme!.value); 70 | } 71 | 72 | this.currentTheme = window.localStorage.getItem('theme')!; 73 | } 74 | 75 | get openCloseTrigger() { 76 | return this.mobileMenuOpen ? 'open' : 'closed'; 77 | } 78 | 79 | logout(): void { 80 | this.store.dispatch(logoutAction()); 81 | } 82 | 83 | toggleMobileMenu(): void { 84 | this.showDialog = false; 85 | this.mobileMenuOpen = !this.mobileMenuOpen; 86 | } 87 | 88 | openSidebar(): void { 89 | this.openMobileSidebar.emit(true); 90 | } 91 | 92 | @HostListener('document:click', ['$event.target']) 93 | public onPageClick(targetElement: HTMLElement): void { 94 | const clickedInside = 95 | this.menuDropdown.nativeElement.contains(targetElement); 96 | 97 | if (!clickedInside) { 98 | this.mobileMenuOpen = false; 99 | } 100 | } 101 | 102 | updateTheme(theme: string): void { 103 | document.documentElement.setAttribute('data-theme', theme); 104 | window.localStorage.setItem('theme', theme); 105 | 106 | this.currentTheme = theme; 107 | this.showDialog = false; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/app/shared/themes/components/logo/logo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'logo-svg', 5 | template: ` 6 | 11 | 14 | 17 | 20 | 23 | 26 | 29 | 32 | 36 | 38 | 39 | `, 40 | }) 41 | export class LogoComponent { 42 | @Input() class!: string; 43 | } 44 | -------------------------------------------------------------------------------- /src/app/shared/themes/components/sidebar/sidebar.component.html: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /src/app/shared/themes/components/sidebar/sidebar.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .menu { 3 | @apply flex items-center px-6 py-2 text-sm font-medium border-l-4 border-transparent; 4 | 5 | &-current { 6 | @apply border-primary-600; 7 | color: theme('colors.primary.600') !important; 8 | &:hover { 9 | color: theme('colors.primary.600') !important; 10 | } 11 | 12 | svg { 13 | @apply group-hover:text-primary-600; 14 | stroke: theme('colors.primary.600') !important; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/shared/themes/components/sidebar/sidebar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | 3 | import { Menu } from '@app/shared/interfaces/menu'; 4 | import { SidebarService } from '../../services/sidebar.service'; 5 | 6 | @Component({ 7 | selector: 'admin-sidebar', 8 | templateUrl: './sidebar.component.html', 9 | styleUrls: ['./sidebar.component.scss'], 10 | }) 11 | export class SidebarComponent implements OnInit { 12 | @Input() menus!: Menu[]; 13 | 14 | constructor(private sidebarService: SidebarService) {} 15 | 16 | ngOnInit(): void { 17 | this.menus = this.sidebarService.renderSidebar(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/shared/themes/layouts/auth/auth.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 |
7 |

Copyright © {{ date }} Laravel Cameroun.

8 | 22 |
23 |
24 |
25 | 26 | 33 |
34 | -------------------------------------------------------------------------------- /src/app/shared/themes/layouts/auth/auth.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'auth-layout', 5 | templateUrl: './auth.component.html', 6 | }) 7 | export class AuthComponent { 8 | date: number = new Date().getFullYear(); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/shared/themes/layouts/cpanel/cpanel.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |
8 | 9 |
10 |
11 | 12 | 13 | 14 |
15 | 25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 | 36 |
37 |
38 |
39 |
40 |
-------------------------------------------------------------------------------- /src/app/shared/themes/layouts/cpanel/cpanel.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { 3 | animate, 4 | state, 5 | style, 6 | transition, 7 | trigger, 8 | } from '@angular/animations'; 9 | 10 | import { LoadingService } from '../../services/loading.service'; 11 | 12 | @Component({ 13 | selector: 'cpanel-layout', 14 | templateUrl: './cpanel.component.html', 15 | animations: [ 16 | trigger('openClose', [ 17 | state('open', style({ transform: 'translateX(0%)' })), 18 | state('closed', style({ transform: 'translateX(-100%)' })), 19 | transition('open => closed', [animate('300ms ease-in-out')]), 20 | transition('closed => open', [animate('300ms ease-in-out')]), 21 | ]), 22 | trigger('openBackdrop', [ 23 | state('open', style({ opacity: 1, transitionProperty: 'opacity' })), 24 | state('closed', style({ opacity: 0, transitionProperty: 'opacity' })), 25 | transition('open => closed', [ 26 | animate('300ms cubic-bezier(0.4, 0, 0.2, 1)'), 27 | ]), 28 | transition('closed => open', [ 29 | animate('300ms cubic-bezier(0.4, 0, 0.2, 1)'), 30 | ]), 31 | ]), 32 | trigger('closeButton', [ 33 | state('open', style({ opacity: 1 })), 34 | state('closed', style({ opacity: 0 })), 35 | transition('open => closed', [animate('300ms ease-in-out')]), 36 | transition('closed => open', [animate('300ms ease-in-out')]), 37 | ]), 38 | ], 39 | }) 40 | export class CpanelComponent implements OnInit { 41 | loading!: boolean; 42 | mobileSidebarOpen!: boolean; 43 | 44 | toggleSidebar(): void { 45 | this.mobileSidebarOpen = !this.mobileSidebarOpen; 46 | } 47 | 48 | openSidebar($event: boolean): void { 49 | this.mobileSidebarOpen = $event; 50 | } 51 | 52 | get openCloseTrigger() { 53 | return this.mobileSidebarOpen ? 'open' : 'closed'; 54 | } 55 | 56 | constructor(public loadingService: LoadingService) {} 57 | 58 | ngOnInit(): void { 59 | this.loadingService.isLoading$.subscribe( 60 | loading => (this.loading = loading) 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/app/shared/themes/pages/not-found/not-found.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | Administration 6 | 7 |
8 |
9 |
10 |
11 |

404

12 |

Page introuvable.

13 |

Désolé, nous n'avons pas trouvé la page que vous recherchez.

14 | 20 |
21 |
22 |
23 | 34 |
35 | -------------------------------------------------------------------------------- /src/app/shared/themes/pages/not-found/not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { LocalStorageService } from '@app/modules/authentication/services/local-storage.service'; 5 | 6 | @Component({ 7 | templateUrl: './not-found.component.html', 8 | }) 9 | export class NotFoundComponent implements OnInit { 10 | homeUrl!: string; 11 | isLoggedIn: boolean = false; 12 | 13 | constructor( 14 | private localStorageService: LocalStorageService, 15 | private router: Router 16 | ) {} 17 | 18 | ngOnInit(): void { 19 | const token = this.localStorageService.getAccessToken(); 20 | 21 | this.isLoggedIn = this.localStorageService.getAccessToken() ? true : false; 22 | 23 | if (!token) { 24 | this.router.navigateByUrl('/auth/login'); 25 | } 26 | 27 | this.homeUrl = '/dashboard'; 28 | } 29 | 30 | redirectToHome() { 31 | this.router.navigateByUrl(`${this.homeUrl}`); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/shared/themes/services/loading.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class LoadingService { 8 | public isLoading$: BehaviorSubject = new BehaviorSubject( 9 | false 10 | ); 11 | 12 | constructor() {} 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/themes/services/sidebar.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { adminMenu } from '@modules/dashboard/navigation/admin.menu'; 4 | import { LocalStorageService } from '@app/modules/authentication/services/local-storage.service'; 5 | import { Menu } from '@app/shared/interfaces/menu'; 6 | import { ADMIN_ROLE, DEVELOPER_ROLE } from '@app/core/guards/user.roles'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class SidebarService { 12 | menus!: Menu[]; 13 | 14 | constructor(private localStorageService: LocalStorageService) {} 15 | 16 | renderSidebar(): Menu[] { 17 | const roles = this.localStorageService.getLocalStorage('roles')!; 18 | const USER_ROLES: string[] = JSON.parse(roles); 19 | 20 | if ( 21 | USER_ROLES.includes(ADMIN_ROLE) || 22 | USER_ROLES.includes(DEVELOPER_ROLE) 23 | ) { 24 | this.menus = adminMenu; 25 | } 26 | 27 | return this.menus; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/shared/themes/theme.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | import { MatProgressBarModule } from '@angular/material/progress-bar'; 5 | import { MatSlideToggleModule } from '@angular/material/slide-toggle'; 6 | import { MatRippleModule } from '@angular/material/core'; 7 | import { NgxPermissionsModule } from 'ngx-permissions'; 8 | 9 | import { SkeletonModule } from '../components/skeletons/skeleton.module'; 10 | 11 | import { AuthComponent } from './layouts/auth/auth.component'; 12 | import { CpanelComponent } from './layouts/cpanel/cpanel.component'; 13 | import { SidebarComponent } from './components/sidebar/sidebar.component'; 14 | import { LogoComponent } from './components/logo/logo.component'; 15 | import { HeaderComponent } from './components/header/header.component'; 16 | import { NotFoundComponent } from './pages/not-found/not-found.component'; 17 | 18 | const MODULES = [ 19 | CommonModule, 20 | RouterModule, 21 | MatProgressBarModule, 22 | MatSlideToggleModule, 23 | MatRippleModule, 24 | SkeletonModule, 25 | ]; 26 | 27 | @NgModule({ 28 | declarations: [ 29 | AuthComponent, 30 | CpanelComponent, 31 | SidebarComponent, 32 | LogoComponent, 33 | HeaderComponent, 34 | NotFoundComponent, 35 | ], 36 | imports: [ 37 | ...MODULES, 38 | NgxPermissionsModule.forChild({ 39 | rolesIsolate: true, 40 | }), 41 | ], 42 | exports: [LogoComponent, ...MODULES], 43 | }) 44 | export class ThemeModule {} 45 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravelcm/angular-admin-panel/9281111b3444e0236f79dbbcbeb9487e7528eade/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravelcm/angular-admin-panel/9281111b3444e0236f79dbbcbeb9487e7528eade/src/assets/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/assets/favicons/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravelcm/angular-admin-panel/9281111b3444e0236f79dbbcbeb9487e7528eade/src/assets/favicons/android-chrome-384x384.png -------------------------------------------------------------------------------- /src/assets/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravelcm/angular-admin-panel/9281111b3444e0236f79dbbcbeb9487e7528eade/src/assets/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravelcm/angular-admin-panel/9281111b3444e0236f79dbbcbeb9487e7528eade/src/assets/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravelcm/angular-admin-panel/9281111b3444e0236f79dbbcbeb9487e7528eade/src/assets/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravelcm/angular-admin-panel/9281111b3444e0236f79dbbcbeb9487e7528eade/src/assets/favicons/favicon.ico -------------------------------------------------------------------------------- /src/assets/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravelcm/angular-admin-panel/9281111b3444e0236f79dbbcbeb9487e7528eade/src/assets/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /src/assets/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 17 | 22 | 27 | 29 | 32 | 36 | 42 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/assets/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-384x384.png", 12 | "sizes": "384x384", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravelcm/angular-admin-panel/9281111b3444e0236f79dbbcbeb9487e7528eade/src/assets/images/.gitkeep -------------------------------------------------------------------------------- /src/assets/images/wallpaper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravelcm/angular-admin-panel/9281111b3444e0236f79dbbcbeb9487e7528eade/src/assets/images/wallpaper.jpg -------------------------------------------------------------------------------- /src/assets/json/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravelcm/angular-admin-panel/9281111b3444e0236f79dbbcbeb9487e7528eade/src/assets/json/.gitkeep -------------------------------------------------------------------------------- /src/assets/svg/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravelcm/angular-admin-panel/9281111b3444e0236f79dbbcbeb9487e7528eade/src/assets/svg/.gitkeep -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare var process: { 2 | env: { 3 | NG_APP_NAME: string; 4 | NG_APP_DEBUG: false; 5 | NG_APP_VERSION: string; 6 | NG_APP_BASE_URL: string; 7 | NG_APP_API_URL: string; 8 | NG_APP_API_VERSION: string; 9 | NG_APP_SENTRY_DSN: string | undefined; 10 | NG_APP_SENTRY_TRACES_SAMPLE_RATE: number | undefined; 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | appName: process.env.NG_APP_NAME, 4 | debug: process.env.NG_APP_DEBUG, 5 | version: process.env.NG_APP_VERSION, 6 | baseUrl: process.env.NG_APP_BASE_URL, 7 | apiUrl: process.env.NG_APP_API_URL, 8 | apiVersion: process.env.NG_APP_API_VERSION, 9 | sentryDsn: process.env.NG_APP_SENTRY_DSN, 10 | sentryTracesSampleRate: process.env.NG_APP_SENTRY_TRACES_SAMPLE_RATE, 11 | }; 12 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | appName: process.env.NG_APP_NAME, 8 | debug: process.env.NG_APP_DEBUG, 9 | version: process.env.NG_APP_VERSION, 10 | baseUrl: process.env.NG_APP_BASE_URL, 11 | apiUrl: process.env.NG_APP_API_URL, 12 | apiVersion: process.env.NG_APP_API_VERSION, 13 | sentryDsn: process.env.NG_APP_SENTRY_DSN, 14 | sentryTracesSampleRate: process.env.NG_APP_SENTRY_TRACES_SAMPLE_RATE, 15 | }; 16 | 17 | /* 18 | * For easier debugging in development mode, you can import the following file 19 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 20 | * 21 | * This import should be commented out in production mode because it will have a negative impact 22 | * on performance if an error is thrown. 23 | */ 24 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 25 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Admin - Cpanel 6 | 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 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import * as Sentry from '@sentry/angular'; 4 | import { BrowserTracing } from '@sentry/tracing'; 5 | 6 | import { AppModule } from './app/app.module'; 7 | import { environment } from './environments/environment'; 8 | 9 | Sentry.init({ 10 | dsn: environment.sentryDsn, 11 | integrations: [ 12 | new BrowserTracing({ 13 | tracingOrigins: ['localhost', environment.apiUrl], 14 | routingInstrumentation: Sentry.routingInstrumentation, 15 | }), 16 | ], 17 | 18 | // Set tracesSampleRate to 1.0 to capture 100% 19 | // of transactions for performance monitoring. 20 | // We recommend adjusting this value in production 21 | tracesSampleRate: environment.sentryTracesSampleRate, 22 | }); 23 | 24 | if (environment.production) { 25 | enableProdMode(); 26 | } 27 | 28 | platformBrowserDynamic() 29 | .bootstrapModule(AppModule) 30 | .catch(err => console.error(err)); 31 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. 3 | */ 4 | import '@angular/localize/init'; 5 | 6 | /** 7 | * This file includes polyfills needed by Angular and is loaded before the app. 8 | * You can add your own extra polyfills to this file. 9 | * 10 | * This file is divided into 2 sections: 11 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 12 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 13 | * file. 14 | * 15 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 16 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 17 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 18 | * 19 | * Learn more in https://angular.io/guide/browser-support 20 | */ 21 | 22 | /*************************************************************************************************** 23 | * BROWSER POLYFILLS 24 | */ 25 | 26 | /** 27 | * By default, zone.js will patch all possible macroTask and DomEvents 28 | * user can disable parts of macroTask/DomEvents patch by setting following flags 29 | * because those flags need to be set before `zone.js` being loaded, and webpack 30 | * will put import in the top of bundle, so user need to create a separate file 31 | * in this directory (for example: zone-flags.ts), and put the following flags 32 | * into that file, and then add the following code before importing zone.js. 33 | * import './zone-flags'; 34 | * 35 | * The flags allowed in zone-flags.ts are listed here. 36 | * 37 | * The following flags will work for all browsers. 38 | * 39 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 40 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 41 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 42 | * 43 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 44 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 45 | * 46 | * (window as any).__Zone_enable_cross_context_check = true; 47 | * 48 | */ 49 | 50 | /*************************************************************************************************** 51 | * Zone JS is required by default for Angular itself. 52 | */ 53 | import 'zone.js'; // Included with Angular CLI. 54 | 55 | 56 | /*************************************************************************************************** 57 | * APPLICATION IMPORTS 58 | */ 59 | -------------------------------------------------------------------------------- /src/sass/_base.scss: -------------------------------------------------------------------------------- 1 | // Base style 2 | html, body { height: 100%; } 3 | -------------------------------------------------------------------------------- /src/sass/_material.scss: -------------------------------------------------------------------------------- 1 | // Custom Theming for Angular Material 2 | // For more information: https://material.angular.io/guide/theming 3 | @use '@angular/material' as mat; 4 | 5 | // Include the common styles for Angular Material. We include this here so that you only 6 | // have to load a single css file for Angular Material in your app. 7 | // Be sure that you only ever include this mixin once! 8 | @include mat.core(); 9 | 10 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 11 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 12 | // hue. Available color palettes: https://material.io/design/color/ 13 | $admin-primary: mat.define-palette(mat.$green-palette); 14 | $admin-accent: mat.define-palette(mat.$blue-palette); 15 | 16 | // The warn palette is optional (defaults to orange). 17 | $admin-warn: mat.define-palette(mat.$amber-palette); 18 | 19 | // Create the theme object. A theme consists of configurations for individual 20 | // theming systems such as "color" or "typography". 21 | $admin-theme: mat.define-light-theme(( 22 | color: ( 23 | primary: $admin-primary, 24 | accent: $admin-accent, 25 | warn: $admin-warn, 26 | ) 27 | )); 28 | 29 | // Include theme styles for core and each component used in your app. 30 | // Alternatively, you can import and @include the theme mixins for each component 31 | // that you are using. 32 | @include mat.all-component-themes($admin-theme); -------------------------------------------------------------------------------- /src/sass/_plugin.scss: -------------------------------------------------------------------------------- 1 | // Library custom style -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Style.scss 3 | * This is the main stylesheet for this project. 4 | * 5 | * @author Arthur Monney 6 | * @version 1.0.0 7 | * @license MIT 8 | * @since September 2022 9 | * @see https://design.cosna-afrique.com for more information. 10 | * 11 | */ 12 | 13 | @import 'tailwindcss/base'; 14 | @import 'tailwindcss/components'; 15 | @import 'tailwindcss/utilities'; 16 | 17 | @import 'sass/_base'; 18 | @import 'sass/_material'; 19 | @import 'sass/_plugin'; 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require('tailwindcss/defaultTheme'); 2 | const colors = require('tailwindcss/colors'); 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | module.exports = { 6 | important: true, 7 | darkMode: 'class', 8 | content: [ 9 | './src/**/*.{html,ts}', 10 | ], 11 | theme: { 12 | extend: { 13 | colors: { 14 | primary: colors.green, 15 | secondary: colors.emerald 16 | }, 17 | fontFamily: { 18 | sans: ['Inter var', ...fontFamily.sans], 19 | mono: ['Roboto Mono', ...fontFamily.mono], 20 | }, 21 | maxWidth: { 22 | '8xl': '90rem', 23 | }, 24 | screens: { 25 | '2xl': '1536px', 26 | } 27 | }, 28 | }, 29 | plugins: [ 30 | require('@tailwindcss/aspect-ratio'), 31 | require('@tailwindcss/forms'), 32 | require('@tailwindcss/typography'), 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /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": "./src", 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": "es2017", 20 | "module": "es2020", 21 | "lib": [ 22 | "es2020", 23 | "dom" 24 | ], 25 | "paths": { 26 | "@app/*": ["app/*"], 27 | "@shared/*": ["app/shared/*"], 28 | "@modules/*": ["app/modules/*"], 29 | "@core/*": ["app/core/*"] 30 | } 31 | }, 32 | "angularCompilerOptions": { 33 | "enableI18nLegacyMessageIdFormat": false, 34 | "strictInjectionParameters": true, 35 | "strictInputAccessModifiers": true, 36 | "strictTemplates": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------