feat: add DCB BO admin dashboard - Authentication system with JWT - Modular architecture with services for each feature
This commit is contained in:
commit
ab55d6c59c
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
ij_typescript_use_double_quotes = false
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# Node
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"useTabs": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"arrowParens": "always"
|
||||||
|
}
|
||||||
|
|
||||||
59
README.md
Normal file
59
README.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Simple
|
||||||
|
|
||||||
|
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.0.2.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
To start a local development server, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng generate component component-name
|
||||||
|
```
|
||||||
|
|
||||||
|
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng generate --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To build the project run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
For end-to-end (e2e) testing, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||||
135
angular.json
Normal file
135
angular.json
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"simple": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular/build:application",
|
||||||
|
"options": {
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"@angular/localize/init"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"stylePreprocessorOptions": {
|
||||||
|
"sass": {
|
||||||
|
"silenceDeprecations": [
|
||||||
|
"color-functions",
|
||||||
|
"global-builtin",
|
||||||
|
"import",
|
||||||
|
"mixed-decls"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
},
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss",
|
||||||
|
"quill/dist/quill.bubble.css"
|
||||||
|
],
|
||||||
|
"scripts": [
|
||||||
|
"node_modules/jquery/dist/jquery.js",
|
||||||
|
"node_modules/jszip/dist/jszip.js",
|
||||||
|
"node_modules/datatables.net/js/dataTables.js",
|
||||||
|
"node_modules/datatables.net-buttons/js/dataTables.buttons.min.js",
|
||||||
|
"node_modules/datatables.net-buttons/js/buttons.colVis.min.js",
|
||||||
|
"node_modules/datatables.net-buttons/js/buttons.html5.js",
|
||||||
|
"node_modules/datatables.net-buttons/js/buttons.print.min.js",
|
||||||
|
"node_modules/datatables.net-select/js/dataTables.select.min.js",
|
||||||
|
"node_modules/datatables.net-fixedheader/js/dataTables.fixedHeader.js"
|
||||||
|
],
|
||||||
|
"allowedCommonJsDependencies": [
|
||||||
|
"jsvectormap/dist/maps/world-merc.js",
|
||||||
|
"jsvectormap/dist/maps/world.js",
|
||||||
|
"jsvectormap/dist/maps/us-aea-en.js",
|
||||||
|
"jsvectormap/dist/maps/canada.js",
|
||||||
|
"jsvectormap/dist/maps/russia.js",
|
||||||
|
"jsvectormap/dist/maps/iraq.js",
|
||||||
|
"jsvectormap/dist/maps/spain.js",
|
||||||
|
"@/assets/js/in-mill-en.js",
|
||||||
|
"leaflet",
|
||||||
|
"quill-delta",
|
||||||
|
"jsvectormap",
|
||||||
|
"dropzone"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "8MB",
|
||||||
|
"maximumError": "8MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "4kB",
|
||||||
|
"maximumError": "8MB"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular/build:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "simple:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "simple:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular/build:extract-i18n"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular/build:karma",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": "ea3963b1-124e-4d56-8eff-392a70b78193"
|
||||||
|
}
|
||||||
|
}
|
||||||
76
generate-modules copy.js
Normal file
76
generate-modules copy.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// generate-modules.js
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const modules = [
|
||||||
|
{ name: 'dashboard', children: [] },
|
||||||
|
{ name: 'team', children: [] },
|
||||||
|
{ name: 'transactions', children: ['list', 'filters', 'details', 'export'] },
|
||||||
|
{ name: 'merchants', children: ['list', 'config', 'history'] },
|
||||||
|
{ name: 'operators', children: ['config', 'stats'] },
|
||||||
|
{ name: 'notifications', children: ['list', 'filters', 'actions'] },
|
||||||
|
{ name: 'webhooks', children: ['history', 'status', 'retry'] },
|
||||||
|
{ name: 'users', children: ['list', 'roles'] },
|
||||||
|
{ name: 'settings', children: [] },
|
||||||
|
{ name: 'integrations', children: [] },
|
||||||
|
{ name: 'support', children: [] },
|
||||||
|
{ name: 'profile', children: [] },
|
||||||
|
{ name: 'documentation', children: [] },
|
||||||
|
{ name: 'help', children: [] },
|
||||||
|
{ name: 'about', children: [] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const baseDir = path.join(__dirname, 'src/app/modules');
|
||||||
|
|
||||||
|
function createModule(module) {
|
||||||
|
const modulePath = path.join(baseDir, module.name);
|
||||||
|
fs.mkdirSync(modulePath, { recursive: true });
|
||||||
|
|
||||||
|
// components folder
|
||||||
|
fs.mkdirSync(path.join(modulePath, 'components'), { recursive: true });
|
||||||
|
|
||||||
|
// main module files
|
||||||
|
fs.writeFileSync(path.join(modulePath, `${module.name}.ts`), `
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-${module.name}',
|
||||||
|
templateUrl: './${module.name}.html',
|
||||||
|
})
|
||||||
|
export class ${capitalize(module.name)} {}
|
||||||
|
`.trim());
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(modulePath, `${module.name}.html`), `<p>${capitalize(module.name)}</p>`);
|
||||||
|
fs.writeFileSync(path.join(modulePath, `${module.name}.spec.ts`), `
|
||||||
|
import { ${capitalize(module.name)} } from './${module.name}';
|
||||||
|
describe('${capitalize(module.name)}', () => {});
|
||||||
|
`.trim());
|
||||||
|
|
||||||
|
// children
|
||||||
|
module.children.forEach(child => {
|
||||||
|
const childPath = path.join(modulePath, child);
|
||||||
|
fs.mkdirSync(childPath, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(childPath, `${child}.ts`), `
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-${module.name}-${child}',
|
||||||
|
templateUrl: './${child}.html',
|
||||||
|
})
|
||||||
|
export class ${capitalize(module.name)}${capitalize(child)} {}
|
||||||
|
`.trim());
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(childPath, `${child}.html`), `<p>${capitalize(module.name)} - ${capitalize(child)}</p>`);
|
||||||
|
fs.writeFileSync(path.join(childPath, `${child}.spec.ts`), `
|
||||||
|
import { ${capitalize(module.name)}${capitalize(child)} } from './${child}';
|
||||||
|
describe('${capitalize(module.name)}${capitalize(child)}', () => {});
|
||||||
|
`.trim());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function capitalize(str) {
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
modules.forEach(createModule);
|
||||||
|
console.log('Modules generated successfully in src/app/modules');
|
||||||
103
generate-modules.js
Normal file
103
generate-modules.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// generate-modules-with-services-folder.js
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const modules = [
|
||||||
|
{ name: 'transactions', children: ['list', 'filters', 'details', 'export'] },
|
||||||
|
{ name: 'merchants', children: ['list', 'config', 'history'] },
|
||||||
|
{ name: 'operators', children: ['config', 'stats'] },
|
||||||
|
{ name: 'notifications', children: ['list', 'filters', 'actions'] },
|
||||||
|
{ name: 'webhooks', children: ['history', 'status', 'retry'] },
|
||||||
|
{ name: 'users', children: ['list', 'roles'] },
|
||||||
|
{ name: 'settings', children: [] },
|
||||||
|
{ name: 'integrations', children: [] },
|
||||||
|
{ name: 'support', children: [] },
|
||||||
|
{ name: 'profile', children: [] },
|
||||||
|
{ name: 'documentation', children: [] },
|
||||||
|
{ name: 'help', children: [] },
|
||||||
|
{ name: 'about', children: [] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const baseDir = path.join(__dirname, 'src/app/modules');
|
||||||
|
|
||||||
|
function createModule(module) {
|
||||||
|
const modulePath = path.join(baseDir, module.name);
|
||||||
|
fs.mkdirSync(modulePath, { recursive: true });
|
||||||
|
|
||||||
|
// components folder
|
||||||
|
fs.mkdirSync(path.join(modulePath, 'components'), { recursive: true });
|
||||||
|
|
||||||
|
// services folder inside module
|
||||||
|
const serviceFolder = path.join(modulePath, 'services');
|
||||||
|
fs.mkdirSync(serviceFolder, { recursive: true });
|
||||||
|
|
||||||
|
// main module files
|
||||||
|
fs.writeFileSync(path.join(modulePath, `${module.name}.ts`), `
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-${module.name}',
|
||||||
|
templateUrl: './${module.name}.html',
|
||||||
|
})
|
||||||
|
export class ${capitalize(module.name)} {}
|
||||||
|
`.trim());
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(modulePath, `${module.name}.html`), `<p>${capitalize(module.name)}</p>`);
|
||||||
|
fs.writeFileSync(path.join(modulePath, `${module.name}.spec.ts`), `
|
||||||
|
import { ${capitalize(module.name)} } from './${module.name}';
|
||||||
|
describe('${capitalize(module.name)}', () => {});
|
||||||
|
`.trim());
|
||||||
|
|
||||||
|
// service for module
|
||||||
|
fs.writeFileSync(path.join(serviceFolder, `${module.name}.service.ts`), `
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ${capitalize(module.name)}Service {
|
||||||
|
constructor() {}
|
||||||
|
}
|
||||||
|
`.trim());
|
||||||
|
|
||||||
|
// children
|
||||||
|
module.children.forEach(child => {
|
||||||
|
const childPath = path.join(modulePath, child);
|
||||||
|
fs.mkdirSync(childPath, { recursive: true });
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(childPath, `${child}.ts`), `
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-${module.name}-${child}',
|
||||||
|
templateUrl: './${child}.html',
|
||||||
|
})
|
||||||
|
export class ${capitalize(module.name)}${capitalize(child)} {}
|
||||||
|
`.trim());
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(childPath, `${child}.html`), `<p>${capitalize(module.name)} - ${capitalize(child)}</p>`);
|
||||||
|
fs.writeFileSync(path.join(childPath, `${child}.spec.ts`), `
|
||||||
|
import { ${capitalize(module.name)}${capitalize(child)} } from './${child}';
|
||||||
|
describe('${capitalize(module.name)}${capitalize(child)}', () => {});
|
||||||
|
`.trim());
|
||||||
|
|
||||||
|
// optional: service for child inside service folder
|
||||||
|
fs.writeFileSync(path.join(serviceFolder, `${child}.service.ts`), `
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ${capitalize(module.name)}${capitalize(child)}Service {
|
||||||
|
constructor() {}
|
||||||
|
}
|
||||||
|
`.trim());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function capitalize(str) {
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
modules.forEach(createModule);
|
||||||
|
console.log('Modules and services generated successfully with service folder in each module');
|
||||||
11023
package-lock.json
generated
Normal file
11023
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
87
package.json
Normal file
87
package.json
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"name": "dcb-bo-admin",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test",
|
||||||
|
"format": "prettier --write src/**/*.{ts,html}"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/common": "^20.3.6",
|
||||||
|
"@angular/compiler": "^20.3.6",
|
||||||
|
"@angular/core": "^20.3.6",
|
||||||
|
"@angular/forms": "^20.3.6",
|
||||||
|
"@angular/localize": "^20.3.6",
|
||||||
|
"@angular/platform-browser": "^20.3.6",
|
||||||
|
"@angular/router": "^20.3.6",
|
||||||
|
"@bluehalo/ngx-leaflet": "^20.0.0",
|
||||||
|
"@fullcalendar/angular": "^6.1.19",
|
||||||
|
"@fullcalendar/core": "^6.1.19",
|
||||||
|
"@fullcalendar/daygrid": "^6.1.19",
|
||||||
|
"@fullcalendar/interaction": "^6.1.19",
|
||||||
|
"@fullcalendar/list": "^6.1.19",
|
||||||
|
"@fullcalendar/timegrid": "^6.1.19",
|
||||||
|
"@ng-bootstrap/ng-bootstrap": "^19.0.1",
|
||||||
|
"@ng-icons/core": "^32.2.0",
|
||||||
|
"@ng-icons/lucide": "^32.2.0",
|
||||||
|
"@ng-icons/tabler-icons": "^32.2.0",
|
||||||
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"angular-datatables": "^19.0.0",
|
||||||
|
"angularx-flatpickr": "^8.1.0",
|
||||||
|
"bootstrap": "^5.3.8",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
|
"choices.js": "^11.1.0",
|
||||||
|
"datatables": "^1.10.18",
|
||||||
|
"datatables.net": "^2.3.4",
|
||||||
|
"datatables.net-bs5": "^2.3.4",
|
||||||
|
"datatables.net-buttons": "^3.2.5",
|
||||||
|
"datatables.net-buttons-bs5": "^3.2.5",
|
||||||
|
"datatables.net-buttons-dt": "^3.2.5",
|
||||||
|
"datatables.net-fixedheader-bs5": "^4.0.4",
|
||||||
|
"datatables.net-responsive-bs5": "^3.0.7",
|
||||||
|
"datatables.net-select": "^3.1.3",
|
||||||
|
"datatables.net-select-bs5": "^3.1.3",
|
||||||
|
"esbuild": "^0.25.11",
|
||||||
|
"flatpickr": "^4.6.13",
|
||||||
|
"jquery": "^3.7.1",
|
||||||
|
"jsvectormap": "^1.7.0",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
|
"keycloak-angular": "^20.0.0",
|
||||||
|
"keycloak-js": "^26.2.1",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"ng-otp-input": "^2.0.9",
|
||||||
|
"ng2-charts": "^8.0.0",
|
||||||
|
"ngx-countup": "^13.2.0",
|
||||||
|
"ngx-dropzone-wrapper": "^17.0.0",
|
||||||
|
"ngx-quill": "^28.0.1",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"quill": "^2.0.3",
|
||||||
|
"rxjs": "~7.8.2",
|
||||||
|
"simplebar-angular": "^3.3.2",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"zone.js": "^0.15.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular/build": "^20.3.6",
|
||||||
|
"@angular/cli": "^20.3.6",
|
||||||
|
"@angular/compiler-cli": "^20.3.6",
|
||||||
|
"@types/jasmine": "~5.1.12",
|
||||||
|
"@types/jquery": "^3.5.33",
|
||||||
|
"@types/leaflet": "^1.9.21",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"jasmine-core": "~5.12.0",
|
||||||
|
"karma": "~6.4.4",
|
||||||
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
"karma-coverage": "~2.2.1",
|
||||||
|
"karma-jasmine": "~5.1.0",
|
||||||
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"vite": "^7.1.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
20
src/app/app.config.ts
Normal file
20
src/app/app.config.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {
|
||||||
|
ApplicationConfig,
|
||||||
|
provideBrowserGlobalErrorListeners,
|
||||||
|
provideZonelessChangeDetection
|
||||||
|
} from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
|
import { routes } from './app.routes';
|
||||||
|
import { authInterceptor } from './core/interceptors/auth.interceptor';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideBrowserGlobalErrorListeners(),
|
||||||
|
provideZonelessChangeDetection(),
|
||||||
|
provideRouter(routes),
|
||||||
|
provideHttpClient(withInterceptors([authInterceptor])),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
1
src/app/app.html
Normal file
1
src/app/app.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<router-outlet />
|
||||||
28
src/app/app.routes.ts
Normal file
28
src/app/app.routes.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
import { VerticalLayout } from '@layouts/vertical-layout/vertical-layout';
|
||||||
|
import { authGuard } from './core/guards/auth.guard';
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
|
||||||
|
|
||||||
|
// Routes publiques (auth)
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./modules/auth/auth.route').then(mod => mod.Auth_ROUTES),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Routes protégées
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: VerticalLayout,
|
||||||
|
canActivate: [authGuard],
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./modules/modules-routing.module').then(
|
||||||
|
m => m.ModulesRoutingModule
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Catch-all
|
||||||
|
{ path: '**', redirectTo: '/auth/sign-in' },
|
||||||
|
];
|
||||||
0
src/app/app.scss
Normal file
0
src/app/app.scss
Normal file
25
src/app/app.spec.ts
Normal file
25
src/app/app.spec.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { provideZonelessChangeDetection } from '@angular/core'
|
||||||
|
import { TestBed } from '@angular/core/testing'
|
||||||
|
import { App } from './app'
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [App],
|
||||||
|
providers: [provideZonelessChangeDetection()],
|
||||||
|
}).compileComponents()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(App)
|
||||||
|
const app = fixture.componentInstance
|
||||||
|
expect(app).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render title', () => {
|
||||||
|
const fixture = TestBed.createComponent(App)
|
||||||
|
fixture.detectChanges()
|
||||||
|
const compiled = fixture.nativeElement as HTMLElement
|
||||||
|
expect(compiled.querySelector('h1')?.textContent).toContain('DCB BO Admin')
|
||||||
|
})
|
||||||
|
})
|
||||||
53
src/app/app.ts
Normal file
53
src/app/app.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
|
||||||
|
import * as tablerIcons from '@ng-icons/tabler-icons';
|
||||||
|
import * as tablerIconsFill from '@ng-icons/tabler-icons/fill';
|
||||||
|
import * as lucideIcons from '@ng-icons/lucide';
|
||||||
|
import { provideIcons } from '@ng-icons/core';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
import { filter, map, mergeMap } from 'rxjs';
|
||||||
|
import { AuthService } from './core/services/auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
imports: [RouterOutlet],
|
||||||
|
templateUrl: './app.html',
|
||||||
|
styleUrls: ['./app.scss'],
|
||||||
|
viewProviders: [
|
||||||
|
provideIcons({ ...tablerIcons, ...tablerIconsFill, ...lucideIcons }),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class App implements OnInit {
|
||||||
|
private titleService = inject(Title);
|
||||||
|
private router = inject(Router);
|
||||||
|
private activatedRoute = inject(ActivatedRoute);
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
// Initialiser l'authentification
|
||||||
|
await this.authService.initialize();
|
||||||
|
|
||||||
|
// Configurer le titre de la page
|
||||||
|
this.setupTitleListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupTitleListener(): void {
|
||||||
|
this.router.events
|
||||||
|
.pipe(
|
||||||
|
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
|
||||||
|
map(() => {
|
||||||
|
let route = this.activatedRoute;
|
||||||
|
while (route.firstChild) {
|
||||||
|
route = route.firstChild;
|
||||||
|
}
|
||||||
|
return route;
|
||||||
|
}),
|
||||||
|
mergeMap((route) => route.data)
|
||||||
|
)
|
||||||
|
.subscribe((data) => {
|
||||||
|
if (data['title']) {
|
||||||
|
this.titleService.setTitle(`${data['title']} | DCB BO Admin`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/app/components/app-logo.ts
Normal file
32
src/app/components/app-logo.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-app-logo',
|
||||||
|
imports: [RouterLink, NgIcon],
|
||||||
|
template: `
|
||||||
|
<a routerLink="/" class="logo-dark">
|
||||||
|
<span class="d-flex align-items-center gap-1">
|
||||||
|
<span class="avatar avatar-xs rounded-circle text-bg-dark">
|
||||||
|
<span class="avatar-title">
|
||||||
|
<ng-icon name="lucideSparkles" class="fs-md"></ng-icon>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="logo-text text-body fw-bold fs-xl">Simple</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a routerLink="/" class="logo-light">
|
||||||
|
<span class="d-flex align-items-center gap-1">
|
||||||
|
<span class="avatar avatar-xs rounded-circle text-bg-dark">
|
||||||
|
<span class="avatar-title">
|
||||||
|
<ng-icon name="lucideSparkles" class="fs-md"></ng-icon>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="logo-text text-white fw-bold fs-xl">Simple</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class AppLogo {}
|
||||||
151
src/app/components/chartjs.ts
Normal file
151
src/app/components/chartjs.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
OnInit,
|
||||||
|
OnDestroy,
|
||||||
|
inject,
|
||||||
|
ViewChild,
|
||||||
|
AfterViewInit,
|
||||||
|
HostListener,
|
||||||
|
} from '@angular/core'
|
||||||
|
import {
|
||||||
|
BaseChartDirective,
|
||||||
|
provideCharts,
|
||||||
|
withDefaultRegisterables,
|
||||||
|
} from 'ng2-charts'
|
||||||
|
import { ChartConfiguration } from 'chart.js'
|
||||||
|
import { LayoutStoreService } from '@core/services/layout-store.service'
|
||||||
|
import { Subscription } from 'rxjs'
|
||||||
|
import { merge } from 'lodash-es'
|
||||||
|
import { getColor } from '@/app/utils/color-utils'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chartjs',
|
||||||
|
standalone: true,
|
||||||
|
imports: [BaseChartDirective],
|
||||||
|
providers: [provideCharts(withDefaultRegisterables())],
|
||||||
|
template: `
|
||||||
|
<canvas
|
||||||
|
[height]="height"
|
||||||
|
baseChart
|
||||||
|
[data]="options.data"
|
||||||
|
[options]="options.options"
|
||||||
|
[type]="options.type"
|
||||||
|
style="width:100%;display:block"
|
||||||
|
></canvas>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class Chartjs implements OnInit, OnDestroy, AfterViewInit {
|
||||||
|
@Input() getOptions!: () => ChartConfiguration
|
||||||
|
@Input() height: number = 300
|
||||||
|
|
||||||
|
@ViewChild(BaseChartDirective) chart?: BaseChartDirective
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
setTimeout(() => this.chart?.chart?.resize(), 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:resize')
|
||||||
|
onWindowResize() {
|
||||||
|
setTimeout(() => this.chart?.chart?.resize(), 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
options!: ChartConfiguration
|
||||||
|
private layoutSub!: Subscription
|
||||||
|
|
||||||
|
get bodyFont() {
|
||||||
|
return getComputedStyle(document.body).fontFamily.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultOptions = (): ChartConfiguration['options'] => ({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
top: -10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
font: { family: this.bodyFont },
|
||||||
|
color: getColor('secondary-color'),
|
||||||
|
display: true,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
font: { family: this.bodyFont },
|
||||||
|
color: getColor('secondary-color'),
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
color: getColor('chart-border-color'),
|
||||||
|
lineWidth: 1,
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
display: false,
|
||||||
|
dash: [5, 5],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'top',
|
||||||
|
labels: {
|
||||||
|
font: { family: this.bodyFont },
|
||||||
|
color: getColor('secondary-color'),
|
||||||
|
usePointStyle: true,
|
||||||
|
pointStyle: 'circle',
|
||||||
|
boxWidth: 8,
|
||||||
|
boxHeight: 8,
|
||||||
|
padding: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
titleFont: { family: this.bodyFont },
|
||||||
|
bodyFont: { family: this.bodyFont },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
layout = inject(LayoutStoreService)
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.setChartOptions()
|
||||||
|
|
||||||
|
// Refresh chart on theme/skin change
|
||||||
|
this.layoutSub = this.layout.layoutState$.subscribe(() => {
|
||||||
|
this.setChartOptions()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.layoutSub?.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private setChartOptions() {
|
||||||
|
const userConfig = this.getOptions()
|
||||||
|
userConfig.options = merge({}, this.defaultOptions(), userConfig.options)
|
||||||
|
|
||||||
|
if (userConfig.options?.scales) {
|
||||||
|
if (userConfig.options.scales['x']) {
|
||||||
|
userConfig.options.scales['x'].type = 'category'
|
||||||
|
}
|
||||||
|
if (userConfig.options.scales['y']) {
|
||||||
|
userConfig.options.scales['y'].type = 'linear'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options = userConfig
|
||||||
|
setTimeout(() => this.chart?.chart?.resize(), 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
126
src/app/components/file-uploader.ts
Normal file
126
src/app/components/file-uploader.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
import { formatFileSize } from '../utils/file-utils'
|
||||||
|
import {
|
||||||
|
DROPZONE_CONFIG,
|
||||||
|
DropzoneConfigInterface,
|
||||||
|
DropzoneModule,
|
||||||
|
} from 'ngx-dropzone-wrapper'
|
||||||
|
|
||||||
|
type UploadedFile = {
|
||||||
|
name: string
|
||||||
|
size: number
|
||||||
|
type: string
|
||||||
|
dataURL?: string
|
||||||
|
loading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_DROPZONE_CONFIG: DropzoneConfigInterface = {
|
||||||
|
// Change this to your upload POST address:
|
||||||
|
url: 'https://httpbin.org/post',
|
||||||
|
maxFilesize: 50,
|
||||||
|
acceptedFiles: 'image/*',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'FileUploader',
|
||||||
|
standalone: true,
|
||||||
|
imports: [DropzoneModule, NgIcon],
|
||||||
|
template: `
|
||||||
|
<dropzone
|
||||||
|
class="dropzone"
|
||||||
|
[config]="dropzoneConfig"
|
||||||
|
[message]="dropzone"
|
||||||
|
(addedFile)="onFileAdded($event)"
|
||||||
|
></dropzone>
|
||||||
|
@if (uploadedFiles) {
|
||||||
|
<div class="dropzone-previews mt-3" id="file-previews">
|
||||||
|
@for (file of uploadedFiles; track file.name; let index = $index) {
|
||||||
|
<div class="card mt-1 mb-0 border-dashed border">
|
||||||
|
<div class="p-2">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<img
|
||||||
|
data-dz-thumbnail=""
|
||||||
|
[src]="file.dataURL"
|
||||||
|
class="avatar-sm rounded bg-light"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col ps-0">
|
||||||
|
<a href="javascript:void(0);" class="fw-semibold">{{
|
||||||
|
file.name
|
||||||
|
}}</a>
|
||||||
|
<p class="mb-0 text-muted" data-dz-size="">
|
||||||
|
<strong>{{ formatFileSize(file.size) }}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<a
|
||||||
|
(click)="removeFile(index)"
|
||||||
|
class="btn btn-link shadow-none btn-lg text-danger"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerX" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: DROPZONE_CONFIG,
|
||||||
|
useValue: DEFAULT_DROPZONE_CONFIG,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class FileUploader {
|
||||||
|
formatFileSize = formatFileSize
|
||||||
|
uploadedFiles: UploadedFile[] = []
|
||||||
|
|
||||||
|
dropzoneConfig: DropzoneConfigInterface = {
|
||||||
|
url: 'https://httpbin.org/post',
|
||||||
|
maxFilesize: 50,
|
||||||
|
clickable: true,
|
||||||
|
addRemoveLinks: true,
|
||||||
|
previewsContainer: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
dropzone = `
|
||||||
|
<div class="dz-message needsclick">
|
||||||
|
<div class="avatar-lg mx-auto my-3">
|
||||||
|
<span class="avatar-title bg-info-subtle text-info rounded-circle">
|
||||||
|
<span class="fs-24 text-info">
|
||||||
|
<span class="fs-24 upload-icon"></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="mb-2">Drop files here or click to upload.</h4>
|
||||||
|
<p class="text-muted fst-italic mb-3">You can drag images here, or browse files via the button below.</p>
|
||||||
|
<span class="d-block pb-3">
|
||||||
|
<span type="button" class="btn btn-sm shadow btn-default">Browse Images</span>
|
||||||
|
</span>
|
||||||
|
</div>`
|
||||||
|
|
||||||
|
imageURL: string = ''
|
||||||
|
|
||||||
|
onFileAdded(file: any) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||||
|
const dataUrl = e.target?.result as string
|
||||||
|
this.uploadedFiles.push({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
dataURL: dataUrl,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFile(index: number) {
|
||||||
|
this.uploadedFiles.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/app/components/page-title/page-title.html
Normal file
13
src/app/components/page-title/page-title.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<div class="row justify-content-center py-5">
|
||||||
|
<div class="col-xxl-5 col-xl-7 text-center">
|
||||||
|
<span
|
||||||
|
class="badge badge-default fw-normal shadow px-2 py-1 mb-2 fst-italic fs-xxs"
|
||||||
|
>
|
||||||
|
<ng-icon [name]="badge.icon" class="fs-sm me-1" />
|
||||||
|
{{ badge.text }}
|
||||||
|
</span>
|
||||||
|
<h3 class="fw-bold">{{ title }}</h3>
|
||||||
|
|
||||||
|
<p class="fs-md text-muted mb-0">{{ subTitle }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
22
src/app/components/page-title/page-title.spec.ts
Normal file
22
src/app/components/page-title/page-title.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { PageTitle } from './page-title'
|
||||||
|
|
||||||
|
describe('PageTitle', () => {
|
||||||
|
let component: PageTitle
|
||||||
|
let fixture: ComponentFixture<PageTitle>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PageTitle],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PageTitle)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
20
src/app/components/page-title/page-title.ts
Normal file
20
src/app/components/page-title/page-title.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Component, Input } from '@angular/core'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-page-title',
|
||||||
|
imports: [NgIcon],
|
||||||
|
templateUrl: './page-title.html',
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class PageTitle {
|
||||||
|
@Input() title: string = ''
|
||||||
|
@Input() subTitle: string = ''
|
||||||
|
@Input() badge: {
|
||||||
|
text: string
|
||||||
|
icon: string
|
||||||
|
} = {
|
||||||
|
text: '',
|
||||||
|
icon: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/app/components/password-strength-bar.ts
Normal file
36
src/app/components/password-strength-bar.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { calculatePasswordStrength } from '@/app/utils/password-utils'
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
type OnChanges,
|
||||||
|
type SimpleChanges,
|
||||||
|
} from '@angular/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-password-strength-bar',
|
||||||
|
imports: [],
|
||||||
|
template: ` <div class="password-bar my-2">
|
||||||
|
@for (bar of strengthBars; track i; let i = $index) {
|
||||||
|
<div
|
||||||
|
[class]="
|
||||||
|
'strong-bar ' +
|
||||||
|
(i < passwordStrength ? 'bar-active-' + passwordStrength : '')
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<p class="text-muted fs-xs mb-0">
|
||||||
|
Use 8+ characters with letters, numbers & symbols.
|
||||||
|
</p>`,
|
||||||
|
})
|
||||||
|
export class PasswordStrengthBar implements OnChanges {
|
||||||
|
@Input() password: string = ''
|
||||||
|
passwordStrength: number = 0
|
||||||
|
strengthBars = new Array(4)
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes['password']) {
|
||||||
|
this.passwordStrength = calculatePasswordStrength(this.password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/app/components/ui-card.ts
Normal file
90
src/app/components/ui-card.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { Component, Input } from '@angular/core'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-ui-card',
|
||||||
|
imports: [NgIcon, NgbCollapse],
|
||||||
|
template: `
|
||||||
|
@if (isVisible) {
|
||||||
|
<div
|
||||||
|
class="card {{ isCollapsed ? 'card-collapse' : '' }} {{ className }}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="card-header justify-content-between align-items-center"
|
||||||
|
[class]="isCollapsed ? 'border-0' : ''"
|
||||||
|
>
|
||||||
|
<h5 class="card-title">
|
||||||
|
{{ title }}
|
||||||
|
<ng-content select="[badge-text]"></ng-content>
|
||||||
|
</h5>
|
||||||
|
<div>
|
||||||
|
@if (isTogglable || isReloadable || isCloseable) {
|
||||||
|
<div class="card-action">
|
||||||
|
@if (isTogglable) {
|
||||||
|
<button
|
||||||
|
(click)="isCollapsed = !isCollapsed"
|
||||||
|
class="card-action-item border-0"
|
||||||
|
>
|
||||||
|
@if (!isCollapsed) {
|
||||||
|
<ng-icon name="tablerChevronUp" />
|
||||||
|
}
|
||||||
|
@if (isCollapsed) {
|
||||||
|
<ng-icon name="tablerChevronDown" />
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (isReloadable) {
|
||||||
|
<button (click)="reload()" class="card-action-item border-0">
|
||||||
|
<ng-icon name="tablerRefresh" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (isCloseable) {
|
||||||
|
<button (click)="close()" class="card-action-item border-0">
|
||||||
|
<ng-icon name="tablerX" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<ng-content select="[helper-text]"></ng-content>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="card-body {{ bodyClass }}"
|
||||||
|
#collapse="ngbCollapse"
|
||||||
|
[(ngbCollapse)]="isCollapsed"
|
||||||
|
>
|
||||||
|
<ng-content select="[card-body]"></ng-content>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (isReloading) {
|
||||||
|
<div class="card-overlay d-flex">
|
||||||
|
<div class="spinner-border text-primary"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class UiCard {
|
||||||
|
@Input() title!: string
|
||||||
|
@Input() isTogglable?: boolean
|
||||||
|
@Input() isReloadable?: boolean
|
||||||
|
@Input() isCloseable?: boolean
|
||||||
|
@Input() bodyClass?: string
|
||||||
|
@Input() className?: string
|
||||||
|
|
||||||
|
isCollapsed = false
|
||||||
|
isReloading = false
|
||||||
|
isVisible = true
|
||||||
|
|
||||||
|
reload() {
|
||||||
|
this.isReloading = true
|
||||||
|
setTimeout(() => (this.isReloading = false), 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/app/components/vector-map.ts
Normal file
56
src/app/components/vector-map.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
Component,
|
||||||
|
type ElementRef,
|
||||||
|
HostListener,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core'
|
||||||
|
import JsVectorMap from 'jsvectormap'
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
jsVectorMap?: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-vector-map',
|
||||||
|
standalone: true,
|
||||||
|
template:
|
||||||
|
'<div #mapContainer [style.width]="width" [style.height]="height"></div>',
|
||||||
|
})
|
||||||
|
export class VectorMap implements AfterViewInit, OnDestroy {
|
||||||
|
@Input() width: string = ''
|
||||||
|
@Input() height: string = ''
|
||||||
|
@Input() options: Record<string, unknown> = {}
|
||||||
|
@ViewChild('mapContainer', { static: true }) mapContainerRef!: ElementRef
|
||||||
|
|
||||||
|
mapInstance!: any
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
const container = this.mapContainerRef.nativeElement
|
||||||
|
const width = container.offsetWidth
|
||||||
|
const height = container.offsetHeight
|
||||||
|
|
||||||
|
if (width && height) {
|
||||||
|
this.mapInstance = new JsVectorMap({
|
||||||
|
selector: container,
|
||||||
|
...this.options,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.warn('JsVectorMap: container has invalid dimensions.')
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:resize')
|
||||||
|
onWindowResize(): void {
|
||||||
|
this.mapInstance?.updateSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.mapInstance?.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/app/constants/index.ts
Normal file
29
src/app/constants/index.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
type CurrencyType = 'XOF' | '$' | '€'
|
||||||
|
|
||||||
|
export const colorVariants = [
|
||||||
|
'primary',
|
||||||
|
'secondary',
|
||||||
|
'success',
|
||||||
|
'danger',
|
||||||
|
'warning',
|
||||||
|
'info',
|
||||||
|
'light',
|
||||||
|
'dark',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const currency: CurrencyType = 'XOF'
|
||||||
|
|
||||||
|
export const currentYear = new Date().getFullYear()
|
||||||
|
|
||||||
|
export const credits = {
|
||||||
|
website: '',
|
||||||
|
name: 'DCB Team',
|
||||||
|
buyLink: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const appName = 'DCB BO Admin'
|
||||||
|
export const appTitle = 'DCB BO Admin'
|
||||||
|
export const appDescription: string =
|
||||||
|
'Direct Carrier Billing (DCB) permet aux utilisateurs d’effectuer des achats directement via le crédit de leur téléphone portable. Simple, rapide et sécurisé, le DCB fonctionne sur tous les appareils mobiles pour les abonnés et les utilisateurs prépayés.'
|
||||||
|
|
||||||
|
export const basePath: string = ''
|
||||||
32
src/app/core/directive/choices-select.directive.ts
Normal file
32
src/app/core/directive/choices-select.directive.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Directive, ElementRef, Input, type OnInit } from '@angular/core'
|
||||||
|
import Choices, { Options as ChoiceOption } from 'choices.js'
|
||||||
|
|
||||||
|
export type SelectOptions = Partial<ChoiceOption>
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[choicesSelect]',
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class ChoiceSelectInputDirective implements OnInit {
|
||||||
|
@Input() className?: string
|
||||||
|
@Input() onChange?: (text: string) => void
|
||||||
|
@Input() options?: SelectOptions
|
||||||
|
|
||||||
|
constructor(private eleRef: ElementRef) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const choices = new Choices(this.eleRef.nativeElement, {
|
||||||
|
...this.options,
|
||||||
|
placeholder: true,
|
||||||
|
allowHTML: true,
|
||||||
|
shouldSort: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
choices.passedElement.element.addEventListener('change', (e: Event) => {
|
||||||
|
if (!(e.target instanceof HTMLSelectElement)) return
|
||||||
|
if (this.onChange) {
|
||||||
|
this.onChange(e.target.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/app/core/directive/counter.directive.ts
Normal file
34
src/app/core/directive/counter.directive.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Directive, Input, Output, EventEmitter } from '@angular/core'
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[appCounter]',
|
||||||
|
exportAs: 'appCounter',
|
||||||
|
})
|
||||||
|
export class CounterDirective {
|
||||||
|
@Input() count: number = 0
|
||||||
|
@Input() min: number = 0
|
||||||
|
@Input() max: number = 999999
|
||||||
|
|
||||||
|
@Output() countChange = new EventEmitter<number>()
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
if (this.count < this.max) {
|
||||||
|
this.count++
|
||||||
|
this.countChange.emit(this.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decrement() {
|
||||||
|
if (this.count > this.min) {
|
||||||
|
this.count--
|
||||||
|
this.countChange.emit(this.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(val: number) {
|
||||||
|
if (!isNaN(val)) {
|
||||||
|
this.count = Math.min(this.max, Math.max(this.min, val))
|
||||||
|
this.countChange.emit(this.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/app/core/guards/auth.guard.ts
Normal file
20
src/app/core/guards/auth.guard.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { CanActivateFn, Router } from '@angular/router';
|
||||||
|
import { AuthService } from '../services/auth.service';
|
||||||
|
|
||||||
|
export const authGuard: CanActivateFn = (route, state) => {
|
||||||
|
const authService = inject(AuthService);
|
||||||
|
const router = inject(Router);
|
||||||
|
|
||||||
|
console.log('Guard check for:', state.url, 'isAuthenticated:', authService.isAuthenticated());
|
||||||
|
|
||||||
|
if (authService.isAuthenticated()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rediriger vers login avec l'URL de retour
|
||||||
|
router.navigate(['/auth/sign-in'], {
|
||||||
|
queryParams: { returnUrl: state.url }
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
};
|
||||||
43
src/app/core/interceptors/auth.interceptor.ts
Normal file
43
src/app/core/interceptors/auth.interceptor.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { HttpInterceptorFn, HttpRequest } from '@angular/common/http';
|
||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { AuthService } from '../services/auth.service';
|
||||||
|
|
||||||
|
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
|
const authService = inject(AuthService);
|
||||||
|
|
||||||
|
// On ignore les requêtes de login, refresh, logout
|
||||||
|
if (isAuthRequest(req)) {
|
||||||
|
return next(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authService.getToken();
|
||||||
|
|
||||||
|
// On ajoute le token uniquement si c’est une requête API
|
||||||
|
if (token && isApiRequest(req)) {
|
||||||
|
const cloned = req.clone({
|
||||||
|
setHeaders: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return next(cloned);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(req);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Détermine si c’est une requête vers ton backend API
|
||||||
|
function isApiRequest(req: HttpRequest<any>): boolean {
|
||||||
|
return req.url.includes('/api/') || (
|
||||||
|
req.url.includes('/auth/') && !isAuthRequest(req)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Liste des endpoints où le token ne doit pas être ajouté
|
||||||
|
function isAuthRequest(req: HttpRequest<any>): boolean {
|
||||||
|
const url = req.url;
|
||||||
|
return (
|
||||||
|
url.includes('/auth/login') ||
|
||||||
|
url.includes('/auth/refresh') ||
|
||||||
|
url.includes('/auth/logout')
|
||||||
|
);
|
||||||
|
}
|
||||||
192
src/app/core/services/auth.service.ts
Normal file
192
src/app/core/services/auth.service.ts
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { environment } from '@environments/environment';
|
||||||
|
import { BehaviorSubject, tap, catchError, Observable, throwError } from 'rxjs';
|
||||||
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
|
||||||
|
interface DecodedToken {
|
||||||
|
exp: number;
|
||||||
|
iat?: number;
|
||||||
|
sub?: string;
|
||||||
|
preferred_username?: string;
|
||||||
|
email?: string;
|
||||||
|
given_name?: string;
|
||||||
|
family_name?: string;
|
||||||
|
realm_access?: {
|
||||||
|
roles: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResponse {
|
||||||
|
access_token: string;
|
||||||
|
expires_in?: number;
|
||||||
|
refresh_token?: string;
|
||||||
|
token_type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AuthService {
|
||||||
|
private readonly tokenKey = 'access_token';
|
||||||
|
private readonly refreshTokenKey = 'refresh_token';
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
private authState$ = new BehaviorSubject<boolean>(this.isAuthenticated());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisation simple - à appeler dans app.component.ts
|
||||||
|
*/
|
||||||
|
initialize(): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const token = this.getToken();
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isTokenExpired(token)) {
|
||||||
|
this.refreshToken().subscribe({
|
||||||
|
next: () => resolve(true),
|
||||||
|
error: () => {
|
||||||
|
this.clearSession(false);
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentifie l'utilisateur via le backend NestJS
|
||||||
|
*/
|
||||||
|
login(username: string, password: string): Observable<AuthResponse> {
|
||||||
|
return this.http.post<AuthResponse>(
|
||||||
|
`${environment.apiUrl}/auth/login`,
|
||||||
|
{ username, password }
|
||||||
|
).pipe(
|
||||||
|
tap(response => {
|
||||||
|
this.handleLoginResponse(response);
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Login failed', error);
|
||||||
|
return throwError(() => this.getErrorMessage(error));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rafraîchit le token d'accès (retourne un Observable maintenant)
|
||||||
|
*/
|
||||||
|
refreshToken(): Observable<AuthResponse> {
|
||||||
|
const refreshToken = localStorage.getItem(this.refreshTokenKey);
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
return throwError(() => 'No refresh token available');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.post<AuthResponse>(
|
||||||
|
`${environment.apiUrl}/auth/refresh`,
|
||||||
|
{ refresh_token: refreshToken }
|
||||||
|
).pipe(
|
||||||
|
tap(response => {
|
||||||
|
this.handleLoginResponse(response);
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Token refresh failed', error);
|
||||||
|
this.clearSession();
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déconnecte l'utilisateur
|
||||||
|
*/
|
||||||
|
logout(redirect = true): void {
|
||||||
|
this.clearSession(redirect);
|
||||||
|
|
||||||
|
// Appel API optionnel (ne pas bloquer dessus)
|
||||||
|
this.http.post(`${environment.apiUrl}/auth/logout`, {}).subscribe({
|
||||||
|
error: (err) => console.warn('Logout API call failed', err)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearSession(redirect: boolean = true): void {
|
||||||
|
localStorage.removeItem(this.tokenKey);
|
||||||
|
localStorage.removeItem(this.refreshTokenKey);
|
||||||
|
this.authState$.next(false);
|
||||||
|
|
||||||
|
if (redirect) {
|
||||||
|
this.router.navigate(['/auth/sign-in']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleLoginResponse(response: AuthResponse): void {
|
||||||
|
if (response?.access_token) {
|
||||||
|
localStorage.setItem(this.tokenKey, response.access_token);
|
||||||
|
|
||||||
|
if (response.refresh_token) {
|
||||||
|
localStorage.setItem(this.refreshTokenKey, response.refresh_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.authState$.next(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getErrorMessage(error: any): string {
|
||||||
|
if (error?.error?.message) {
|
||||||
|
return error.error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.status === 401) {
|
||||||
|
return 'Invalid username or password';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Login failed. Please try again.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le token JWT stocké
|
||||||
|
*/
|
||||||
|
getToken(): string | null {
|
||||||
|
return localStorage.getItem(this.tokenKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le token est expiré
|
||||||
|
*/
|
||||||
|
isTokenExpired(token: string): boolean {
|
||||||
|
try {
|
||||||
|
const decoded: DecodedToken = jwtDecode(token);
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
// Marge de sécurité de 60 secondes
|
||||||
|
return decoded.exp < (now + 60);
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur est authentifié
|
||||||
|
*/
|
||||||
|
isAuthenticated(): boolean {
|
||||||
|
const token = this.getToken();
|
||||||
|
if (!token) return false;
|
||||||
|
|
||||||
|
return !this.isTokenExpired(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
onAuthState() {
|
||||||
|
return this.authState$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les infos utilisateur depuis le backend
|
||||||
|
*/
|
||||||
|
getProfile(): Observable<any> {
|
||||||
|
return this.http.get(`${environment.apiUrl}/auth/me`);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/app/core/services/language.service.ts
Normal file
45
src/app/core/services/language.service.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { BehaviorSubject } from 'rxjs'
|
||||||
|
import { LanguageOptionType } from '@/app/types/layout'
|
||||||
|
|
||||||
|
const STORAGE_KEY = '__DCB_BO_ADMIN_LANG__'
|
||||||
|
|
||||||
|
const availableLanguages: LanguageOptionType[] = [
|
||||||
|
{
|
||||||
|
code: 'fr',
|
||||||
|
name: 'Français',
|
||||||
|
nativeName: 'Français',
|
||||||
|
flag: 'assets/images/flags/fr.svg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'en',
|
||||||
|
name: 'English',
|
||||||
|
nativeName: 'English',
|
||||||
|
flag: 'assets/images/flags/us.svg',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class LanguageService {
|
||||||
|
private currentLangSubject = new BehaviorSubject<LanguageOptionType>(
|
||||||
|
availableLanguages[0]
|
||||||
|
)
|
||||||
|
currentLang$ = this.currentLangSubject.asObservable()
|
||||||
|
|
||||||
|
getLanguages(): LanguageOptionType[] {
|
||||||
|
return availableLanguages
|
||||||
|
}
|
||||||
|
|
||||||
|
setLanguage(code: string) {
|
||||||
|
const lang = availableLanguages.find((l) => l.code === code)
|
||||||
|
if (lang) {
|
||||||
|
this.currentLangSubject.next(lang)
|
||||||
|
localStorage.setItem(STORAGE_KEY, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initLanguage() {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (saved) this.setLanguage(saved)
|
||||||
|
}
|
||||||
|
}
|
||||||
274
src/app/core/services/layout-store.service.ts
Normal file
274
src/app/core/services/layout-store.service.ts
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
import { effect, Injectable, signal } from '@angular/core'
|
||||||
|
import { LayoutState } from '@/app/types/layout'
|
||||||
|
|
||||||
|
import { NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { BehaviorSubject } from 'rxjs'
|
||||||
|
import { Customizer } from '@layouts/components/customizer/customizer'
|
||||||
|
|
||||||
|
const STORAGE_KEY = '__DCB_BO_ADMIN_CONFIG__'
|
||||||
|
|
||||||
|
const defaultState: LayoutState = {
|
||||||
|
skin: 'shadcn',
|
||||||
|
theme: 'light',
|
||||||
|
position: 'fixed',
|
||||||
|
topbar: { color: 'light' },
|
||||||
|
sidenav: {
|
||||||
|
color: 'light',
|
||||||
|
size: 'default',
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
monochrome: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class LayoutStoreService {
|
||||||
|
constructor(private offcanvasService: NgbOffcanvas) {
|
||||||
|
this.applyAllAttributes()
|
||||||
|
effect(() => {
|
||||||
|
this.applyAllAttributes()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
state = signal<LayoutState>(this.loadInitialState())
|
||||||
|
|
||||||
|
private html = document.documentElement
|
||||||
|
|
||||||
|
private layoutStateSubject = new BehaviorSubject<LayoutState>(this.state())
|
||||||
|
readonly layoutState$ = this.layoutStateSubject.asObservable()
|
||||||
|
|
||||||
|
private loadInitialState(): LayoutState {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY)
|
||||||
|
return stored ? JSON.parse(stored) : defaultState
|
||||||
|
} catch {
|
||||||
|
return defaultState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private persistToStorage() {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.state()))
|
||||||
|
}
|
||||||
|
|
||||||
|
get skin() {
|
||||||
|
return this.state().skin
|
||||||
|
}
|
||||||
|
|
||||||
|
get theme() {
|
||||||
|
return this.state().theme
|
||||||
|
}
|
||||||
|
|
||||||
|
get position() {
|
||||||
|
return this.state().position
|
||||||
|
}
|
||||||
|
|
||||||
|
get topbarColor() {
|
||||||
|
return this.state().topbar.color
|
||||||
|
}
|
||||||
|
|
||||||
|
get sidenavColor() {
|
||||||
|
return this.state().sidenav.color
|
||||||
|
}
|
||||||
|
|
||||||
|
get sidenavSize() {
|
||||||
|
return this.state().sidenav.size
|
||||||
|
}
|
||||||
|
|
||||||
|
get sidenavUser() {
|
||||||
|
return this.state().sidenav.user
|
||||||
|
}
|
||||||
|
|
||||||
|
get isLoading() {
|
||||||
|
return this.state().isLoading
|
||||||
|
}
|
||||||
|
|
||||||
|
setSkin(skin: LayoutState['skin'], persist = true): void {
|
||||||
|
this.setHtmlAttribute('data-skin', skin)
|
||||||
|
if (persist) {
|
||||||
|
this.state.update((s) => ({ ...s, skin }))
|
||||||
|
this.persistToStorage()
|
||||||
|
}
|
||||||
|
this.layoutStateSubject.next({ ...this.state(), skin })
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(theme: LayoutState['theme'], persist = true): void {
|
||||||
|
this.setHtmlAttribute(
|
||||||
|
'data-bs-theme',
|
||||||
|
theme === 'system' ? this.getSystemTheme() : theme
|
||||||
|
)
|
||||||
|
if (persist) {
|
||||||
|
this.state.update((s) => ({ ...s, theme }))
|
||||||
|
this.persistToStorage()
|
||||||
|
}
|
||||||
|
this.layoutStateSubject.next({ ...this.state(), theme })
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayoutPosition(position: LayoutState['position'], persist = true): void {
|
||||||
|
this.setHtmlAttribute('data-layout-position', position)
|
||||||
|
if (persist) {
|
||||||
|
this.state.update((s) => ({ ...s, position }))
|
||||||
|
this.persistToStorage()
|
||||||
|
}
|
||||||
|
this.layoutStateSubject.next({ ...this.state(), position })
|
||||||
|
}
|
||||||
|
|
||||||
|
setTopbarColor(color: LayoutState['topbar']['color'], persist = true): void {
|
||||||
|
this.setHtmlAttribute('data-topbar-color', color)
|
||||||
|
if (persist) {
|
||||||
|
this.state.update((s) => ({
|
||||||
|
...s,
|
||||||
|
topbar: { ...s.topbar, color },
|
||||||
|
}))
|
||||||
|
this.persistToStorage()
|
||||||
|
}
|
||||||
|
this.layoutStateSubject.next({
|
||||||
|
...this.state(),
|
||||||
|
topbar: { ...this.state().topbar, color },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setSidenavColor(
|
||||||
|
color: LayoutState['sidenav']['color'],
|
||||||
|
persist = true
|
||||||
|
): void {
|
||||||
|
this.setHtmlAttribute('data-sidenav-color', color)
|
||||||
|
if (persist) {
|
||||||
|
this.state.update((s) => ({
|
||||||
|
...s,
|
||||||
|
sidenav: { ...s.sidenav, color },
|
||||||
|
}))
|
||||||
|
this.persistToStorage()
|
||||||
|
}
|
||||||
|
this.layoutStateSubject.next({
|
||||||
|
...this.state(),
|
||||||
|
sidenav: { ...this.state().sidenav, color },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setSidenavSize(size: LayoutState['sidenav']['size'], persist = true): void {
|
||||||
|
this.setHtmlAttribute('data-sidenav-size', size)
|
||||||
|
if (persist) {
|
||||||
|
this.state.update((s) => ({
|
||||||
|
...s,
|
||||||
|
sidenav: { ...s.sidenav, size },
|
||||||
|
}))
|
||||||
|
this.persistToStorage()
|
||||||
|
}
|
||||||
|
this.layoutStateSubject.next({
|
||||||
|
...this.state(),
|
||||||
|
sidenav: { ...this.state().sidenav, size },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMonochrome(persist = true): void {
|
||||||
|
const monochrome = !this.state().monochrome
|
||||||
|
|
||||||
|
if (monochrome) {
|
||||||
|
this.html.classList.toggle('monochrome')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (persist) {
|
||||||
|
this.state.update((s) => ({ ...s, monochrome: monochrome }))
|
||||||
|
this.persistToStorage()
|
||||||
|
}
|
||||||
|
this.layoutStateSubject.next({ ...this.state(), monochrome: monochrome })
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSidenavUser(persist = true): void {
|
||||||
|
const user = !this.state().sidenav.user
|
||||||
|
this.setHtmlAttribute('data-sidenav-user', String(user))
|
||||||
|
if (persist) {
|
||||||
|
this.state.update((s) => ({
|
||||||
|
...s,
|
||||||
|
sidenav: { ...s.sidenav, user },
|
||||||
|
}))
|
||||||
|
this.persistToStorage()
|
||||||
|
}
|
||||||
|
this.layoutStateSubject.next({
|
||||||
|
...this.state(),
|
||||||
|
sidenav: { ...this.state().sidenav, user },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(isLoading: boolean): void {
|
||||||
|
this.state.update((s) => ({
|
||||||
|
...s,
|
||||||
|
isLoading: isLoading,
|
||||||
|
}))
|
||||||
|
this.persistToStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(persist = true): void {
|
||||||
|
this.state.set(defaultState)
|
||||||
|
this.applyAllAttributes()
|
||||||
|
if (persist) this.persistToStorage()
|
||||||
|
|
||||||
|
// required to trigger change detection (for charts)
|
||||||
|
this.layoutStateSubject.next(this.state())
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyAllAttributes(): void {
|
||||||
|
const current = this.state()
|
||||||
|
this.setHtmlAttribute('data-skin', current.skin)
|
||||||
|
this.setHtmlAttribute(
|
||||||
|
'data-bs-theme',
|
||||||
|
current.theme === 'system' ? this.getSystemTheme() : current.theme
|
||||||
|
)
|
||||||
|
this.setHtmlAttribute('data-layout-position', current.position)
|
||||||
|
this.setHtmlAttribute('data-topbar-color', current.topbar.color)
|
||||||
|
if (current.monochrome) {
|
||||||
|
this.html.classList.add('monochrome')
|
||||||
|
} else {
|
||||||
|
this.html.classList.remove('monochrome')
|
||||||
|
}
|
||||||
|
this.setHtmlAttribute('data-sidenav-color', current.sidenav.color)
|
||||||
|
this.setHtmlAttribute('data-sidenav-size', current.sidenav.size)
|
||||||
|
this.setHtmlAttribute('data-sidenav-user', String(current.sidenav.user))
|
||||||
|
}
|
||||||
|
|
||||||
|
openCustomizer(): void {
|
||||||
|
this.offcanvasService.open(Customizer, {
|
||||||
|
position: 'end',
|
||||||
|
backdrop: true,
|
||||||
|
scroll: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setHtmlAttribute(attr: string, value: string) {
|
||||||
|
this.html.setAttribute(attr, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeHtmlAttribute(attr: string) {
|
||||||
|
this.html.removeAttribute(attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
getSystemTheme(): 'light' | 'dark' {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
? 'dark'
|
||||||
|
: 'light'
|
||||||
|
}
|
||||||
|
|
||||||
|
showBackdrop() {
|
||||||
|
const backdrop = document.createElement('div')
|
||||||
|
backdrop.id = 'custom-backdrop'
|
||||||
|
backdrop.className = 'offcanvas-backdrop fade show'
|
||||||
|
document.body.appendChild(backdrop)
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
if (window.innerWidth > 767) {
|
||||||
|
document.body.style.paddingRight = '15px'
|
||||||
|
}
|
||||||
|
backdrop.addEventListener('click', () => {
|
||||||
|
this.html.classList.remove('sidebar-enable')
|
||||||
|
this.hideBackdrop()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
hideBackdrop() {
|
||||||
|
const backdrop = document.getElementById('custom-backdrop')
|
||||||
|
if (backdrop) {
|
||||||
|
document.body.removeChild(backdrop)
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
document.body.style.paddingRight = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
213
src/app/layouts/components/customizer/customizer.html
Normal file
213
src/app/layouts/components/customizer/customizer.html
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
<div
|
||||||
|
class="d-flex justify-content-between text-bg-primary gap-2 p-3"
|
||||||
|
style="background-image: url(assets/images/user-bg-pattern.png)"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1 fw-bold text-white text-uppercase">Admin Customizer</h5>
|
||||||
|
<p class="text-white text-opacity-75 fst-italic fw-medium mb-0">
|
||||||
|
Easily configure layout, styles, and preferences for your admin interface.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-grow-0">
|
||||||
|
<button
|
||||||
|
(click)="close()"
|
||||||
|
type="button"
|
||||||
|
class="d-block btn btn-sm bg-white bg-opacity-25 text-white rounded-circle btn-icon"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerX" class="fs-lg" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ngx-simplebar class="offcanvas-body p-0 h-100" style="max-height: 80vh">
|
||||||
|
<div class="p-3 border-bottom border-dashed">
|
||||||
|
<h5 class="mb-3 fw-bold">Color Scheme</h5>
|
||||||
|
<div class="row">
|
||||||
|
@for (item of themeOptions; track item.theme) {
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="form-check card-radio">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="data-bs-theme"
|
||||||
|
[id]="'theme-' + item.theme"
|
||||||
|
[value]="item.theme"
|
||||||
|
[checked]="layout.theme === item.theme"
|
||||||
|
(change)="layout.setTheme(item.theme)"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="form-check-label p-0 w-100"
|
||||||
|
[for]="'theme-' + item.theme"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
[src]="item.image"
|
||||||
|
alt="layout-img"
|
||||||
|
class="img-fluid overflow-hidden"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<h5 class="text-center text-muted mt-2 mb-0">
|
||||||
|
{{ toPascalCase(item.theme) }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 border-bottom border-dashed">
|
||||||
|
<h5 class="mb-3 fw-bold">Topbar Color</h5>
|
||||||
|
<div class="row g-3">
|
||||||
|
@for (item of topBarColorOptions; track item.color) {
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="form-check card-radio">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="data-topbar-color"
|
||||||
|
[id]="'topbar-color-' + item.color"
|
||||||
|
[value]="item.color"
|
||||||
|
[checked]="layout.topbarColor === item.color"
|
||||||
|
(change)="layout.setTopbarColor(item.color)"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="form-check-label p-0 w-100"
|
||||||
|
[for]="'topbar-color-' + item.color"
|
||||||
|
>
|
||||||
|
<img [src]="item.image" alt="layout-img" class="img-fluid" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<h5 class="text-center text-muted mt-2 mb-0">
|
||||||
|
{{ toPascalCase(item.color) }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 border-bottom border-dashed">
|
||||||
|
<h5 class="mb-3 fw-bold">Sidenav Color</h5>
|
||||||
|
<div class="row g-3">
|
||||||
|
@for (item of sidenavColorOptions; track item.color) {
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="form-check card-radio">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="data-menu-color"
|
||||||
|
[id]="'sidenav-color-' + item.color"
|
||||||
|
[value]="item.color"
|
||||||
|
[checked]="layout.sidenavColor === item.color"
|
||||||
|
(change)="layout.setSidenavColor(item.color)"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="form-check-label p-0 w-100"
|
||||||
|
[for]="'sidenav-color-' + item.color"
|
||||||
|
>
|
||||||
|
<img [src]="item.image" alt="layout-img" class="img-fluid" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<h5 class="text-center text-muted mt-2 mb-0">
|
||||||
|
{{ toPascalCase(item.color) }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 border-bottom border-dashed">
|
||||||
|
<h5 class="mb-3 fw-bold">Sidebar Size</h5>
|
||||||
|
<div class="row g-3">
|
||||||
|
@for (item of sidenavSizeOptions; track item.size) {
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="form-check card-radio">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="data-sidenav-size"
|
||||||
|
[id]="'sidenav-size-' + item.size"
|
||||||
|
[value]="item.size"
|
||||||
|
[checked]="layout.sidenavSize === item.size"
|
||||||
|
(change)="layout.setSidenavSize(item.size)"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="form-check-label p-0 w-100"
|
||||||
|
[for]="'sidenav-size-' + item.size"
|
||||||
|
>
|
||||||
|
<img [src]="item.image" alt="layout-img" class="img-fluid" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<h5 class="text-center text-muted mt-2 mb-0">{{ item.label }}</h5>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 border-bottom border-dashed">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="fw-bold mb-0">Layout Position</h5>
|
||||||
|
<div class="btn-group radio" role="group">
|
||||||
|
@for (item of layoutPositionOptions; track item.position) {
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="data-layout-position"
|
||||||
|
[id]="'layout-position-' + item.position"
|
||||||
|
[value]="item.position"
|
||||||
|
[checked]="layout.position === item.position"
|
||||||
|
(change)="layout.setLayoutPosition(item.position)"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="btn btn-sm btn-soft-primary w-sm"
|
||||||
|
[for]="'layout-position-' + item.position"
|
||||||
|
>
|
||||||
|
{{ toPascalCase(item.position) }}
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<label class="fw-bold m-0" for="sidebaruser-check"
|
||||||
|
>Sidebar User Info</label
|
||||||
|
>
|
||||||
|
</h5>
|
||||||
|
<div class="form-check form-switch fs-lg">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
name="sidebar-user"
|
||||||
|
[checked]="layout.sidenavUser"
|
||||||
|
(change)="layout.toggleSidenavUser()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ngx-simplebar>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="offcanvas-footer border-top p-3 text-center">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<button
|
||||||
|
(click)="layout.reset()"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary fw-semibold py-2 w-100"
|
||||||
|
id="reset-layout"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
target="_blank"
|
||||||
|
class="btn btn-danger bg-gradient py-2 fw-semibold w-100"
|
||||||
|
>Buy Now</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
22
src/app/layouts/components/customizer/customizer.spec.ts
Normal file
22
src/app/layouts/components/customizer/customizer.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { Customizer } from './customizer'
|
||||||
|
|
||||||
|
describe('Customizer', () => {
|
||||||
|
let component: Customizer
|
||||||
|
let fixture: ComponentFixture<Customizer>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Customizer],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Customizer)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
96
src/app/layouts/components/customizer/customizer.ts
Normal file
96
src/app/layouts/components/customizer/customizer.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { NgbActiveOffcanvas } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { SimplebarAngularModule } from 'simplebar-angular'
|
||||||
|
import { NgIcon, provideIcons } from '@ng-icons/core'
|
||||||
|
import { tablerX } from '@ng-icons/tabler-icons'
|
||||||
|
import { LayoutStoreService } from '@core/services/layout-store.service'
|
||||||
|
import {
|
||||||
|
LayoutPositionType,
|
||||||
|
LayoutSkinType,
|
||||||
|
LayoutThemeType,
|
||||||
|
SideNavType,
|
||||||
|
TopBarType,
|
||||||
|
} from '@/app/types/layout'
|
||||||
|
import { toPascalCase } from '@/app/utils/string-utils'
|
||||||
|
|
||||||
|
const light = 'assets/images/layouts/light.svg'
|
||||||
|
const dark = 'assets/images/layouts/dark.svg'
|
||||||
|
|
||||||
|
const lightTopBarImg = 'assets/images/layouts/topbar-light.svg'
|
||||||
|
const darkTopBarImg = 'assets/images/layouts/topbar-dark.svg'
|
||||||
|
|
||||||
|
const lightSideNavImg = 'assets/images/layouts/light.svg'
|
||||||
|
const darkSideNavImg = 'assets/images/layouts/sidenav-dark.svg'
|
||||||
|
|
||||||
|
const compactSideNavImg = 'assets/images/layouts/sidebar-compact.svg'
|
||||||
|
const smallSideNavImg = 'assets/images/layouts/sidebar-condensed.svg'
|
||||||
|
|
||||||
|
type SkinOptionType = {
|
||||||
|
skin: LayoutSkinType
|
||||||
|
image: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeOptionType = {
|
||||||
|
theme: LayoutThemeType
|
||||||
|
image: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TopBarColorOptionType = {
|
||||||
|
color: TopBarType['color']
|
||||||
|
image: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SideNavColorOptionType = {
|
||||||
|
color: SideNavType['color']
|
||||||
|
image: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SideNavSizeOptionType = {
|
||||||
|
label: string
|
||||||
|
size: SideNavType['size']
|
||||||
|
image: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-customizer',
|
||||||
|
imports: [SimplebarAngularModule, NgIcon],
|
||||||
|
templateUrl: './customizer.html',
|
||||||
|
viewProviders: [provideIcons({ tablerX })],
|
||||||
|
})
|
||||||
|
export class Customizer {
|
||||||
|
constructor(
|
||||||
|
public activeOffcanvas: NgbActiveOffcanvas,
|
||||||
|
public layout: LayoutStoreService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.activeOffcanvas.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
themeOptions: ThemeOptionType[] = [
|
||||||
|
{ theme: 'light', image: light },
|
||||||
|
{ theme: 'dark', image: dark },
|
||||||
|
]
|
||||||
|
|
||||||
|
topBarColorOptions: TopBarColorOptionType[] = [
|
||||||
|
{ color: 'light', image: lightTopBarImg },
|
||||||
|
{ color: 'dark', image: darkTopBarImg },
|
||||||
|
]
|
||||||
|
|
||||||
|
sidenavColorOptions: SideNavColorOptionType[] = [
|
||||||
|
{ color: 'light', image: lightSideNavImg },
|
||||||
|
{ color: 'dark', image: darkSideNavImg },
|
||||||
|
]
|
||||||
|
|
||||||
|
sidenavSizeOptions: SideNavSizeOptionType[] = [
|
||||||
|
{ size: 'default', image: lightSideNavImg, label: 'Default' },
|
||||||
|
{ size: 'collapse', image: smallSideNavImg, label: 'collapse' },
|
||||||
|
]
|
||||||
|
|
||||||
|
layoutPositionOptions: { position: LayoutPositionType }[] = [
|
||||||
|
{ position: 'fixed' },
|
||||||
|
{ position: 'scrollable' },
|
||||||
|
]
|
||||||
|
protected readonly toPascalCase = toPascalCase
|
||||||
|
}
|
||||||
185
src/app/layouts/components/data.ts
Normal file
185
src/app/layouts/components/data.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import { MenuItemType } from '@/app/types/layout'
|
||||||
|
|
||||||
|
type UserDropdownItemType = {
|
||||||
|
label?: string
|
||||||
|
icon?: string
|
||||||
|
url?: string
|
||||||
|
isDivider?: boolean
|
||||||
|
isHeader?: boolean
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userDropdownItems: UserDropdownItemType[] = [
|
||||||
|
{
|
||||||
|
label: 'Welcome back!',
|
||||||
|
isHeader: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Profile',
|
||||||
|
icon: 'tablerUserCircle',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Notifications',
|
||||||
|
icon: 'tablerBellRinging',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Account Settings',
|
||||||
|
icon: 'tablerSettings2',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Support Center',
|
||||||
|
icon: 'tablerHeadset',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isDivider: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Lock Screen',
|
||||||
|
icon: 'tablerLock',
|
||||||
|
url: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Log Out',
|
||||||
|
icon: 'tablerLogout2',
|
||||||
|
url: '#',
|
||||||
|
class: 'fw-semibold',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const menuItems: MenuItemType[] = [
|
||||||
|
// ---------------------------
|
||||||
|
// Pilotage & Supervision
|
||||||
|
// ---------------------------
|
||||||
|
{ label: 'Pilotage', isTitle: true },
|
||||||
|
{
|
||||||
|
label: 'Tableau de Bord',
|
||||||
|
icon: 'lucideBarChart2',
|
||||||
|
isCollapsed: true,
|
||||||
|
children: [
|
||||||
|
{ label: 'Vue Globale', url: '/dashboard/overview' },
|
||||||
|
{ label: 'KPIs & Graphiques', url: '/dashboard/kpis' },
|
||||||
|
{ label: 'Rapports', url: '/dashboard/reports' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Rapports Avancés',
|
||||||
|
icon: 'lucideFile',
|
||||||
|
isCollapsed: true,
|
||||||
|
children: [
|
||||||
|
{ label: 'Financiers', url: '/reports/financial' },
|
||||||
|
{ label: 'Opérationnels', url: '/reports/operations' },
|
||||||
|
{ label: 'Export CSV / PDF', url: '/reports/export' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Transactions & Opérations
|
||||||
|
// ---------------------------
|
||||||
|
{ label: 'Business & Transactions', isTitle: true },
|
||||||
|
{
|
||||||
|
label: 'Transactions DCB',
|
||||||
|
icon: 'lucideCreditCard',
|
||||||
|
isCollapsed: true,
|
||||||
|
children: [
|
||||||
|
{ label: 'Liste & Recherche', url: '/transactions/list' },
|
||||||
|
{ label: 'Filtres Avancés', url: '/transactions/filters' },
|
||||||
|
{ label: 'Détails & Logs', url: '/transactions/details' },
|
||||||
|
{ label: 'Export', url: '/transactions/export' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Marchands',
|
||||||
|
icon: 'lucideStore',
|
||||||
|
isCollapsed: true,
|
||||||
|
children: [
|
||||||
|
{ label: 'Liste des Marchands', url: '/merchants/list' },
|
||||||
|
{ label: 'Configuration API / Webhooks', url: '/merchants/config' },
|
||||||
|
{ label: 'Statistiques & Historique', url: '/merchants/history' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Opérateurs',
|
||||||
|
icon: 'lucideServer',
|
||||||
|
isCollapsed: true,
|
||||||
|
children: [
|
||||||
|
{ label: 'Paramètres d’Intégration', url: '/operators/config' },
|
||||||
|
{ label: 'Performance & Monitoring', url: '/operators/stats' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Notifications & Communication
|
||||||
|
// ---------------------------
|
||||||
|
{ label: 'Communication', isTitle: true },
|
||||||
|
{
|
||||||
|
label: 'Notifications',
|
||||||
|
icon: 'lucideBell',
|
||||||
|
isCollapsed: true,
|
||||||
|
children: [
|
||||||
|
{ label: 'Liste des Notifications', url: '/notifications/list' },
|
||||||
|
{ label: 'Filtrage par Type', url: '/notifications/filters' },
|
||||||
|
{ label: 'Actions Automatiques', url: '/notifications/actions' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Webhooks',
|
||||||
|
icon: 'lucideShare',
|
||||||
|
isCollapsed: true,
|
||||||
|
children: [
|
||||||
|
{ label: 'Historique', url: '/webhooks/history' },
|
||||||
|
{ label: 'Statut des Requêtes', url: '/webhooks/status' },
|
||||||
|
{ label: 'Relancer Webhook', url: '/webhooks/retry' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Utilisateurs & Sécurité
|
||||||
|
// ---------------------------
|
||||||
|
{ label: 'Utilisateurs & Sécurité', isTitle: true },
|
||||||
|
{
|
||||||
|
label: 'Gestion des Utilisateurs',
|
||||||
|
icon: 'lucideUsers',
|
||||||
|
isCollapsed: true,
|
||||||
|
children: [
|
||||||
|
{ label: 'Liste des Utilisateurs', url: '/users/list' },
|
||||||
|
{ label: 'Rôles & Permissions', url: '/users/roles' },
|
||||||
|
{ label: 'Audit & Historique', url: '/users/audits' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Authentification',
|
||||||
|
icon: 'lucideFingerprint',
|
||||||
|
isCollapsed: true,
|
||||||
|
children: [
|
||||||
|
{ label: 'Login / Logout', url: '/auth/login' },
|
||||||
|
{ label: 'Réinitialisation Mot de Passe', url: '/auth/reset-password' },
|
||||||
|
{ label: 'Gestion des Sessions', url: '/auth/sessions' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Paramètres & Intégrations
|
||||||
|
// ---------------------------
|
||||||
|
{ label: 'Configuration', isTitle: true },
|
||||||
|
{ label: 'Paramètres Système', icon: 'lucideSettings', url: '/settings' },
|
||||||
|
{ label: 'Intégrations Externes', icon: 'lucidePlug', url: '/integrations' },
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Support & Profil
|
||||||
|
// ---------------------------
|
||||||
|
{ label: 'Support & Profil', isTitle: true },
|
||||||
|
{ label: 'Support', icon: 'lucideLifeBuoy', url: '/support' },
|
||||||
|
{ label: 'Mon Profil', icon: 'lucideUser', url: '/profile' },
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Informations
|
||||||
|
// ---------------------------
|
||||||
|
{ label: 'Informations', isTitle: true },
|
||||||
|
{ label: 'Documentation', icon: 'lucideBookOpen', url: '/documentation' },
|
||||||
|
{ label: 'Aide', icon: 'lucideHelpCircle', url: '/help' },
|
||||||
|
{ label: 'À propos', icon: 'lucideInfo', url: '/about' },
|
||||||
|
]
|
||||||
14
src/app/layouts/components/footer/footer.html
Normal file
14
src/app/layouts/components/footer/footer.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<footer class="footer">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 text-center text-md-start">
|
||||||
|
© {{ currentYear }} {{ appName }}. Tous droits réservés.
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="text-md-end d-none d-md-block">
|
||||||
|
Développé par <span class="fw-semibold">{{ credits.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
22
src/app/layouts/components/footer/footer.spec.ts
Normal file
22
src/app/layouts/components/footer/footer.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { Footer } from './footer'
|
||||||
|
|
||||||
|
describe('Footer', () => {
|
||||||
|
let component: Footer
|
||||||
|
let fixture: ComponentFixture<Footer>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Footer],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Footer)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
13
src/app/layouts/components/footer/footer.ts
Normal file
13
src/app/layouts/components/footer/footer.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { appName, credits, currentYear } from '@/app/constants'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-footer',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './footer.html',
|
||||||
|
})
|
||||||
|
export class Footer {
|
||||||
|
currentYear = currentYear
|
||||||
|
appName = appName
|
||||||
|
credits = credits
|
||||||
|
}
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
<ul class="side-nav">
|
||||||
|
@for (item of menuItems; track $index) {
|
||||||
|
@if (item.isTitle) {
|
||||||
|
<li class="side-nav-title mt-2">{{ item.label }}</li>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!item.isTitle) {
|
||||||
|
<!-- menu item without any child -->
|
||||||
|
@if (!hasSubMenu(item)) {
|
||||||
|
<ng-container *ngTemplateOutlet="MenuItem; context: { item }" />
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- menu item with child -->
|
||||||
|
@if (hasSubMenu(item)) {
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="MenuItemWithChildren; context: { item }"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ng-template #MenuItemWithChildren let-item="item">
|
||||||
|
<li class="side-nav-item" [class.active]="isChildActive(item)">
|
||||||
|
<button
|
||||||
|
(click)="item.isCollapsed = !item.isCollapsed"
|
||||||
|
class="side-nav-link"
|
||||||
|
[attr.aria-expanded]="!item.isCollapsed"
|
||||||
|
>
|
||||||
|
@if (item.icon) {
|
||||||
|
<ng-icon class="menu-icon" [name]="item.icon"></ng-icon>
|
||||||
|
}
|
||||||
|
<span class="menu-text">{{ item.label }}</span>
|
||||||
|
@if (item.badge) {
|
||||||
|
<span [class]="`badge text-bg-${item.badge.variant}`">{{
|
||||||
|
item.badge.text
|
||||||
|
}}</span>
|
||||||
|
}
|
||||||
|
@if (!item.badge) {
|
||||||
|
<span class="menu-arrow">
|
||||||
|
<ng-icon name="tablerChevronDown"></ng-icon>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
#collapse="ngbCollapse"
|
||||||
|
[(ngbCollapse)]="item.isCollapsed"
|
||||||
|
class="collapse"
|
||||||
|
>
|
||||||
|
<ul class="sub-menu">
|
||||||
|
@for (child of item.children; track $index) {
|
||||||
|
<!-- menu item without any child -->
|
||||||
|
@if (!hasSubMenu(child)) {
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="MenuItem; context: { item: child }"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- menu item with child -->
|
||||||
|
@if (hasSubMenu(child)) {
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="MenuItemWithChildren; context: { item: child }"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #MenuItem let-item="item">
|
||||||
|
<li class="side-nav-item" [class.active]="isActive(item)">
|
||||||
|
@if (item.url) {
|
||||||
|
<a
|
||||||
|
[routerLink]="item.url"
|
||||||
|
[target]="item.target"
|
||||||
|
class="side-nav-link"
|
||||||
|
[class.disabled]="item.isDisabled"
|
||||||
|
[class.special-menu]="item.isSpecial"
|
||||||
|
[class.active]="isActive(item)"
|
||||||
|
[attr.data-active-link]="isActive(item)"
|
||||||
|
>
|
||||||
|
@if (item.icon) {
|
||||||
|
<ng-icon class="menu-icon" [name]="item.icon"></ng-icon>
|
||||||
|
}
|
||||||
|
<span class="menu-text">{{ item.label }}</span>
|
||||||
|
@if (item.badge) {
|
||||||
|
<span
|
||||||
|
class="badge text-bg-{{ item.badge.variant }}"
|
||||||
|
[innerHTML]="item.badge.text"
|
||||||
|
></span>
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
</ng-template>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { AppMenuComponent } from './app-menu.component'
|
||||||
|
|
||||||
|
describe('AppMenuComponent', () => {
|
||||||
|
let component: AppMenuComponent
|
||||||
|
let fixture: ComponentFixture<AppMenuComponent>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AppMenuComponent],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AppMenuComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
inject,
|
||||||
|
OnInit,
|
||||||
|
TemplateRef,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { MenuItemType } from '@/app/types/layout'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NavigationEnd, Router, RouterLink } from '@angular/router'
|
||||||
|
import { filter } from 'rxjs'
|
||||||
|
import { scrollToElement } from '@/app/utils/layout-utils'
|
||||||
|
import { menuItems } from '@layouts/components/data'
|
||||||
|
import { LayoutStoreService } from '@core/services/layout-store.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-menu',
|
||||||
|
imports: [NgIcon, NgbCollapse, RouterLink, CommonModule],
|
||||||
|
templateUrl: './app-menu.component.html',
|
||||||
|
})
|
||||||
|
export class AppMenuComponent implements OnInit {
|
||||||
|
router = inject(Router)
|
||||||
|
layout = inject(LayoutStoreService)
|
||||||
|
|
||||||
|
@ViewChild('MenuItemWithChildren', { static: true })
|
||||||
|
menuItemWithChildren!: TemplateRef<{ item: MenuItemType }>
|
||||||
|
|
||||||
|
@ViewChild('MenuItem', { static: true })
|
||||||
|
menuItem!: TemplateRef<{ item: MenuItemType }>
|
||||||
|
|
||||||
|
menuItems = menuItems
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.router.events
|
||||||
|
.pipe(filter((event) => event instanceof NavigationEnd))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.expandActivePaths(this.menuItems)
|
||||||
|
setTimeout(() => this.scrollToActiveLink(), 50)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.expandActivePaths(this.menuItems)
|
||||||
|
setTimeout(() => this.scrollToActiveLink(), 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSubMenu(item: MenuItemType): boolean {
|
||||||
|
return !!item.children
|
||||||
|
}
|
||||||
|
|
||||||
|
expandActivePaths(items: MenuItemType[]) {
|
||||||
|
for (const item of items) {
|
||||||
|
if (this.hasSubMenu(item)) {
|
||||||
|
item.isCollapsed = !this.isChildActive(item)
|
||||||
|
this.expandActivePaths(item.children || [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isChildActive(item: MenuItemType): boolean {
|
||||||
|
if (item.url && this.router.url === item.url) return true
|
||||||
|
if (!item.children) return false
|
||||||
|
return item.children.some((child: MenuItemType) =>
|
||||||
|
this.isChildActive(child)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive(item: MenuItemType): boolean {
|
||||||
|
return this.router.url === item.url
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToActiveLink(): void {
|
||||||
|
const activeItem = document.querySelector(
|
||||||
|
'[data-active-link="true"]'
|
||||||
|
) as HTMLElement
|
||||||
|
const scrollContainer = document.querySelector(
|
||||||
|
'#sidenav .simplebar-content-wrapper'
|
||||||
|
) as HTMLElement
|
||||||
|
|
||||||
|
if (activeItem && scrollContainer) {
|
||||||
|
const containerRect = scrollContainer.getBoundingClientRect()
|
||||||
|
const itemRect = activeItem.getBoundingClientRect()
|
||||||
|
|
||||||
|
const offset = itemRect.top - containerRect.top - window.innerHeight * 0.4
|
||||||
|
|
||||||
|
scrollToElement(scrollContainer, scrollContainer.scrollTop + offset, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<div class="sidenav-user d-flex align-items-center">
|
||||||
|
<img
|
||||||
|
src="assets/images/users/user-2.jpg"
|
||||||
|
class="rounded-circle me-2"
|
||||||
|
width="36"
|
||||||
|
alt="user-image"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h5 class="my-0 fw-semibold">{{ user?.given_name }} - {{ user?.family_name }}</h5>
|
||||||
|
<h6 class="my-0 text-muted">Administrateur</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { UserProfileComponent } from './user-profile.component'
|
||||||
|
|
||||||
|
describe('UserProfileComponent', () => {
|
||||||
|
let component: UserProfileComponent
|
||||||
|
let fixture: ComponentFixture<UserProfileComponent>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [UserProfileComponent],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(UserProfileComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { Component, inject, ChangeDetectorRef } from '@angular/core';
|
||||||
|
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { userDropdownItems } from '@layouts/components/data';
|
||||||
|
import { AuthService } from '@/app/core/services/auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user-profile',
|
||||||
|
standalone: true,
|
||||||
|
imports: [NgbCollapseModule],
|
||||||
|
templateUrl: './user-profile.component.html',
|
||||||
|
})
|
||||||
|
export class UserProfileComponent {
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
private cdr = inject(ChangeDetectorRef);
|
||||||
|
|
||||||
|
user: any = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.loadUser();
|
||||||
|
this.authService.onAuthState().subscribe(() => this.loadUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
loadUser() {
|
||||||
|
this.authService.getProfile().subscribe({
|
||||||
|
next: profile => {
|
||||||
|
this.user = profile;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.user = null;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/app/layouts/components/sidenav/sidenav.component.html
Normal file
20
src/app/layouts/components/sidenav/sidenav.component.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<div class="sidenav-menu">
|
||||||
|
<ngx-simplebar id="sidenav" class="scrollbar">
|
||||||
|
<!-- User -->
|
||||||
|
@if (layout.sidenavUser) {
|
||||||
|
<app-user-profile />
|
||||||
|
}
|
||||||
|
|
||||||
|
<!--- Sidenav Menu -->
|
||||||
|
<app-menu />
|
||||||
|
</ngx-simplebar>
|
||||||
|
<div class="menu-collapse-box d-none d-xl-block">
|
||||||
|
<button class="button-collapse-toggle" (click)="toggleCollapseMenu()">
|
||||||
|
<ng-icon
|
||||||
|
name="lucideSquareChevronLeft"
|
||||||
|
class=".align-middle flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span>Collapse Menu</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
22
src/app/layouts/components/sidenav/sidenav.component.spec.ts
Normal file
22
src/app/layouts/components/sidenav/sidenav.component.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { SidenavComponent } from './sidenav.component'
|
||||||
|
|
||||||
|
describe('SidenavComponent', () => {
|
||||||
|
let component: SidenavComponent
|
||||||
|
let fixture: ComponentFixture<SidenavComponent>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SidenavComponent],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SidenavComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
26
src/app/layouts/components/sidenav/sidenav.component.ts
Normal file
26
src/app/layouts/components/sidenav/sidenav.component.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { UserProfileComponent } from '@layouts/components/sidenav/components/user-profile/user-profile.component'
|
||||||
|
import { AppMenuComponent } from '@layouts/components/sidenav/components/app-menu/app-menu.component'
|
||||||
|
import { SimplebarAngularModule } from 'simplebar-angular'
|
||||||
|
import { LayoutStoreService } from '@core/services/layout-store.service'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-sidenav',
|
||||||
|
imports: [
|
||||||
|
UserProfileComponent,
|
||||||
|
AppMenuComponent,
|
||||||
|
SimplebarAngularModule,
|
||||||
|
NgIcon,
|
||||||
|
],
|
||||||
|
templateUrl: './sidenav.component.html',
|
||||||
|
})
|
||||||
|
export class SidenavComponent {
|
||||||
|
constructor(public layout: LayoutStoreService) {}
|
||||||
|
|
||||||
|
toggleCollapseMenu() {
|
||||||
|
this.layout.setSidenavSize(
|
||||||
|
this.layout.sidenavSize === 'default' ? 'collapse' : 'default'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<div class="topbar-item">
|
||||||
|
<button (click)="layout.openCustomizer()" class="topbar-link" type="button">
|
||||||
|
<ng-icon name="lucideSettings" class="fs-xxl" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { CustomizerToggler } from './customizer-toggler'
|
||||||
|
|
||||||
|
describe('CustomizerToggler', () => {
|
||||||
|
let component: CustomizerToggler
|
||||||
|
let fixture: ComponentFixture<CustomizerToggler>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CustomizerToggler],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CustomizerToggler)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { LayoutStoreService } from '@core/services/layout-store.service'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-customizer-toggler',
|
||||||
|
imports: [NgIcon],
|
||||||
|
templateUrl: './customizer-toggler.html',
|
||||||
|
})
|
||||||
|
export class CustomizerToggler {
|
||||||
|
constructor(public layout: LayoutStoreService) {}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
<div class="topbar-item">
|
||||||
|
<div ngbDropdown placement="bottom-right" class="dropdown">
|
||||||
|
<button
|
||||||
|
class="topbar-link fw-semibold drop-arrow-none"
|
||||||
|
ngbDropdownToggle
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
[src]="selectedLang.flag"
|
||||||
|
alt="user-image"
|
||||||
|
class="w-100 rounded me-2"
|
||||||
|
height="18"
|
||||||
|
id="selected-language-image"
|
||||||
|
/>
|
||||||
|
<span id="selected-language-code">
|
||||||
|
{{ selectedLang.code.toUpperCase() }}</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu class="dropdown-menu dropdown-menu-end">
|
||||||
|
@for (language of languages; track i; let i = $index) {
|
||||||
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
role="button"
|
||||||
|
(click)="changeLanguage(language.code)"
|
||||||
|
[attr.title]="language.name"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
[src]="language.flag"
|
||||||
|
alt="English"
|
||||||
|
class="me-1 rounded"
|
||||||
|
height="18"
|
||||||
|
/>
|
||||||
|
<span class="align-middle">{{ language.nativeName }}</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { LanguageDropdown } from './language-dropdown'
|
||||||
|
|
||||||
|
describe('LanguageDropdown', () => {
|
||||||
|
let component: LanguageDropdown
|
||||||
|
let fixture: ComponentFixture<LanguageDropdown>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [LanguageDropdown],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LanguageDropdown)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core'
|
||||||
|
import {
|
||||||
|
NgbDropdown,
|
||||||
|
NgbDropdownMenu,
|
||||||
|
NgbDropdownToggle,
|
||||||
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { LanguageOptionType } from '@/app/types/layout'
|
||||||
|
import { LanguageService } from '@core/services/language.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-language-dropdown',
|
||||||
|
imports: [NgbDropdown, NgbDropdownMenu, NgbDropdownToggle],
|
||||||
|
templateUrl: './language-dropdown.html',
|
||||||
|
})
|
||||||
|
export class LanguageDropdown implements OnInit {
|
||||||
|
languages: LanguageOptionType[] = []
|
||||||
|
selectedLang: LanguageOptionType = this.languages[0]
|
||||||
|
|
||||||
|
constructor(private langService: LanguageService) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.languages = this.langService.getLanguages()
|
||||||
|
this.langService.currentLang$.subscribe(
|
||||||
|
(lang) => (this.selectedLang = lang)
|
||||||
|
)
|
||||||
|
this.langService.initLanguage()
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLanguage(code: string) {
|
||||||
|
this.langService.setLanguage(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
<div class="topbar-item d-none d-md-flex">
|
||||||
|
<div ngbDropdown class="dropdown">
|
||||||
|
<button
|
||||||
|
ngbDropdownToggle
|
||||||
|
class="topbar-link btn shadow-none btn-link px-2 dropdown-toggle drop-arrow-none show"
|
||||||
|
>
|
||||||
|
Mega Menu
|
||||||
|
<ng-icon name="tablerChevronDown" class="ms-1" />
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu class="dropdown-menu dropdown-menu-xxl p-0">
|
||||||
|
<div class="h-100" style="max-height: 380px" data-simplebar>
|
||||||
|
<div class="row g-0">
|
||||||
|
@for (item of megaMenuItems; track i;let i = $index) {
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="p-3">
|
||||||
|
<h5 class="fw-semibold fs-sm dropdown-header">
|
||||||
|
{{ item.title }}
|
||||||
|
</h5>
|
||||||
|
<ul class="list-unstyled megamenu-list">
|
||||||
|
@for (link of item.links; track i;let i = $index) {
|
||||||
|
<li>
|
||||||
|
<a [routerLink]="link.url" class="dropdown-item"
|
||||||
|
>{{ link.label }}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { MegaMenu } from './mega-menu'
|
||||||
|
|
||||||
|
describe('MegaMenu', () => {
|
||||||
|
let component: MegaMenu
|
||||||
|
let fixture: ComponentFixture<MegaMenu>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MegaMenu],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(MegaMenu)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
import {
|
||||||
|
NgbDropdown,
|
||||||
|
NgbDropdownMenu,
|
||||||
|
NgbDropdownToggle,
|
||||||
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
|
||||||
|
type MegaMenuType = {
|
||||||
|
title: string
|
||||||
|
links: {
|
||||||
|
label: string
|
||||||
|
url: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-mega-menu',
|
||||||
|
imports: [
|
||||||
|
RouterLink,
|
||||||
|
NgIcon,
|
||||||
|
NgbDropdown,
|
||||||
|
NgbDropdownToggle,
|
||||||
|
NgbDropdownMenu,
|
||||||
|
],
|
||||||
|
templateUrl: './mega-menu.html',
|
||||||
|
})
|
||||||
|
export class MegaMenu {
|
||||||
|
megaMenuItems: MegaMenuType[] = [
|
||||||
|
{
|
||||||
|
title: 'Workspace Tools',
|
||||||
|
links: [
|
||||||
|
{ label: 'My Dashboard', url: '#;' },
|
||||||
|
{ label: 'Recent Activity', url: '#;' },
|
||||||
|
{ label: 'Notification Center', url: '#;' },
|
||||||
|
{ label: 'File Manager', url: '#;' },
|
||||||
|
{ label: 'Calendar View', url: '#;' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Team Operations',
|
||||||
|
links: [
|
||||||
|
{ label: 'Team Overview', url: '#;' },
|
||||||
|
{ label: 'Meeting Schedule', url: '#;' },
|
||||||
|
{ label: 'Time Sheets', url: '#;' },
|
||||||
|
{ label: 'Feedback Hub', url: '#;' },
|
||||||
|
{ label: 'Resource Allocation', url: '#;' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Account Settings',
|
||||||
|
links: [
|
||||||
|
{ label: 'Profile Settings', url: '#;' },
|
||||||
|
{ label: 'Billing & Plans', url: '#;' },
|
||||||
|
{ label: 'Integrations', url: '#;' },
|
||||||
|
{ label: 'Privacy &Security', url: '#;' },
|
||||||
|
{ label: 'Support Center', url: '#;' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
<div class="topbar-item">
|
||||||
|
<div ngbDropdown>
|
||||||
|
<button
|
||||||
|
class="topbar-link dropdown-toggle drop-arrow-none"
|
||||||
|
ngbDropdownToggle
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideBell" class="fs-xxl" />
|
||||||
|
<span class="badge badge-square text-bg-success topbar-badge"
|
||||||
|
>{{ notifications.length }}</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="dropdown-menu-lg" ngbDropdownMenu>
|
||||||
|
<div class="px-3 py-2 border-bottom">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col">
|
||||||
|
<h6 class="m-0 fs-md fw-semibold">Notifications</h6>
|
||||||
|
</div>
|
||||||
|
<div class="col text-end">
|
||||||
|
<span class="badge text-bg-light badge-label py-1">
|
||||||
|
{{ notifications.length }} Alerts
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ngx-simplebar style="max-height: 300px; overflow: auto">
|
||||||
|
@for (notification of notifications; track notification.id) {
|
||||||
|
<button
|
||||||
|
class="dropdown-item notification-item py-2 text-wrap"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span class="d-flex gap-2">
|
||||||
|
@if (notification.avatar) {
|
||||||
|
<span class="flex-shrink-0">
|
||||||
|
<img
|
||||||
|
[src]="notification.avatar"
|
||||||
|
class="avatar-md rounded-circle"
|
||||||
|
alt="User Avatar"
|
||||||
|
width="36"
|
||||||
|
height="36"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
} @if (notification.icon) {
|
||||||
|
<span class="avatar-md flex-shrink-0">
|
||||||
|
<span
|
||||||
|
class="avatar-title bg-primary-subtle text-primary rounded-circle fs-22"
|
||||||
|
>
|
||||||
|
<ng-icon name="{{ notification.icon }}" class="fs-md" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<span class="flex-grow-1 text-muted">
|
||||||
|
@if (notification.type === 'message') {
|
||||||
|
<span class="fw-medium text-body">{{ notification.title }}</span>
|
||||||
|
{{ notification.description }} } @else {
|
||||||
|
<span class="fw-medium text-body">
|
||||||
|
{{ notification.title ? notification.title :
|
||||||
|
notification.description }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<br />
|
||||||
|
<span class="fs-xs">{{ notification.time }}</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex-shrink-0 text-muted btn shadow-none btn-link p-0"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideCircleX" class="fs-xxl"></ng-icon>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
} @if (notifications.length === 0) {
|
||||||
|
<div class="text-center py-4 text-muted">No notifications</div>
|
||||||
|
}
|
||||||
|
</ngx-simplebar>
|
||||||
|
<a
|
||||||
|
href="javascript:void(0);"
|
||||||
|
class="dropdown-item text-center text-reset text-decoration-underline link-offset-2 fw-bold notify-item border-top border-light py-2"
|
||||||
|
>
|
||||||
|
View All Notifications
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { NotificationDropdown } from './notification-dropdown'
|
||||||
|
|
||||||
|
describe('NotificationDropdown', () => {
|
||||||
|
let component: NotificationDropdown
|
||||||
|
let fixture: ComponentFixture<NotificationDropdown>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [NotificationDropdown],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(NotificationDropdown)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import {
|
||||||
|
NgbDropdown,
|
||||||
|
NgbDropdownMenu,
|
||||||
|
NgbDropdownToggle,
|
||||||
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { SimplebarAngularModule } from 'simplebar-angular'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
|
||||||
|
const user3 = 'assets/images/users/user-3.jpg'
|
||||||
|
const user4 = 'assets/images/users/user-4.jpg'
|
||||||
|
|
||||||
|
type NotificationType = {
|
||||||
|
id: string
|
||||||
|
type: 'notification' | 'message'
|
||||||
|
avatar?: string
|
||||||
|
icon?: string
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
time: string
|
||||||
|
isActive?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-notification-dropdown',
|
||||||
|
imports: [
|
||||||
|
NgbDropdown,
|
||||||
|
NgbDropdownToggle,
|
||||||
|
SimplebarAngularModule,
|
||||||
|
NgbDropdownMenu,
|
||||||
|
NgIcon,
|
||||||
|
],
|
||||||
|
templateUrl: './notification-dropdown.html',
|
||||||
|
})
|
||||||
|
export class NotificationDropdown {
|
||||||
|
notifications: NotificationType[] = [
|
||||||
|
{
|
||||||
|
id: 'notification-1',
|
||||||
|
type: 'notification',
|
||||||
|
icon: 'lucideCloudCog',
|
||||||
|
title: 'Backup completed successfully',
|
||||||
|
time: 'Just now',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notification-2',
|
||||||
|
type: 'notification',
|
||||||
|
icon: 'lucideBug',
|
||||||
|
title: 'New bug reported in Payment Module',
|
||||||
|
time: '8 minutes ago',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'message-3',
|
||||||
|
type: 'notification',
|
||||||
|
icon: 'lucideFileWarning',
|
||||||
|
description: 'Security policy update required for your account',
|
||||||
|
time: '22 minutes ago',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notification-6',
|
||||||
|
type: 'notification',
|
||||||
|
icon: 'lucideMail',
|
||||||
|
title: "You've received a new support ticket",
|
||||||
|
time: '18 minutes ago',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notification-7',
|
||||||
|
type: 'notification',
|
||||||
|
icon: 'lucideCalendarClock',
|
||||||
|
title: 'System maintenance starts at 12 AM',
|
||||||
|
time: '1 hour ago',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
<div class="topbar-item me-2">
|
||||||
|
<div ngbDropdown>
|
||||||
|
<button
|
||||||
|
class="topbar-link fw-semibold drop-arrow-none"
|
||||||
|
ngbDropdownToggle
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="assets/images/themes/{{ selectedSkin }}.svg"
|
||||||
|
alt="user-image"
|
||||||
|
class="w-100 rounded me-2"
|
||||||
|
height="18"
|
||||||
|
/>
|
||||||
|
<span class="text-nowrap"> {{ selectedSkin | titlecase}} </span>
|
||||||
|
<span class="dot-blink" aria-label="live status indicator"></span>
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu class="dropdown-menu-lg dropdown-menu-end p-1">
|
||||||
|
<ngx-simplebar class="h-100" style="max-height: 250px">
|
||||||
|
<div class="row g-0">
|
||||||
|
@for (group of [skinOptions.slice(0, 7), skinOptions.slice(7)]; track
|
||||||
|
i; let i = $index) {
|
||||||
|
<div class="col-md-6">
|
||||||
|
@for (skin of group; track skin.name) {
|
||||||
|
<button
|
||||||
|
class="dropdown-item position-relative"
|
||||||
|
[class.drop-custom-active]="skin.name === selectedSkin"
|
||||||
|
(click)="setSkin(skin.name)"
|
||||||
|
>
|
||||||
|
<img [src]="skin.img" alt="" class="me-1 rounded" height="18" />
|
||||||
|
<span class="align-middle">{{ skin.name | titlecase}}</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ngx-simplebar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { ThemeDropdown } from './theme-dropdown'
|
||||||
|
|
||||||
|
describe('ThemeDropdown', () => {
|
||||||
|
let component: ThemeDropdown
|
||||||
|
let fixture: ComponentFixture<ThemeDropdown>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ThemeDropdown],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ThemeDropdown)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { Component, inject } from '@angular/core'
|
||||||
|
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { LanguageOptionType, LayoutSkinType } from '@/app/types/layout'
|
||||||
|
import { LayoutStoreService } from '@core/services/layout-store.service'
|
||||||
|
import { TitleCasePipe } from '@angular/common'
|
||||||
|
import { SimplebarAngularModule } from 'simplebar-angular'
|
||||||
|
|
||||||
|
const shadcn = 'assets/images/themes/shadcn.svg'
|
||||||
|
const corporate = 'assets/images/themes/corporate.svg'
|
||||||
|
const spotify = 'assets/images/themes/spotify.svg'
|
||||||
|
const saas = 'assets/images/themes/saas.svg'
|
||||||
|
const nature = 'assets/images/themes/nature.svg'
|
||||||
|
const vintage = 'assets/images/themes/vintage.svg'
|
||||||
|
const leafline = 'assets/images/themes/leafline.svg'
|
||||||
|
const ghibli = 'assets/images/themes/ghibli.svg'
|
||||||
|
const slack = 'assets/images/themes/slack.svg'
|
||||||
|
const material = 'assets/images/themes/material.svg'
|
||||||
|
const flat = 'assets/images/themes/flat.svg'
|
||||||
|
const pastel = 'assets/images/themes/pastel.svg'
|
||||||
|
const caffieine = 'assets/images/themes/caffieine.svg'
|
||||||
|
const redshift = 'assets/images/themes/redshift.svg'
|
||||||
|
|
||||||
|
type SkinOptionType = {
|
||||||
|
name: LayoutSkinType
|
||||||
|
img: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-theme-dropdown',
|
||||||
|
imports: [NgbDropdownModule, TitleCasePipe, SimplebarAngularModule],
|
||||||
|
templateUrl: './theme-dropdown.html',
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class ThemeDropdown {
|
||||||
|
private layoutStore = inject(LayoutStoreService)
|
||||||
|
skinOptions: SkinOptionType[] = [
|
||||||
|
{ name: 'shadcn', img: shadcn },
|
||||||
|
{ name: 'corporate', img: corporate },
|
||||||
|
{ name: 'spotify', img: spotify },
|
||||||
|
{ name: 'saas', img: saas },
|
||||||
|
{ name: 'nature', img: nature },
|
||||||
|
{ name: 'vintage', img: vintage },
|
||||||
|
{ name: 'leafline', img: leafline },
|
||||||
|
{ name: 'ghibli', img: ghibli },
|
||||||
|
{ name: 'slack', img: slack },
|
||||||
|
{ name: 'material', img: material },
|
||||||
|
{ name: 'flat', img: flat },
|
||||||
|
{ name: 'pastel', img: pastel },
|
||||||
|
{ name: 'caffieine', img: caffieine },
|
||||||
|
{ name: 'redshift', img: redshift },
|
||||||
|
]
|
||||||
|
get selectedSkin(): LayoutSkinType {
|
||||||
|
return this.layoutStore.skin
|
||||||
|
}
|
||||||
|
|
||||||
|
setSkin(skin: LayoutSkinType) {
|
||||||
|
this.layoutStore.setSkin(skin, true) // persist
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<div class="topbar-item">
|
||||||
|
<button (click)="toggleTheme()" class="topbar-link" type="button">
|
||||||
|
@if (layout.theme === 'light') {
|
||||||
|
<ng-icon name="lucideMoon" class="fs-xxl mode-light-moon" />
|
||||||
|
} @if (layout.theme === 'dark') {
|
||||||
|
<ng-icon name="lucideSun" class="fs-xxl mode-light-sun" />
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { ThemeToggler } from './theme-toggler'
|
||||||
|
|
||||||
|
describe('ThemeToggler', () => {
|
||||||
|
let component: ThemeToggler
|
||||||
|
let fixture: ComponentFixture<ThemeToggler>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ThemeToggler],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ThemeToggler)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { LayoutStoreService } from '@core/services/layout-store.service'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-theme-toggler',
|
||||||
|
imports: [NgIcon],
|
||||||
|
templateUrl: './theme-toggler.html',
|
||||||
|
})
|
||||||
|
export class ThemeToggler {
|
||||||
|
constructor(public layout: LayoutStoreService) {}
|
||||||
|
|
||||||
|
toggleTheme() {
|
||||||
|
if (this.layout.theme === 'light') {
|
||||||
|
this.layout.setTheme('dark')
|
||||||
|
} else {
|
||||||
|
this.layout.setTheme('light')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
<div class="topbar-item nav-user">
|
||||||
|
<div ngbDropdown [placement]="'bottom-end'" class="dropdown">
|
||||||
|
<button
|
||||||
|
ngbDropdownToggle
|
||||||
|
class="topbar-link dropdown-toggle drop-arrow-none px-2"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="assets/images/users/user-2.jpg"
|
||||||
|
width="32"
|
||||||
|
class="rounded-circle d-flex"
|
||||||
|
alt="user-image"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu class="dropdown-menu dropdown-menu-end">
|
||||||
|
@for (item of menuItems; track i; let i = $index) {
|
||||||
|
<div>
|
||||||
|
@if (item.isHeader) {
|
||||||
|
<div class="dropdown-header noti-title">
|
||||||
|
<h6 class="text-overflow m-0">{{ item.label }}</h6>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (item.isDivider) {
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
}
|
||||||
|
@if (!item.isHeader && !item.isDivider) {
|
||||||
|
@if (item.label === 'Log Out') {
|
||||||
|
<!-- Bouton Logout avec appel de méthode -->
|
||||||
|
<button
|
||||||
|
class="dropdown-item fw-semibold text-danger"
|
||||||
|
(click)="logout()">
|
||||||
|
<ng-icon
|
||||||
|
name="tablerLogout2"
|
||||||
|
size="17"
|
||||||
|
class="align-middle d-inline-flex align-items-center me-2"
|
||||||
|
/>
|
||||||
|
<span class="align-middle">Log Out</span>
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<!-- Autres items avec navigation normale -->
|
||||||
|
<a [routerLink]="item.url" class="dropdown-item" [class]="item.class">
|
||||||
|
<ng-icon
|
||||||
|
[name]="item.icon"
|
||||||
|
size="17"
|
||||||
|
class="align-middle d-inline-flex align-items-center me-2"
|
||||||
|
/>
|
||||||
|
<span class="align-middle" [innerHTML]="item.label"></span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { UserProfile } from './user-profile'
|
||||||
|
|
||||||
|
describe('UserProfile', () => {
|
||||||
|
let component: UserProfile
|
||||||
|
let fixture: ComponentFixture<UserProfile>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [UserProfile],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(UserProfile)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import { Component, inject } from '@angular/core'
|
||||||
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
import {
|
||||||
|
NgbDropdown,
|
||||||
|
NgbDropdownMenu,
|
||||||
|
NgbDropdownToggle,
|
||||||
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { userDropdownItems } from '@layouts/components/data'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user-profile-topbar',
|
||||||
|
imports: [
|
||||||
|
NgbDropdown,
|
||||||
|
NgbDropdownMenu,
|
||||||
|
NgbDropdownToggle,
|
||||||
|
RouterLink,
|
||||||
|
NgIcon,
|
||||||
|
],
|
||||||
|
templateUrl: './user-profile.html',
|
||||||
|
})
|
||||||
|
export class UserProfile {
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
|
||||||
|
menuItems = userDropdownItems;
|
||||||
|
|
||||||
|
// Méthode pour gérer le logout
|
||||||
|
logout() {
|
||||||
|
this.authService.logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour gérer les clics sur les items du menu
|
||||||
|
handleItemClick(item: any) {
|
||||||
|
if (item.label === 'Log Out') {
|
||||||
|
this.logout();
|
||||||
|
}
|
||||||
|
// Pour les autres items, la navigation se fait via routerLink
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/app/layouts/components/topbar/topbar.html
Normal file
85
src/app/layouts/components/topbar/topbar.html
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<header class="app-topbar">
|
||||||
|
<div class="container-fluid topbar-menu">
|
||||||
|
<div class="d-flex align-items-center align-items-center gap-2">
|
||||||
|
<div class="logo-topbar">
|
||||||
|
<a routerLink="/" class="logo-dark">
|
||||||
|
<span class="d-flex align-items-center gap-1">
|
||||||
|
<span class="avatar avatar-xs rounded-circle text-bg-dark">
|
||||||
|
<span class="avatar-title">
|
||||||
|
<ng-icon name="lucideSparkles" class="fs-md" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="logo-text text-body fw-bold fs-xl">BO Admin</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a routerLink="/" class="logo-light">
|
||||||
|
<span class="d-flex align-items-center gap-1">
|
||||||
|
<span class="avatar avatar-xs rounded-circle text-bg-dark">
|
||||||
|
<span class="avatar-title">
|
||||||
|
<ng-icon name="lucideSparkles" class="fs-md" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="logo-text text-white fw-bold fs-xl">BO Admin</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-lg-none d-flex mx-1">
|
||||||
|
<a routerLink="/">
|
||||||
|
<img src="assets/images/logo-sm.png" height="28" alt="Logo" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="button-collapse-toggle d-xl-none"
|
||||||
|
(click)="toggleSidebar()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucideMenu" class="fs-22"></ng-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="topbar-item d-none d-lg-flex">
|
||||||
|
<a
|
||||||
|
[routerLink]="[]"
|
||||||
|
class="topbar-link btn shadow-none btn-link px-2 disabled"
|
||||||
|
>
|
||||||
|
v1.0.0</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<div class="app-search d-none d-xl-flex me-xl-2">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
class="form-control topbar-search"
|
||||||
|
name="search"
|
||||||
|
placeholder="Search for something..."
|
||||||
|
/>
|
||||||
|
<ng-icon name="lucideSearch" class="app-search-icon text-muted" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-theme-dropdown />
|
||||||
|
|
||||||
|
<app-language-dropdown class="d-none d-md-flex" />
|
||||||
|
|
||||||
|
<app-notification-dropdown />
|
||||||
|
|
||||||
|
<app-customizer-toggler class="d-none d-sm-flex" />
|
||||||
|
|
||||||
|
<app-theme-toggler class="d-none d-sm-flex" />
|
||||||
|
|
||||||
|
<div class="topbar-item d-none d-sm-flex">
|
||||||
|
<button
|
||||||
|
class="topbar-link"
|
||||||
|
id="monochrome-mode"
|
||||||
|
type="button"
|
||||||
|
(click)="layout.toggleMonochrome()"
|
||||||
|
>
|
||||||
|
<ng-icon name="lucidePalette" class="fs-xxl mode-light-moon" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-user-profile-topbar />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
22
src/app/layouts/components/topbar/topbar.spec.ts
Normal file
22
src/app/layouts/components/topbar/topbar.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { Topbar } from './topbar'
|
||||||
|
|
||||||
|
describe('Topbar', () => {
|
||||||
|
let component: Topbar
|
||||||
|
let fixture: ComponentFixture<Topbar>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Topbar],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Topbar)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
50
src/app/layouts/components/topbar/topbar.ts
Normal file
50
src/app/layouts/components/topbar/topbar.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
import { LayoutStoreService } from '@core/services/layout-store.service'
|
||||||
|
|
||||||
|
import { MegaMenu } from '@layouts/components/topbar/components/mega-menu/mega-menu'
|
||||||
|
import { LanguageDropdown } from '@layouts/components/topbar/components/language-dropdown/language-dropdown'
|
||||||
|
import { ThemeToggler } from '@layouts/components/topbar/components/theme-toggler/theme-toggler'
|
||||||
|
import { CustomizerToggler } from '@layouts/components/topbar/components/customizer-toggler/customizer-toggler'
|
||||||
|
import { UserProfile } from '@layouts/components/topbar/components/user-profile/user-profile'
|
||||||
|
import { NotificationDropdown } from '@layouts/components/topbar/components/notification-dropdown/notification-dropdown'
|
||||||
|
import { ThemeDropdown } from '@layouts/components/topbar/components/theme-dropdown/theme-dropdown'
|
||||||
|
import {
|
||||||
|
NgbActiveOffcanvas,
|
||||||
|
NgbDropdownModule,
|
||||||
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-topbar',
|
||||||
|
imports: [
|
||||||
|
NgIcon,
|
||||||
|
RouterLink,
|
||||||
|
NgbDropdownModule,
|
||||||
|
LanguageDropdown,
|
||||||
|
CustomizerToggler,
|
||||||
|
ThemeToggler,
|
||||||
|
UserProfile,
|
||||||
|
NotificationDropdown,
|
||||||
|
ThemeDropdown,
|
||||||
|
],
|
||||||
|
templateUrl: './topbar.html',
|
||||||
|
})
|
||||||
|
export class Topbar {
|
||||||
|
constructor(public layout: LayoutStoreService) {}
|
||||||
|
|
||||||
|
toggleSidebar() {
|
||||||
|
const html = document.documentElement
|
||||||
|
const currentSize = html.getAttribute('data-sidenav-size')
|
||||||
|
const savedSize = this.layout.sidenavSize
|
||||||
|
|
||||||
|
if (currentSize === 'offcanvas') {
|
||||||
|
html.classList.toggle('sidebar-enable')
|
||||||
|
this.layout.showBackdrop()
|
||||||
|
} else {
|
||||||
|
this.layout.setSidenavSize(
|
||||||
|
currentSize === 'collapse' ? 'default' : 'collapse'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/app/layouts/vertical-layout/vertical-layout.html
Normal file
19
src/app/layouts/vertical-layout/vertical-layout.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
@if (layout.isLoading) {
|
||||||
|
<div class="loading-overlay">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<app-topbar />
|
||||||
|
|
||||||
|
<app-sidenav />
|
||||||
|
|
||||||
|
<div class="content-page">
|
||||||
|
<router-outlet />
|
||||||
|
|
||||||
|
<app-footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
22
src/app/layouts/vertical-layout/vertical-layout.spec.ts
Normal file
22
src/app/layouts/vertical-layout/vertical-layout.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { VerticalLayout } from './vertical-layout'
|
||||||
|
|
||||||
|
describe('VerticalLayout', () => {
|
||||||
|
let component: VerticalLayout
|
||||||
|
let fixture: ComponentFixture<VerticalLayout>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [VerticalLayout],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(VerticalLayout)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
41
src/app/layouts/vertical-layout/vertical-layout.ts
Normal file
41
src/app/layouts/vertical-layout/vertical-layout.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
|
import { RouterOutlet } from '@angular/router'
|
||||||
|
import { LayoutStoreService } from '@core/services/layout-store.service'
|
||||||
|
import { SidenavComponent } from '@layouts/components/sidenav/sidenav.component'
|
||||||
|
import { Topbar } from '@layouts/components/topbar/topbar'
|
||||||
|
import { Footer } from '@layouts/components/footer/footer'
|
||||||
|
import { debounceTime, fromEvent, Subscription } from 'rxjs'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-vertical-layout',
|
||||||
|
imports: [RouterOutlet, SidenavComponent, Topbar, Footer],
|
||||||
|
templateUrl: './vertical-layout.html',
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class VerticalLayout implements OnInit, OnDestroy {
|
||||||
|
constructor(public layout: LayoutStoreService) {}
|
||||||
|
|
||||||
|
resizeSubscription!: Subscription
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.onResize()
|
||||||
|
|
||||||
|
this.resizeSubscription = fromEvent(window, 'resize')
|
||||||
|
.pipe(debounceTime(200))
|
||||||
|
.subscribe(() => this.onResize())
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize(): void {
|
||||||
|
const width = window.innerWidth
|
||||||
|
|
||||||
|
if (width <= 1140) {
|
||||||
|
this.layout.setSidenavSize('offcanvas')
|
||||||
|
} else {
|
||||||
|
this.layout.setSidenavSize('default')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.resizeSubscription?.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/app/modules/about/about.html
Normal file
1
src/app/modules/about/about.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<p>About</p>
|
||||||
2
src/app/modules/about/about.spec.ts
Normal file
2
src/app/modules/about/about.spec.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { About } from './about';
|
||||||
|
describe('About', () => {});
|
||||||
7
src/app/modules/about/about.ts
Normal file
7
src/app/modules/about/about.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-about',
|
||||||
|
templateUrl: './about.html',
|
||||||
|
})
|
||||||
|
export class About {}
|
||||||
8
src/app/modules/about/services/about.service.ts
Normal file
8
src/app/modules/about/services/about.service.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AboutService {
|
||||||
|
constructor() {}
|
||||||
|
}
|
||||||
40
src/app/modules/auth/auth.route.ts
Normal file
40
src/app/modules/auth/auth.route.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Routes } from '@angular/router'
|
||||||
|
import { SignIn } from '@/app/modules/auth/sign-in'
|
||||||
|
import { SignUp } from '@/app/modules/auth/sign-up'
|
||||||
|
import { ResetPassword } from '@/app/modules/auth/reset-password'
|
||||||
|
import { NewPassword } from '@/app/modules/auth/new-password'
|
||||||
|
import { TwoFactor } from '@/app/modules/auth/two-factor'
|
||||||
|
import { LockScreen } from '@/app/modules/auth/lock-screen'
|
||||||
|
|
||||||
|
export const Auth_ROUTES: Routes = [
|
||||||
|
{
|
||||||
|
path: 'auth/sign-in',
|
||||||
|
component: SignIn,
|
||||||
|
data: { title: 'Sign In' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'auth/sign-up',
|
||||||
|
component: SignUp,
|
||||||
|
data: { title: 'Sign Up' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'auth/reset-password',
|
||||||
|
component: ResetPassword,
|
||||||
|
data: { title: 'Reset Password' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'auth/new-password',
|
||||||
|
component: NewPassword,
|
||||||
|
data: { title: 'New Password' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'auth/two-factor',
|
||||||
|
component: TwoFactor,
|
||||||
|
data: { title: 'Two Factor' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'auth/lock-screen',
|
||||||
|
component: LockScreen,
|
||||||
|
data: { title: 'Lock Screen' },
|
||||||
|
},
|
||||||
|
]
|
||||||
88
src/app/modules/auth/lock-screen.ts
Normal file
88
src/app/modules/auth/lock-screen.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { appName, credits, currentYear } from '@/app/constants'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { AppLogo } from '@app/components/app-logo'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-lock-screen',
|
||||||
|
imports: [RouterLink, AppLogo],
|
||||||
|
template: `
|
||||||
|
<div class="auth-box overflow-hidden align-items-center d-flex">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-xxl-4 col-md-6 col-sm-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="auth-brand mb-4">
|
||||||
|
<app-app-logo />
|
||||||
|
<p class="text-muted w-lg-75 mt-3">
|
||||||
|
This screen is locked. Enter your password to continue
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<img
|
||||||
|
src="assets/images/users/user-2.jpg"
|
||||||
|
class="rounded-circle img-thumbnail avatar-xxl mb-2"
|
||||||
|
alt="thumbnail"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<h5 class="my-0 fw-semibold">Maxine Kennedy</h5>
|
||||||
|
<h6 class="my-0 text-muted">Admin Head</h6>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="userPassword" class="form-label"
|
||||||
|
>Password <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="userPassword"
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary fw-semibold py-2"
|
||||||
|
>
|
||||||
|
Unlock
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="text-muted text-center mt-4 mb-0">
|
||||||
|
Not you? Return to
|
||||||
|
<a
|
||||||
|
routerLink="/auth/sign-in"
|
||||||
|
class="text-decoration-underline link-offset-3 fw-semibold"
|
||||||
|
>Sign in</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-center text-muted mt-4 mb-0">
|
||||||
|
© {{ currentYear }} {{ appName }}. Tous droits réservés. — Développé par
|
||||||
|
<span class="fw-semibold">{{ credits.name }}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class LockScreen {
|
||||||
|
protected readonly appName = appName
|
||||||
|
protected readonly currentYear = currentYear
|
||||||
|
protected readonly credits = credits
|
||||||
|
}
|
||||||
163
src/app/modules/auth/new-password.ts
Normal file
163
src/app/modules/auth/new-password.ts
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { appName, credits, currentYear } from '@/app/constants'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { PasswordStrengthBar } from '@app/components/password-strength-bar'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
|
import { AppLogo } from '@app/components/app-logo'
|
||||||
|
import { NgOtpInputModule } from 'ng-otp-input'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-new-password',
|
||||||
|
imports: [
|
||||||
|
RouterLink,
|
||||||
|
PasswordStrengthBar,
|
||||||
|
FormsModule,
|
||||||
|
AppLogo,
|
||||||
|
NgOtpInputModule,
|
||||||
|
],
|
||||||
|
template: `
|
||||||
|
<div class="auth-box overflow-hidden align-items-center d-flex">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-xxl-4 col-md-6 col-sm-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="auth-brand mb-4">
|
||||||
|
<app-app-logo />
|
||||||
|
<p class="text-muted mt-3">
|
||||||
|
We've emailed you a 6-digit verification code. Please enter
|
||||||
|
it below to confirm your email address
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="userEmail" class="form-label"
|
||||||
|
>Email address <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
id="userEmail"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"
|
||||||
|
>Enter your 6-digit code
|
||||||
|
<span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<ng-otp-input
|
||||||
|
[config]="{
|
||||||
|
length: 6,
|
||||||
|
allowNumbersOnly: true,
|
||||||
|
inputClass: 'form-control text-center',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
</ng-otp-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" data-password="bar">
|
||||||
|
<label for="userPassword" class="form-label"
|
||||||
|
>Password <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
[(ngModel)]="password"
|
||||||
|
class="form-control"
|
||||||
|
id="userPassword"
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<app-password-strength-bar [password]="password" />
|
||||||
|
<div class="password-bar my-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="userNewPassword" class="form-label"
|
||||||
|
>Confirm New Password
|
||||||
|
<span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="userNewPassword"
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input form-check-input-light fs-14"
|
||||||
|
type="checkbox"
|
||||||
|
id="termAndPolicy"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="termAndPolicy"
|
||||||
|
>Agree the Terms & Policy</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary fw-semibold py-2"
|
||||||
|
>
|
||||||
|
Update Password
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="mt-4 text-muted text-center mb-4">
|
||||||
|
Don’t have a code?
|
||||||
|
<a
|
||||||
|
href="javascript:void(0);"
|
||||||
|
class="text-decoration-underline link-offset-2 fw-semibold"
|
||||||
|
>Resend</a
|
||||||
|
>
|
||||||
|
or
|
||||||
|
<a
|
||||||
|
href="javascript:void(0);"
|
||||||
|
class="text-decoration-underline link-offset-2 fw-semibold"
|
||||||
|
>Call Us</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p class="text-muted text-center mb-0">
|
||||||
|
Return to
|
||||||
|
<a
|
||||||
|
routerLink="/auth/sign-in"
|
||||||
|
class="text-decoration-underline link-offset-3 fw-semibold"
|
||||||
|
>Sign in</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-center text-muted mt-4 mb-0">
|
||||||
|
© {{ currentYear }} {{ appName }}. Tous droits réservés. — Développé par
|
||||||
|
<span class="fw-semibold">{{ credits.name }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class NewPassword {
|
||||||
|
password: string = ''
|
||||||
|
protected readonly appName = appName
|
||||||
|
protected readonly currentYear = currentYear
|
||||||
|
protected readonly credits = credits
|
||||||
|
}
|
||||||
89
src/app/modules/auth/reset-password.ts
Normal file
89
src/app/modules/auth/reset-password.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { appName, credits, currentYear } from '@/app/constants'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { AppLogo } from '@app/components/app-logo'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-reset-password',
|
||||||
|
imports: [RouterLink, AppLogo],
|
||||||
|
template: `
|
||||||
|
<div class="auth-box overflow-hidden align-items-center d-flex">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-xxl-4 col-md-6 col-sm-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="auth-brand mb-4">
|
||||||
|
<app-app-logo />
|
||||||
|
<p class="text-muted w-lg-75 mt-3">
|
||||||
|
Enter your email address and we'll send you a link to reset
|
||||||
|
your password.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="userEmail" class="form-label"
|
||||||
|
>Email address <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
id="userEmail"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input form-check-input-light fs-14"
|
||||||
|
type="checkbox"
|
||||||
|
id="termAndPolicy"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="termAndPolicy"
|
||||||
|
>Agree the Terms & Policy</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary fw-semibold py-2"
|
||||||
|
>
|
||||||
|
Send Request
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="text-muted text-center mt-4 mb-0">
|
||||||
|
Return to
|
||||||
|
<a
|
||||||
|
routerLink="/auth/sign-in"
|
||||||
|
class="text-decoration-underline link-offset-3 fw-semibold"
|
||||||
|
>Sign in</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-center text-muted mt-4 mb-0">
|
||||||
|
© {{ currentYear }} {{ appName }}. Tous droits réservés. — Développé par
|
||||||
|
<span class="fw-semibold">{{ credits.name }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class ResetPassword {
|
||||||
|
protected readonly appName = appName
|
||||||
|
protected readonly currentYear = currentYear
|
||||||
|
protected readonly credits = credits
|
||||||
|
}
|
||||||
191
src/app/modules/auth/sign-in.ts
Normal file
191
src/app/modules/auth/sign-in.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import { Component, inject, ChangeDetectorRef } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule, NgForm } from '@angular/forms';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { AuthService } from '@core/services/auth.service';
|
||||||
|
import { AppLogo } from '@app/components/app-logo';
|
||||||
|
import { PasswordStrengthBar } from '@app/components/password-strength-bar';
|
||||||
|
import { appName, credits, currentYear } from '@/app/constants';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-sign-in',
|
||||||
|
standalone: true,
|
||||||
|
imports: [FormsModule, CommonModule, AppLogo, PasswordStrengthBar],
|
||||||
|
template: `
|
||||||
|
<div class="auth-box overflow-hidden align-items-center d-flex">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-xxl-4 col-md-6 col-sm-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="auth-brand mb-4">
|
||||||
|
<app-app-logo />
|
||||||
|
<p class="text-muted w-lg-75 mt-3">
|
||||||
|
Let's get you signed in. Enter your username and password to continue.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form #loginForm="ngForm" (ngSubmit)="onSubmit(loginForm)" novalidate>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">
|
||||||
|
Username <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Enter username"
|
||||||
|
required
|
||||||
|
[(ngModel)]="username"
|
||||||
|
#usernameCtrl="ngModel"
|
||||||
|
[class.is-invalid]="usernameCtrl.invalid && usernameCtrl.touched"
|
||||||
|
/>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Username is required
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">
|
||||||
|
Password <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
[(ngModel)]="password"
|
||||||
|
#passwordCtrl="ngModel"
|
||||||
|
[class.is-invalid]="passwordCtrl.invalid && passwordCtrl.touched"
|
||||||
|
/>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Password is required
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password strength bar -->
|
||||||
|
<app-password-strength-bar [password]="password || ''"></app-password-strength-bar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Server-side error message -->
|
||||||
|
<div *ngIf="errorMessage"
|
||||||
|
id="error-message"
|
||||||
|
class="alert alert-danger mt-3"
|
||||||
|
role="alert"
|
||||||
|
tabindex="-1">
|
||||||
|
<i class="ri-error-warning-line me-2"></i>
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="rememberMe"
|
||||||
|
class="form-check-input"
|
||||||
|
[(ngModel)]="rememberMe"
|
||||||
|
name="rememberMe"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="rememberMe">
|
||||||
|
Keep me signed in
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
routerLink="/auth/reset-password"
|
||||||
|
class="text-decoration-underline link-offset-3 text-muted"
|
||||||
|
>
|
||||||
|
Forgot Password?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary fw-semibold py-2"
|
||||||
|
[disabled]="loginForm.invalid || loading"
|
||||||
|
>
|
||||||
|
{{ loading ? 'Signing In...' : 'Sign In' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="text-muted text-center mt-4 mb-0">
|
||||||
|
New here?
|
||||||
|
<a
|
||||||
|
routerLink="/auth/sign-up"
|
||||||
|
class="text-decoration-underline link-offset-3 fw-semibold"
|
||||||
|
>
|
||||||
|
Create an account
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-center text-muted mt-4 mb-0">
|
||||||
|
© {{ currentYear }} {{ appName }}. Tous droits réservés. — Développé par
|
||||||
|
<span class="fw-semibold">{{ credits.name }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class SignIn {
|
||||||
|
protected readonly appName = appName;
|
||||||
|
protected readonly currentYear = currentYear;
|
||||||
|
protected readonly credits = credits;
|
||||||
|
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
private router = inject(Router);
|
||||||
|
private cdRef = inject(ChangeDetectorRef);
|
||||||
|
|
||||||
|
username: string = '';
|
||||||
|
password: string = '';
|
||||||
|
rememberMe: boolean = false;
|
||||||
|
loading: boolean = false;
|
||||||
|
errorMessage: string | null = null;
|
||||||
|
|
||||||
|
onSubmit(form: NgForm) {
|
||||||
|
if (form.invalid) {
|
||||||
|
form.control.markAllAsTouched();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.errorMessage = null;
|
||||||
|
|
||||||
|
this.authService.login(this.username, this.password).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.router.navigate(['/dashboard']);
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.errorMessage = err.error?.message || 'Login failed';
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
// Forcer la mise à jour de la vue
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
|
||||||
|
// Scroll et focus sur l'erreur
|
||||||
|
this.scrollToError();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollToError() {
|
||||||
|
setTimeout(() => {
|
||||||
|
const errorElement = document.getElementById('error-message');
|
||||||
|
if (errorElement) {
|
||||||
|
errorElement.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center'
|
||||||
|
});
|
||||||
|
errorElement.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/app/modules/auth/sign-up.ts
Normal file
124
src/app/modules/auth/sign-up.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { appName, credits, currentYear } from '@/app/constants'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { AppLogo } from '@app/components/app-logo'
|
||||||
|
import { PasswordStrengthBar } from '@app/components/password-strength-bar'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-sign-up',
|
||||||
|
imports: [RouterLink, AppLogo, FormsModule, PasswordStrengthBar],
|
||||||
|
template: `
|
||||||
|
<div class="auth-box overflow-hidden align-items-center d-flex">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-xxl-4 col-md-6 col-sm-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="auth-brand mb-4">
|
||||||
|
<app-app-logo />
|
||||||
|
<p class="text-muted w-lg-75 mt-3">
|
||||||
|
Let’s get you started. Create your account by entering your
|
||||||
|
details below.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="userName" class="form-label"
|
||||||
|
>Name <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="userName"
|
||||||
|
placeholder="Damian D."
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="userEmail" class="form-label"
|
||||||
|
>Email address <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
id="userEmail"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" data-password="bar">
|
||||||
|
<label for="userPassword" class="form-label"
|
||||||
|
>Password <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
class="form-control"
|
||||||
|
id="userPassword"
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
[(ngModel)]="password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<app-password-strength-bar [password]="password" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input form-check-input-light fs-14 mt-0"
|
||||||
|
type="checkbox"
|
||||||
|
id="termAndPolicy"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="termAndPolicy"
|
||||||
|
>Agree the Terms & Policy</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary fw-semibold py-2"
|
||||||
|
>
|
||||||
|
Create Account
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="text-muted text-center mt-4 mb-0">
|
||||||
|
Already have an account?
|
||||||
|
<a
|
||||||
|
routerLink="/auth/sign-in"
|
||||||
|
class="text-decoration-underline link-offset-3 fw-semibold"
|
||||||
|
>Login</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-center text-muted mt-4 mb-0">
|
||||||
|
© {{ currentYear }} {{ appName }}. Tous droits réservés. — Développé par
|
||||||
|
<span class="fw-semibold">{{ credits.name }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class SignUp {
|
||||||
|
password: string = ''
|
||||||
|
protected readonly appName = appName
|
||||||
|
protected readonly currentYear = currentYear
|
||||||
|
protected readonly credits = credits
|
||||||
|
}
|
||||||
92
src/app/modules/auth/two-factor.ts
Normal file
92
src/app/modules/auth/two-factor.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { AppLogo } from '@app/components/app-logo'
|
||||||
|
import { NgOtpInputComponent } from 'ng-otp-input'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { appName, credits, currentYear } from '@/app/constants'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-two-factor',
|
||||||
|
imports: [AppLogo, NgOtpInputComponent, RouterLink],
|
||||||
|
template: `
|
||||||
|
<div class="auth-box overflow-hidden align-items-center d-flex">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-xxl-4 col-md-6 col-sm-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="auth-brand mb-4">
|
||||||
|
<app-app-logo />
|
||||||
|
<p class="text-muted w-lg-75 mt-3">
|
||||||
|
We've emailed you a 6-digit verification code we sent to
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<div class="fw-bold fs-4">+ (12) ******6789</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<label class="form-label"
|
||||||
|
>Enter your 6-digit code
|
||||||
|
<span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<ngx-otp-input
|
||||||
|
[config]="{
|
||||||
|
length: 6,
|
||||||
|
allowNumbersOnly: true,
|
||||||
|
inputClass: 'form-control text-center mb-3',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
</ngx-otp-input>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary fw-semibold py-2"
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="mt-4 text-muted text-center mb-4">
|
||||||
|
Don’t have a code?
|
||||||
|
<a
|
||||||
|
href="javascript:void(0);"
|
||||||
|
class="text-decoration-underline link-offset-2 fw-semibold"
|
||||||
|
>Resend</a
|
||||||
|
>
|
||||||
|
or
|
||||||
|
<a
|
||||||
|
href="javascript:void(0);"
|
||||||
|
class="text-decoration-underline link-offset-2 fw-semibold"
|
||||||
|
>Call Us</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p class="text-muted text-center mb-0">
|
||||||
|
Return to
|
||||||
|
<a
|
||||||
|
routerLink="/auth/sign-in"
|
||||||
|
class="text-decoration-underline link-offset-3 fw-semibold"
|
||||||
|
>Sign in</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-center text-muted mt-4 mb-0">
|
||||||
|
© {{ currentYear }} {{ appName }}. Tous droits réservés. — Développé par
|
||||||
|
<span class="fw-semibold">{{ credits.name }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class TwoFactor {
|
||||||
|
protected readonly appName = appName
|
||||||
|
protected readonly currentYear = currentYear
|
||||||
|
protected readonly credits = credits
|
||||||
|
}
|
||||||
297
src/app/modules/components/basic-wizard.ts
Normal file
297
src/app/modules/components/basic-wizard.ts
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { UiCard } from '@app/components/ui-card'
|
||||||
|
import { wizardSteps } from '@/app/modules/merchants/data'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-basic-wizard',
|
||||||
|
imports: [UiCard, NgIcon],
|
||||||
|
template: `
|
||||||
|
<app-ui-card title="Basic Wizard">
|
||||||
|
<span helper-text class="badge badge-soft-success badge-label fs-xxs py-1"
|
||||||
|
>Exclusive</span
|
||||||
|
>
|
||||||
|
<div class="ins-wizard" card-body>
|
||||||
|
<ul class="nav nav-tabs wizard-tabs" role="tablist">
|
||||||
|
@for (step of wizardSteps; track $index; let i = $index) {
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
href="javascript:void(0);"
|
||||||
|
class="nav-link"
|
||||||
|
[class]="
|
||||||
|
i < currentStep
|
||||||
|
? 'wizard-item-done'
|
||||||
|
: i === currentStep
|
||||||
|
? 'active'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
(click)="goToStep(i)"
|
||||||
|
>
|
||||||
|
<span class="d-flex align-items-center">
|
||||||
|
<ng-icon [name]="step.icon" class="fs-32" />
|
||||||
|
<span class="flex-grow-1 ms-2 text-truncate">
|
||||||
|
<span
|
||||||
|
class="mb-0 lh-base d-block fw-semibold text-body fs-base"
|
||||||
|
>{{ step.title }}</span
|
||||||
|
>
|
||||||
|
<span class="mb-0 fw-normal">{{ step.subtitle }}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content pt-3">
|
||||||
|
@for (step of wizardSteps; track $index; let i = $index) {
|
||||||
|
<div
|
||||||
|
class="tab-pane fade"
|
||||||
|
[class.show]="currentStep === i"
|
||||||
|
[class.active]="currentStep === i"
|
||||||
|
>
|
||||||
|
@switch (i) {
|
||||||
|
@case (0) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">Full Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Enter your full name"
|
||||||
|
name="fullname"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Enter your email"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">Phone Number</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
class="form-control"
|
||||||
|
name="phone"
|
||||||
|
placeholder="Enter your phone number"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">Date of Birth</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
data-provider="flatpickr"
|
||||||
|
data-date-format="d M, Y"
|
||||||
|
placeholder="Select your DOB"
|
||||||
|
class="form-control"
|
||||||
|
name="dob"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@case (1) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">Street Address</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="street"
|
||||||
|
placeholder="123 Main St"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">City</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="city"
|
||||||
|
placeholder="e.g., New York"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">State</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="state"
|
||||||
|
placeholder="e.g., California"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">Zip Code</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="zip"
|
||||||
|
placeholder="e.g., 10001"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@case (2) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">Choose Course</label>
|
||||||
|
<select class="form-select" name="course" required>
|
||||||
|
<option value="">Select</option>
|
||||||
|
<option value="Engineering">Engineering</option>
|
||||||
|
<option value="Medical">Medical</option>
|
||||||
|
<option value="Business">Business</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">Enrollment Type</label>
|
||||||
|
<select class="form-select" name="enrollment" required>
|
||||||
|
<option value="">Select</option>
|
||||||
|
<option value="Full Time">Full Time</option>
|
||||||
|
<option value="Part Time">Part Time</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">Preferred Batch Time</label>
|
||||||
|
<select class="form-select" name="batch_time" required>
|
||||||
|
<option value="">Select Time</option>
|
||||||
|
<option value="Morning">Morning (8am – 12pm)</option>
|
||||||
|
<option value="Afternoon">Afternoon (1pm – 5pm)</option>
|
||||||
|
<option value="Evening">Evening (6pm – 9pm)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">Mode of Study</label>
|
||||||
|
<select class="form-select" name="mode" required>
|
||||||
|
<option value="">Select Mode</option>
|
||||||
|
<option value="Offline">Offline</option>
|
||||||
|
<option value="Online">Online</option>
|
||||||
|
<option value="Hybrid">Hybrid</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@case (3) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">Parent/Guardian Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="parent_name"
|
||||||
|
placeholder="e.g., John Doe"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">Relation</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="relation"
|
||||||
|
placeholder="e.g., Father, Mother"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">Parent Phone</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
class="form-control"
|
||||||
|
name="parent_phone"
|
||||||
|
placeholder="e.g., +1 555 123 4567"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-6 mb-3">
|
||||||
|
<label class="form-label">Parent Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
name="parent_email"
|
||||||
|
placeholder="e.g., parent@example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@case (4) {
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Upload ID Proof</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="form-control"
|
||||||
|
name="id_proof"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Upload Previous Marksheet</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="form-control"
|
||||||
|
name="marksheet"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<div class="d-flex justify-content-between mt-3">
|
||||||
|
@if (i > 0) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
(click)="previousStep()"
|
||||||
|
>
|
||||||
|
← Back:
|
||||||
|
{{ step.title }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (i < wizardSteps.length - 1) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary ms-auto"
|
||||||
|
(click)="nextStep()"
|
||||||
|
>
|
||||||
|
Next: {{ step.title }} →
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (i === wizardSteps.length - 1) {
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
Submit Application
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-ui-card>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class BasicWizard {
|
||||||
|
currentStep = 0
|
||||||
|
|
||||||
|
nextStep() {
|
||||||
|
if (this.currentStep < wizardSteps.length - 1) this.currentStep++
|
||||||
|
}
|
||||||
|
|
||||||
|
previousStep() {
|
||||||
|
if (this.currentStep > 0) this.currentStep--
|
||||||
|
}
|
||||||
|
|
||||||
|
goToStep(index: number) {
|
||||||
|
this.currentStep = index
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonly wizardSteps = wizardSteps
|
||||||
|
}
|
||||||
102
src/app/modules/components/browser-defaults.ts
Normal file
102
src/app/modules/components/browser-defaults.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { UiCard } from '@app/components/ui-card'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-browser-defaults',
|
||||||
|
imports: [UiCard, NgIcon],
|
||||||
|
template: `
|
||||||
|
<app-ui-card title="Browser Defaults">
|
||||||
|
<a
|
||||||
|
helper-text
|
||||||
|
href="https://getbootstrap.com/docs/5.3/forms/validation/#browser-defaults"
|
||||||
|
target="_blank"
|
||||||
|
class="icon-link icon-link-hover link-secondary link-underline-secondarlink-secondary link-underline-opacity-25 fw-semibold"
|
||||||
|
>View Docs
|
||||||
|
<ng-icon name="tablerArrowRight" class="bi fs-lg" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<form card-body class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="validationDefault01" class="form-label">First name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="validationDefault01"
|
||||||
|
value="Mark"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="validationDefault02" class="form-label">Last name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="validationDefault02"
|
||||||
|
value="Otto"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="validationDefaultUsername" class="form-label"
|
||||||
|
>Username</label
|
||||||
|
>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text" id="inputGroupPrepend2">@</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="validationDefaultUsername"
|
||||||
|
aria-describedby="inputGroupPrepend2"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="validationDefault03" class="form-label">City</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="validationDefault03"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="validationDefault04" class="form-label">State</label>
|
||||||
|
<select class="form-select" id="validationDefault04" required>
|
||||||
|
<option selected disabled value="">Choose...</option>
|
||||||
|
<option>...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="validationDefault05" class="form-label">Zip</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="validationDefault05"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
value=""
|
||||||
|
id="invalidCheck2"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="invalidCheck2">
|
||||||
|
Agree to terms and conditions
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<button class="btn btn-primary" type="submit">Submit form</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</app-ui-card>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class BrowserDefaults {}
|
||||||
722
src/app/modules/components/checkboxes-and-radios.ts
Normal file
722
src/app/modules/components/checkboxes-and-radios.ts
Normal file
@ -0,0 +1,722 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { UiCard } from '@app/components/ui-card'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-checkboxes-and-radios',
|
||||||
|
imports: [UiCard],
|
||||||
|
template: `
|
||||||
|
<app-ui-card title="Checks, Radios and Switches">
|
||||||
|
<div class="row" card-body>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Checkboxes</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="checkDefault"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="checkDefault"
|
||||||
|
>Default Checkbox</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input form-check-input-light"
|
||||||
|
id="checkLight"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="checkLight"
|
||||||
|
>Light Checkbox</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="checkInline1"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="checkInline1"
|
||||||
|
>Inline 1</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="checkInline2"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="checkInline2"
|
||||||
|
>Inline 2</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
value=""
|
||||||
|
id="checkIndeterminate"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="checkIndeterminate">
|
||||||
|
Disabled indeterminate checkbox
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
value=""
|
||||||
|
id="checkCheckedDisabled"
|
||||||
|
checked
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="checkCheckedDisabled">
|
||||||
|
Disabled checked checkbox
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-3">Sizes</h5>
|
||||||
|
|
||||||
|
<div class="form-check fs-lg mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input mt-1"
|
||||||
|
id="checkSize1"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label fs-base" for="checkSize1"
|
||||||
|
>I'm 16px Checkbox</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check form-check-secondary fs-xxl mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input mt-1"
|
||||||
|
id="checkSize2"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label fs-base" for="checkSize2"
|
||||||
|
>i'm 20px Checkbox</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Switches</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="form-check form-switch mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="switch1"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="switch1"
|
||||||
|
>Enabled Switch</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="switch2"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="switch2"
|
||||||
|
>Disabled Switch</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-3">Sizes</h5>
|
||||||
|
|
||||||
|
<div class="form-check form-switch fs-lg mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input mt-1"
|
||||||
|
id="checkboxSize16"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label fs-base" for="checkboxSize16"
|
||||||
|
>I'm 16px Switch</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="form-check form-switch form-check-secondary fs-xxl mb-2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input mt-1"
|
||||||
|
id="checkboxSize20"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label fs-base" for="checkboxSize20"
|
||||||
|
>I'm 20px Switch</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Colored Checkboxes</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="d-flex flex-wrap gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="form-check form-check-primary mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="checkPrimary"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="checkPrimary"
|
||||||
|
>Primary</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-secondary mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="checkSecondary"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="checkSecondary"
|
||||||
|
>Secondary</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-success mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="checkSuccess"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="checkSuccess"
|
||||||
|
>Success</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-info mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="checkInfo"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="checkInfo">Info</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="form-check form-check-warning mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="checkWarning"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="checkWarning"
|
||||||
|
>Warning</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-danger mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="checkDanger"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="checkDanger"
|
||||||
|
>Danger</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-dark">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="checkDark"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="checkDark">Dark</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Colored Switches</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="d-flex flex-wrap gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="form-check form-check-primary form-switch mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="switchPrimary"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="switchPrimary"
|
||||||
|
>Primary</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-secondary form-switch mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="switchSecondary"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="switchSecondary"
|
||||||
|
>Secondary</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-success form-switch mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="switchSuccess"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="switchSuccess"
|
||||||
|
>Success</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-info form-switch mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="switchInfo"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="switchInfo"
|
||||||
|
>Info</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="form-check form-check-warning form-switch mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="switchWarning"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="switchWarning"
|
||||||
|
>Warning</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-danger form-switch mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="switchDanger"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="switchDanger"
|
||||||
|
>Danger</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-dark form-switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="switchDark"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="switchDark"
|
||||||
|
>Dark</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Radios</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="form-check-input"
|
||||||
|
name="gridRadio"
|
||||||
|
id="radio1"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radio1">Option 1</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="form-check-input"
|
||||||
|
name="gridRadio"
|
||||||
|
id="radio2"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radio2">Option 2</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="inlineRadioOptions"
|
||||||
|
id="inlineRadio1"
|
||||||
|
value="option1"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="inlineRadio1"
|
||||||
|
>Inline 1</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="inlineRadioOptions"
|
||||||
|
id="inlineRadio2"
|
||||||
|
value="option2"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="inlineRadio2"
|
||||||
|
>Inline 2</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="disabledRadioOptions"
|
||||||
|
id="inlineRadio3"
|
||||||
|
value="option3"
|
||||||
|
checked
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="inlineRadio3"
|
||||||
|
>Disabled Checked Radio</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-3">Sizes</h5>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="form-check fs-lg form-check-inline">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="paymentMethod"
|
||||||
|
id="radioCash"
|
||||||
|
value="cash"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label fs-base" for="radioCash"
|
||||||
|
>Cash</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check fs-lg form-check-inline">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="paymentMethod"
|
||||||
|
id="radioCard"
|
||||||
|
value="card"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label fs-base" for="radioCard"
|
||||||
|
>Card</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="form-check fs-xxl form-check-inline">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="deliveryOption"
|
||||||
|
id="radioPickup"
|
||||||
|
value="pickup"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label fs-base" for="radioPickup"
|
||||||
|
>Pickup</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check fs-xxl form-check-inline">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="deliveryOption"
|
||||||
|
id="radioHome"
|
||||||
|
value="home"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label fs-base" for="radioHome"
|
||||||
|
>Home Delivery</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Reverse</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="w-lg-50">
|
||||||
|
<div class="form-check form-check-reverse mb-2">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
value=""
|
||||||
|
id="reverseCheck1"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="reverseCheck1">
|
||||||
|
Reverse checkbox
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-reverse mb-2">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
value=""
|
||||||
|
id="reverseCheck2"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="reverseCheck2">
|
||||||
|
Disabled reverse radio
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check form-switch form-check-reverse">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="switchCheckReverse"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="switchCheckReverse"
|
||||||
|
>Reverse switch checkbox input</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Colored Radios</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="d-flex flex-wrap gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="form-check form-check-primary mb-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="form-check-input"
|
||||||
|
name="radioPrimary"
|
||||||
|
id="radioPrimary"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioPrimary"
|
||||||
|
>Primary</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-secondary mb-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="form-check-input"
|
||||||
|
name="radioSecondary"
|
||||||
|
id="radioSecondary"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioSecondary"
|
||||||
|
>Secondary</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-success mb-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="form-check-input"
|
||||||
|
name="radioSuccess"
|
||||||
|
id="radioSuccess"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioSuccess"
|
||||||
|
>Success</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-info mb-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="form-check-input"
|
||||||
|
name="radioInfo"
|
||||||
|
id="radioInfo"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioInfo">Info</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="form-check form-check-warning mb-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="form-check-input"
|
||||||
|
name="radioWarning"
|
||||||
|
id="radioWarning"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioWarning"
|
||||||
|
>Warning</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-danger mb-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="form-check-input"
|
||||||
|
name="radioDanger"
|
||||||
|
id="radioDanger"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioDanger"
|
||||||
|
>Danger</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-dark">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="form-check-input"
|
||||||
|
name="radioDark"
|
||||||
|
id="radioDark"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="radioDark">Dark</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Checkbox Toggle</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="mb-2">
|
||||||
|
<input type="checkbox" class="btn-check" id="btncheck1" />
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck1"
|
||||||
|
>Single Toggle</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
aria-label="Checkbox toggle group"
|
||||||
|
>
|
||||||
|
<input type="checkbox" class="btn-check" id="btncheck2" />
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck2"
|
||||||
|
>One</label
|
||||||
|
>
|
||||||
|
|
||||||
|
<input type="checkbox" class="btn-check" id="btncheck3" />
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck3"
|
||||||
|
>Two</label
|
||||||
|
>
|
||||||
|
|
||||||
|
<input type="checkbox" class="btn-check" id="btncheck4" />
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck4"
|
||||||
|
>Three</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Radio Toggle</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
aria-label="Radio toggle group"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="btnradio"
|
||||||
|
id="btnradio1"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-secondary" for="btnradio1"
|
||||||
|
>Left</label
|
||||||
|
>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="btnradio"
|
||||||
|
id="btnradio2"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-secondary" for="btnradio2"
|
||||||
|
>Middle</label
|
||||||
|
>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="btnradio"
|
||||||
|
id="btnradio3"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-secondary" for="btnradio3"
|
||||||
|
>Right</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-ui-card>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class CheckboxesAndRadios {}
|
||||||
373
src/app/modules/components/choicesjs.ts
Normal file
373
src/app/modules/components/choicesjs.ts
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { UiCard } from '@app/components/ui-card'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
import { ChoiceSelectInputDirective } from '@core/directive/choices-select.directive'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-choicesjs',
|
||||||
|
imports: [UiCard, NgIcon, ChoiceSelectInputDirective],
|
||||||
|
template: `
|
||||||
|
<app-ui-card title="Choices.Js" bodyClass="p-0">
|
||||||
|
<div card-body>
|
||||||
|
<div class="card-body rounded-bottom-0">
|
||||||
|
<p class="text-muted mb-2">
|
||||||
|
Choices.js is a lightweight, configurable select box/text input
|
||||||
|
plugin. Similar to Select2 and Selectize but without the jQuery
|
||||||
|
dependency.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="btn btn-link shadow-none p-0 fw-medium"
|
||||||
|
href="https://choices-js.github.io/Choices/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
View Official Website
|
||||||
|
<ng-icon name="tablerChevronRight" class="ms-1"></ng-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body border-top-0 rounded-top-0">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Single Select Input: Default</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code
|
||||||
|
>[options]="{searchEnabled:false}"</code
|
||||||
|
>
|
||||||
|
attribute to set a default
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<select
|
||||||
|
choicesSelect
|
||||||
|
class="form-control"
|
||||||
|
[options]="{ searchEnabled: false }"
|
||||||
|
>
|
||||||
|
<option value="">This is a placeholder</option>
|
||||||
|
<option value="Choice 1">Choice 1</option>
|
||||||
|
<option value="Choice 2">Choice 2</option>
|
||||||
|
<option value="Choice 3">Choice 3</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Single Select Input: Option Groups</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code
|
||||||
|
>[options]="{searchEnabled:false}"</code
|
||||||
|
>
|
||||||
|
attribute to set a group
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<select
|
||||||
|
choicesSelect
|
||||||
|
[options]="{ searchEnabled: false }"
|
||||||
|
class="form-control"
|
||||||
|
id="choices-single-groups"
|
||||||
|
name="choices-single-groups"
|
||||||
|
>
|
||||||
|
<option value="">Choose a city</option>
|
||||||
|
<optgroup label="UK">
|
||||||
|
<option value="London">London</option>
|
||||||
|
<option value="Manchester">Manchester</option>
|
||||||
|
<option value="Liverpool">Liverpool</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="FR">
|
||||||
|
<option value="Paris">Paris</option>
|
||||||
|
<option value="Lyon">Lyon</option>
|
||||||
|
<option value="Marseille">Marseille</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="DE" disabled>
|
||||||
|
<option value="Hamburg">Hamburg</option>
|
||||||
|
<option value="Munich">Munich</option>
|
||||||
|
<option value="Berlin">Berlin</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="US">
|
||||||
|
<option value="New York">New York</option>
|
||||||
|
<option value="Washington" disabled>Washington</option>
|
||||||
|
<option value="Michigan">Michigan</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="SP">
|
||||||
|
<option value="Madrid">Madrid</option>
|
||||||
|
<option value="Barcelona">Barcelona</option>
|
||||||
|
<option value="Malaga">Malaga</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="CA">
|
||||||
|
<option value="Montreal">Montreal</option>
|
||||||
|
<option value="Toronto">Toronto</option>
|
||||||
|
<option value="Vancouver">Vancouver</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Single Select Input: No Search</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code
|
||||||
|
>[options]="{searchEnabled:false,removeItemButton:true}"</code
|
||||||
|
>
|
||||||
|
attribute to set a no search
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<select
|
||||||
|
choicesSelect
|
||||||
|
[options]="{ searchEnabled: false, removeItemButton: true }"
|
||||||
|
class="form-control"
|
||||||
|
id="choices-single-no-search"
|
||||||
|
name="choices-single-no-search"
|
||||||
|
>
|
||||||
|
<option value="Zero">Zero</option>
|
||||||
|
<option value="One">One</option>
|
||||||
|
<option value="Two">Two</option>
|
||||||
|
<option value="Three">Three</option>
|
||||||
|
<option value="Four">Four</option>
|
||||||
|
<option value="Five">Five</option>
|
||||||
|
<option value="Six">Six</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Single Select Input: No Sorting</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code
|
||||||
|
>[options]="{shouldSortItems:false}"</code
|
||||||
|
>
|
||||||
|
attribute to set a no Sorting
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<select
|
||||||
|
choicesSelect
|
||||||
|
[options]="{ shouldSortItems: false, searchEnabled: false }"
|
||||||
|
class="form-control"
|
||||||
|
name="choices-single-no-sorting"
|
||||||
|
>
|
||||||
|
<option value="Madrid">Madrid</option>
|
||||||
|
<option value="Toronto">Toronto</option>
|
||||||
|
<option value="Vancouver">Vancouver</option>
|
||||||
|
<option value="London">London</option>
|
||||||
|
<option value="Manchester">Manchester</option>
|
||||||
|
<option value="Liverpool">Liverpool</option>
|
||||||
|
<option value="Paris">Paris</option>
|
||||||
|
<option value="Malaga">Malaga</option>
|
||||||
|
<option value="Washington" disabled>Washington</option>
|
||||||
|
<option value="Lyon">Lyon</option>
|
||||||
|
<option value="Marseille">Marseille</option>
|
||||||
|
<option value="Hamburg">Hamburg</option>
|
||||||
|
<option value="Munich">Munich</option>
|
||||||
|
<option value="Barcelona">Barcelona</option>
|
||||||
|
<option value="Berlin">Berlin</option>
|
||||||
|
<option value="Montreal">Montreal</option>
|
||||||
|
<option value="New York">New York</option>
|
||||||
|
<option value="Michigan">Michigan</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Multiple Select Input: Default</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code>multiple</code>
|
||||||
|
attribute
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<select
|
||||||
|
choicesSelect
|
||||||
|
multiple
|
||||||
|
class="form-control"
|
||||||
|
id="choices-multiple-default"
|
||||||
|
name="choices-multiple-default"
|
||||||
|
[options]="{ searchEnabled: false }"
|
||||||
|
>
|
||||||
|
<option value="Choice 1" selected>Choice 1</option>
|
||||||
|
<option value="Choice 2">Choice 2</option>
|
||||||
|
<option value="Choice 3">Choice 3</option>
|
||||||
|
<option value="Choice 4" disabled>Choice 4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Multiple Select Input: With Remove Button</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code>
|
||||||
|
multiple
|
||||||
|
[options]="{removeItemButton:true}"</code
|
||||||
|
>
|
||||||
|
<code>[multiple]="true"</code>
|
||||||
|
attribute
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<select
|
||||||
|
class="form-control"
|
||||||
|
choicesSelect
|
||||||
|
[options]="{ removeItemButton: true }"
|
||||||
|
name="choices-multiple-remove-button"
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<option value="Choice 1" selected>Choice 1</option>
|
||||||
|
<option value="Choice 2">Choice 2</option>
|
||||||
|
<option value="Choice 3">Choice 3</option>
|
||||||
|
<option value="Choice 4">Choice 4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Multiple Select Input: Option Groups</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code>multiple</code>
|
||||||
|
attribute
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<select
|
||||||
|
choicesSelect
|
||||||
|
class="form-control"
|
||||||
|
name="choices-multiple-groups"
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<option value="">Choose a city</option>
|
||||||
|
<optgroup label="UK">
|
||||||
|
<option value="London">London</option>
|
||||||
|
<option value="Manchester">Manchester</option>
|
||||||
|
<option value="Liverpool">Liverpool</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="FR">
|
||||||
|
<option value="Paris">Paris</option>
|
||||||
|
<option value="Lyon">Lyon</option>
|
||||||
|
<option value="Marseille">Marseille</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="DE" disabled>
|
||||||
|
<option value="Hamburg">Hamburg</option>
|
||||||
|
<option value="Munich">Munich</option>
|
||||||
|
<option value="Berlin">Berlin</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="US">
|
||||||
|
<option value="New York">New York</option>
|
||||||
|
<option value="Washington" disabled>Washington</option>
|
||||||
|
<option value="Michigan">Michigan</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="SP">
|
||||||
|
<option value="Madrid">Madrid</option>
|
||||||
|
<option value="Barcelona">Barcelona</option>
|
||||||
|
<option value="Malaga">Malaga</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="CA">
|
||||||
|
<option value="Montreal">Montreal</option>
|
||||||
|
<option value="Toronto">Toronto</option>
|
||||||
|
<option value="Vancouver">Vancouver</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Text Input: Limit Values with Remove Button</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code
|
||||||
|
>data-choices data-choices-limit="3"
|
||||||
|
data-choices-removeItem</code
|
||||||
|
>
|
||||||
|
attribute.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
choicesSelect
|
||||||
|
[options]="{ removeItemButton: true }"
|
||||||
|
type="text"
|
||||||
|
value="Task-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Text Input: Unique Values Only</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code
|
||||||
|
>[options]="{uniqueItemText:'
|
||||||
|
't;&quo}</code
|
||||||
|
>
|
||||||
|
attribute
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
choicesSelect
|
||||||
|
[options]="{ duplicateItemsAllowed: false }"
|
||||||
|
type="text"
|
||||||
|
value="Project-A, Project-B"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Text Input: Disabled</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set <code>disabled</code> attribute.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
choicesSelect
|
||||||
|
id="choices-text-disabled"
|
||||||
|
disabled
|
||||||
|
type="text"
|
||||||
|
value="josh@joshuajohnson.co.uk, joe@bloggs.co.uk"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-ui-card>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class Choicesjs {}
|
||||||
196
src/app/modules/components/custom-styles.ts
Normal file
196
src/app/modules/components/custom-styles.ts
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import { Component, inject, OnInit } from '@angular/core'
|
||||||
|
import {
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
UntypedFormBuilder,
|
||||||
|
UntypedFormGroup,
|
||||||
|
Validators,
|
||||||
|
} from '@angular/forms'
|
||||||
|
import { UiCard } from '@app/components/ui-card'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-custom-styles',
|
||||||
|
imports: [UiCard, NgIcon, FormsModule, ReactiveFormsModule],
|
||||||
|
template: `
|
||||||
|
<app-ui-card title="Custom styles Validation">
|
||||||
|
<a
|
||||||
|
helper-text
|
||||||
|
href="https://getbootstrap.com/docs/5.3/forms/validation/#custom-styles"
|
||||||
|
target="_blank"
|
||||||
|
class="icon-link icon-link-hover link-secondary link-underline-secondarlink-secondary link-underline-opacity-25 fw-semibold"
|
||||||
|
>View Docs
|
||||||
|
<ng-icon name="tablerArrowRight" class="bi fs-lg" />
|
||||||
|
</a>
|
||||||
|
<form
|
||||||
|
card-body
|
||||||
|
class="row g-3 needs-validation"
|
||||||
|
novalidate
|
||||||
|
(ngSubmit)="validSubmit()"
|
||||||
|
[formGroup]="validationform"
|
||||||
|
>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="validationCustom01" class="form-label">First Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="validationCustom01"
|
||||||
|
[value]="'John'"
|
||||||
|
required
|
||||||
|
formControlName="firstName"
|
||||||
|
[class]="submit && !form['firstName'].errors ? 'is-valid' : ''"
|
||||||
|
/>
|
||||||
|
<div class="valid-feedback">Looks good!</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="validationCustom02" class="form-label">Last Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="validationCustom02"
|
||||||
|
[value]="'Doe'"
|
||||||
|
formControlName="lastName"
|
||||||
|
[class]="submit && !form['lastName'].errors ? 'is-valid' : ''"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="valid-feedback">Looks good!</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="validationCustomUsername" class="form-label"
|
||||||
|
>Username</label
|
||||||
|
>
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<span class="input-group-text" id="inputGroupPrepend">@</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="johndoe123"
|
||||||
|
id="validationCustomUsername"
|
||||||
|
formControlName="username"
|
||||||
|
[class]="
|
||||||
|
submit && !form['username'].errors
|
||||||
|
? 'is-valid'
|
||||||
|
: submit && form['username'].errors
|
||||||
|
? 'is-invalid'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
aria-describedby="inputGroupPrepend"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="invalid-feedback">Please choose a username.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="validationCustom03" class="form-label">City</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="validationCustom03"
|
||||||
|
formControlName="city"
|
||||||
|
placeholder="San Francisco"
|
||||||
|
[class]="
|
||||||
|
submit && !form['city'].errors
|
||||||
|
? 'is-valid'
|
||||||
|
: submit && form['city'].errors
|
||||||
|
? 'is-invalid'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="invalid-feedback">Please provide a valid city.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="validationCustom04" class="form-label">State</label>
|
||||||
|
<select
|
||||||
|
class="form-select"
|
||||||
|
id="validationCustom04"
|
||||||
|
formControlName="state"
|
||||||
|
[class]="
|
||||||
|
submit && !form['state'].errors
|
||||||
|
? 'is-valid'
|
||||||
|
: submit && form['state'].errors
|
||||||
|
? 'is-invalid'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option selected disabled value="">Choose...</option>
|
||||||
|
<option>...</option>
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback">Please select a valid state.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="validationCustom05" class="form-label">Zip Code</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="validationCustom05"
|
||||||
|
placeholder="94107"
|
||||||
|
required
|
||||||
|
formControlName="zip"
|
||||||
|
[class]="
|
||||||
|
submit && !form['zip'].errors
|
||||||
|
? 'is-valid'
|
||||||
|
: submit && form['zip'].errors
|
||||||
|
? 'is-invalid'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div class="invalid-feedback">Please provide a valid zip.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
value=""
|
||||||
|
id="invalidCheck"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="invalidCheck">
|
||||||
|
i agree to the terms and conditions
|
||||||
|
</label>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
You must agree before submitting.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<button class="btn btn-primary" type="submit">Submit Form</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</app-ui-card>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class CustomStyles implements OnInit {
|
||||||
|
public formBuilder = inject(UntypedFormBuilder)
|
||||||
|
validationform!: UntypedFormGroup
|
||||||
|
submit!: boolean
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.validationform = this.formBuilder.group({
|
||||||
|
firstName: [
|
||||||
|
'john',
|
||||||
|
[Validators.required, Validators.pattern('[a-zA-Z0-9]+')],
|
||||||
|
],
|
||||||
|
lastName: [
|
||||||
|
'Doe',
|
||||||
|
[Validators.required, Validators.pattern('[a-zA-Z0-9]+')],
|
||||||
|
],
|
||||||
|
username: ['', [Validators.required, Validators.pattern('[a-zA-Z0-9]+')]],
|
||||||
|
city: ['', [Validators.required, Validators.pattern('[a-zA-Z0-9]+')]],
|
||||||
|
state: ['', [Validators.required, Validators.pattern('[a-zA-Z0-9]+')]],
|
||||||
|
zip: ['', [Validators.required, Validators.pattern('[a-zA-Z0-9]+')]],
|
||||||
|
agree: ['', [Validators.required]],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get form() {
|
||||||
|
return this.validationform.controls
|
||||||
|
}
|
||||||
|
|
||||||
|
validSubmit() {
|
||||||
|
this.submit = true
|
||||||
|
}
|
||||||
|
}
|
||||||
246
src/app/modules/components/data.ts
Normal file
246
src/app/modules/components/data.ts
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
export const states = [
|
||||||
|
'Alabama',
|
||||||
|
'Alaska',
|
||||||
|
'American Samoa',
|
||||||
|
'Arizona',
|
||||||
|
'Arkansas',
|
||||||
|
'California',
|
||||||
|
'Colorado',
|
||||||
|
'Connecticut',
|
||||||
|
'Delaware',
|
||||||
|
'District Of Columbia',
|
||||||
|
'Federated States Of Micronesia',
|
||||||
|
'Florida',
|
||||||
|
'Georgia',
|
||||||
|
'Guam',
|
||||||
|
'Hawaii',
|
||||||
|
'Idaho',
|
||||||
|
'Illinois',
|
||||||
|
'Indiana',
|
||||||
|
'Iowa',
|
||||||
|
'Kansas',
|
||||||
|
'Kentucky',
|
||||||
|
'Louisiana',
|
||||||
|
'Maine',
|
||||||
|
'Marshall Islands',
|
||||||
|
'Maryland',
|
||||||
|
'Massachusetts',
|
||||||
|
'Michigan',
|
||||||
|
'Minnesota',
|
||||||
|
'Mississippi',
|
||||||
|
'Missouri',
|
||||||
|
'Montana',
|
||||||
|
'Nebraska',
|
||||||
|
'Nevada',
|
||||||
|
'New Hampshire',
|
||||||
|
'New Jersey',
|
||||||
|
'New Mexico',
|
||||||
|
'New York',
|
||||||
|
'North Carolina',
|
||||||
|
'North Dakota',
|
||||||
|
'Northern Mariana Islands',
|
||||||
|
'Ohio',
|
||||||
|
'Oklahoma',
|
||||||
|
'Oregon',
|
||||||
|
'Palau',
|
||||||
|
'Pennsylvania',
|
||||||
|
'Puerto Rico',
|
||||||
|
'Rhode Island',
|
||||||
|
'South Carolina',
|
||||||
|
'South Dakota',
|
||||||
|
'Tennessee',
|
||||||
|
'Texas',
|
||||||
|
'Utah',
|
||||||
|
'Vermont',
|
||||||
|
'Virgin Islands',
|
||||||
|
'Virginia',
|
||||||
|
'Washington',
|
||||||
|
'West Virginia',
|
||||||
|
'Wisconsin',
|
||||||
|
'Wyoming',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const statesWithFlags: { name: string; flag: string }[] = [
|
||||||
|
{
|
||||||
|
name: 'Alabama',
|
||||||
|
flag: '5/5c/Flag_of_Alabama.svg/45px-Flag_of_Alabama.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Alaska',
|
||||||
|
flag: 'e/e6/Flag_of_Alaska.svg/43px-Flag_of_Alaska.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Arizona',
|
||||||
|
flag: '9/9d/Flag_of_Arizona.svg/45px-Flag_of_Arizona.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Arkansas',
|
||||||
|
flag: '9/9d/Flag_of_Arkansas.svg/45px-Flag_of_Arkansas.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'California',
|
||||||
|
flag: '0/01/Flag_of_California.svg/45px-Flag_of_California.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Colorado',
|
||||||
|
flag: '4/46/Flag_of_Colorado.svg/45px-Flag_of_Colorado.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Connecticut',
|
||||||
|
flag: '9/96/Flag_of_Connecticut.svg/39px-Flag_of_Connecticut.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delaware',
|
||||||
|
flag: 'c/c6/Flag_of_Delaware.svg/45px-Flag_of_Delaware.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Florida',
|
||||||
|
flag: 'f/f7/Flag_of_Florida.svg/45px-Flag_of_Florida.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Georgia',
|
||||||
|
flag: '5/54/Flag_of_Georgia_%28U.S._state%29.svg/46px-Flag_of_Georgia_%28U.S._state%29.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Hawaii',
|
||||||
|
flag: 'e/ef/Flag_of_Hawaii.svg/46px-Flag_of_Hawaii.svg.png',
|
||||||
|
},
|
||||||
|
{ name: 'Idaho', flag: 'a/a4/Flag_of_Idaho.svg/38px-Flag_of_Idaho.svg.png' },
|
||||||
|
{
|
||||||
|
name: 'Illinois',
|
||||||
|
flag: '0/01/Flag_of_Illinois.svg/46px-Flag_of_Illinois.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Indiana',
|
||||||
|
flag: 'a/ac/Flag_of_Indiana.svg/45px-Flag_of_Indiana.svg.png',
|
||||||
|
},
|
||||||
|
{ name: 'Iowa', flag: 'a/aa/Flag_of_Iowa.svg/44px-Flag_of_Iowa.svg.png' },
|
||||||
|
{
|
||||||
|
name: 'Kansas',
|
||||||
|
flag: 'd/da/Flag_of_Kansas.svg/46px-Flag_of_Kansas.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Kentucky',
|
||||||
|
flag: '8/8d/Flag_of_Kentucky.svg/46px-Flag_of_Kentucky.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Louisiana',
|
||||||
|
flag: 'e/e0/Flag_of_Louisiana.svg/46px-Flag_of_Louisiana.svg.png',
|
||||||
|
},
|
||||||
|
{ name: 'Maine', flag: '3/35/Flag_of_Maine.svg/45px-Flag_of_Maine.svg.png' },
|
||||||
|
{
|
||||||
|
name: 'Maryland',
|
||||||
|
flag: 'a/a0/Flag_of_Maryland.svg/45px-Flag_of_Maryland.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Massachusetts',
|
||||||
|
flag: 'f/f2/Flag_of_Massachusetts.svg/46px-Flag_of_Massachusetts.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Michigan',
|
||||||
|
flag: 'b/b5/Flag_of_Michigan.svg/45px-Flag_of_Michigan.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Minnesota',
|
||||||
|
flag: 'b/b9/Flag_of_Minnesota.svg/46px-Flag_of_Minnesota.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Mississippi',
|
||||||
|
flag: '4/42/Flag_of_Mississippi.svg/45px-Flag_of_Mississippi.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Missouri',
|
||||||
|
flag: '5/5a/Flag_of_Missouri.svg/46px-Flag_of_Missouri.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Montana',
|
||||||
|
flag: 'c/cb/Flag_of_Montana.svg/45px-Flag_of_Montana.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Nebraska',
|
||||||
|
flag: '4/4d/Flag_of_Nebraska.svg/46px-Flag_of_Nebraska.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Nevada',
|
||||||
|
flag: 'f/f1/Flag_of_Nevada.svg/45px-Flag_of_Nevada.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'New Hampshire',
|
||||||
|
flag: '2/28/Flag_of_New_Hampshire.svg/45px-Flag_of_New_Hampshire.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'New Jersey',
|
||||||
|
flag: '9/92/Flag_of_New_Jersey.svg/45px-Flag_of_New_Jersey.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'New Mexico',
|
||||||
|
flag: 'c/c3/Flag_of_New_Mexico.svg/45px-Flag_of_New_Mexico.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'New York',
|
||||||
|
flag: '1/1a/Flag_of_New_York.svg/46px-Flag_of_New_York.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'North Carolina',
|
||||||
|
flag: 'b/bb/Flag_of_North_Carolina.svg/45px-Flag_of_North_Carolina.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'North Dakota',
|
||||||
|
flag: 'e/ee/Flag_of_North_Dakota.svg/38px-Flag_of_North_Dakota.svg.png',
|
||||||
|
},
|
||||||
|
{ name: 'Ohio', flag: '4/4c/Flag_of_Ohio.svg/46px-Flag_of_Ohio.svg.png' },
|
||||||
|
{
|
||||||
|
name: 'Oklahoma',
|
||||||
|
flag: '6/6e/Flag_of_Oklahoma.svg/45px-Flag_of_Oklahoma.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Oregon',
|
||||||
|
flag: 'b/b9/Flag_of_Oregon.svg/46px-Flag_of_Oregon.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pennsylvania',
|
||||||
|
flag: 'f/f7/Flag_of_Pennsylvania.svg/45px-Flag_of_Pennsylvania.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rhode Island',
|
||||||
|
flag: 'f/f3/Flag_of_Rhode_Island.svg/32px-Flag_of_Rhode_Island.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'South Carolina',
|
||||||
|
flag: '6/69/Flag_of_South_Carolina.svg/45px-Flag_of_South_Carolina.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'South Dakota',
|
||||||
|
flag: '1/1a/Flag_of_South_Dakota.svg/46px-Flag_of_South_Dakota.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Tennessee',
|
||||||
|
flag: '9/9e/Flag_of_Tennessee.svg/46px-Flag_of_Tennessee.svg.png',
|
||||||
|
},
|
||||||
|
{ name: 'Texas', flag: 'f/f7/Flag_of_Texas.svg/45px-Flag_of_Texas.svg.png' },
|
||||||
|
{ name: 'Utah', flag: 'f/f6/Flag_of_Utah.svg/45px-Flag_of_Utah.svg.png' },
|
||||||
|
{
|
||||||
|
name: 'Vermont',
|
||||||
|
flag: '4/49/Flag_of_Vermont.svg/46px-Flag_of_Vermont.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Virginia',
|
||||||
|
flag: '4/47/Flag_of_Virginia.svg/44px-Flag_of_Virginia.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Washington',
|
||||||
|
flag: '5/54/Flag_of_Washington.svg/46px-Flag_of_Washington.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'West Virginia',
|
||||||
|
flag: '2/22/Flag_of_West_Virginia.svg/46px-Flag_of_West_Virginia.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Wisconsin',
|
||||||
|
flag: '2/22/Flag_of_Wisconsin.svg/45px-Flag_of_Wisconsin.svg.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Wyoming',
|
||||||
|
flag: 'b/bc/Flag_of_Wyoming.svg/43px-Flag_of_Wyoming.svg.png',
|
||||||
|
},
|
||||||
|
]
|
||||||
375
src/app/modules/components/flatpickr.ts
Normal file
375
src/app/modules/components/flatpickr.ts
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { UiCard } from '@app/components/ui-card'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
|
import {
|
||||||
|
FlatpickrDirective,
|
||||||
|
provideFlatpickrDefaults,
|
||||||
|
} from 'angularx-flatpickr'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-flatpickr',
|
||||||
|
providers: [provideFlatpickrDefaults()],
|
||||||
|
imports: [UiCard, NgIcon, FormsModule, FlatpickrDirective],
|
||||||
|
template: `
|
||||||
|
<app-ui-card title="Flatpickr" bodyClass="p-0">
|
||||||
|
<div card-body>
|
||||||
|
<div class="card-body rounded-bottom-0">
|
||||||
|
<p class="text-muted mb-2">
|
||||||
|
Lightweight, powerful javascript datetimepicker with no dependencies
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="btn btn-link shadow-none p-0 fw-medium"
|
||||||
|
href="https://flatpickr.js.org/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
View Official Website
|
||||||
|
<ng-icon name="tablerChevronRight" class="ms-1" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body border-top-0 rounded-0">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Basic</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code> [options]="dateFormat: 'd M, Y'"</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
mwlFlatpickr
|
||||||
|
[options]="{ dateFormat: 'd M, Y' }"
|
||||||
|
id="basic-datepicker"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="basicDate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>DateTime</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code>
|
||||||
|
[options]="dateFormat: 'd M, Y
|
||||||
|
H:i;,enableTime:true"</code
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
mwlFlatpickr
|
||||||
|
[(ngModel)]="dateTime"
|
||||||
|
[options]="{ dateFormat: 'd M, Y H:i', enableTime: true }"
|
||||||
|
class="form-control"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Human-Friendly Dates</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code> [options]="dateFormat: 'F d, Y'"</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control flatpickr-input"
|
||||||
|
mwlFlatpickr
|
||||||
|
[options]="{ dateFormat: 'F d, Y' }"
|
||||||
|
[(ngModel)]="humanFriendlyDate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>MinDate and MaxDate</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code>
|
||||||
|
[options]="minDate:
|
||||||
|
'...',maxDate:'...'"</code
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
mwlFlatpickr
|
||||||
|
class="form-control"
|
||||||
|
[options]="{
|
||||||
|
altInput: true,
|
||||||
|
minDate: '25-12-2021',
|
||||||
|
maxDate: '29-12-2021',
|
||||||
|
}"
|
||||||
|
placeholder="Select Date"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Disabling Dates</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code>
|
||||||
|
[options]="dateFormat: 'd M,
|
||||||
|
Y',disable:'[...]'"</code
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
mwlFlatpickr
|
||||||
|
[(ngModel)]="disableDate"
|
||||||
|
[options]="{ dateFormat: 'd M, Y', disable: ['14 5,2025'] }"
|
||||||
|
class="form-control"
|
||||||
|
data-provider="flatpickr"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Selecting Multiple Dates</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code>
|
||||||
|
[options]="{dateFormat: 'd M,
|
||||||
|
Y',mode:'multiple'}"</code
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="multipleDate"
|
||||||
|
mwlFlatpickr
|
||||||
|
class="form-control"
|
||||||
|
[options]="{ dateFormat: 'd M, Y', mode: 'multiple' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Range</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code>
|
||||||
|
[options]="{dateFormat: 'd M,
|
||||||
|
Y',mode:'range'}"</code
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
mwlFlatpickr
|
||||||
|
[options]="{ dateFormat: 'd M, Y', mode: 'range' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Inline</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code>
|
||||||
|
[options]="{dateFormat: 'd M,
|
||||||
|
Y',inline:true}"</code
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
mwlFlatpickr
|
||||||
|
[options]="{ dateFormat: 'd M, Y', inline: true }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body rounded-0 border-top-0">
|
||||||
|
<h4 class="card-title fs-sm fw-bold mb-4">Timepicker</h4>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Timepicker</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code>
|
||||||
|
[options]="{noCalendar:
|
||||||
|
true,enableTime:true}"</code
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
mwlFlatpickr
|
||||||
|
[options]="{
|
||||||
|
noCalendar: true,
|
||||||
|
dateFormat: 'H:i',
|
||||||
|
defaultHour: 1.4,
|
||||||
|
enableTime: true,
|
||||||
|
}"
|
||||||
|
placeholder="Select Time"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>24-hour Time Picker</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code>
|
||||||
|
[options]="{noCalendar:
|
||||||
|
true,enableTime:true,time24hr:true}"</code
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
mwlFlatpickr
|
||||||
|
[options]="{
|
||||||
|
noCalendar: true,
|
||||||
|
dateFormat: 'H:i',
|
||||||
|
defaultHour: 1.4,
|
||||||
|
time24hr: true,
|
||||||
|
enableTime: true,
|
||||||
|
mode: 'multiple',
|
||||||
|
}"
|
||||||
|
placeholder="Select Time"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Time Picker w/ Limits</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code>
|
||||||
|
[options]="{noCalendar:
|
||||||
|
true,enableTime:true,maxTime:'...',minTime:'...'}"</code
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
mwlFlatpickr
|
||||||
|
[options]="{
|
||||||
|
noCalendar: true,
|
||||||
|
dateFormat: 'H:i',
|
||||||
|
defaultHour: 1.4,
|
||||||
|
enableTime: true,
|
||||||
|
maxTime: '16:00',
|
||||||
|
minTime: '13:00',
|
||||||
|
}"
|
||||||
|
placeholder="Select Time"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Preloading Time</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code>
|
||||||
|
[options]="{noCalendar:
|
||||||
|
true,enableTime:true,dateFormat:'H:i'}"</code
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
mwlFlatpickr
|
||||||
|
[options]="{
|
||||||
|
noCalendar: true,
|
||||||
|
dateFormat: 'H:i',
|
||||||
|
time24hr: false,
|
||||||
|
enableTime: true,
|
||||||
|
mode: 'multiple',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5>Inline</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Set
|
||||||
|
<code>
|
||||||
|
[options]="{noCalendar:
|
||||||
|
true,enableTime:true,inline:true}"</code
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div
|
||||||
|
mwlFlatpickr
|
||||||
|
[options]="{ noCalendar: true, enableTime: true, inline: true }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-ui-card>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class Flatpickr {
|
||||||
|
basicDate = '20 Jun, 2025'
|
||||||
|
disableDate = '20 Jun, 2025'
|
||||||
|
multipleDate = '20 Jun, 2025'
|
||||||
|
dateTime = '"20 Jun, 2025 14:25'
|
||||||
|
humanFriendlyDate = 'Jun 20, 2025'
|
||||||
|
}
|
||||||
100
src/app/modules/components/floating-labels.ts
Normal file
100
src/app/modules/components/floating-labels.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { UiCard } from '@app/components/ui-card'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-floating-labels',
|
||||||
|
imports: [UiCard],
|
||||||
|
template: `
|
||||||
|
<app-ui-card title="Floating Labels">
|
||||||
|
<div class="row" card-body>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Email address</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
id="floatingInputEmail"
|
||||||
|
placeholder="name@example.com"
|
||||||
|
/>
|
||||||
|
<label for="floatingInputEmail">Email address</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="floatingTextarea" class="col-form-label"
|
||||||
|
>Comments</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="form-floating">
|
||||||
|
<textarea
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Leave a comment here"
|
||||||
|
id="floatingTextarea"
|
||||||
|
style="height: 100px"
|
||||||
|
></textarea>
|
||||||
|
<label for="floatingTextarea">Comments</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="floatingPassword" class="col-form-label"
|
||||||
|
>Password</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="floatingPassword"
|
||||||
|
placeholder="Password"
|
||||||
|
/>
|
||||||
|
<label for="floatingPassword">Password</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="floatingSelect" class="col-form-label"
|
||||||
|
>Select Menu</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="form-floating">
|
||||||
|
<select
|
||||||
|
class="form-select"
|
||||||
|
id="floatingSelect"
|
||||||
|
aria-label="Floating label select example"
|
||||||
|
>
|
||||||
|
<option selected>Open this select menu</option>
|
||||||
|
<option value="1">One</option>
|
||||||
|
<option value="2">Two</option>
|
||||||
|
<option value="3">Three</option>
|
||||||
|
</select>
|
||||||
|
<label for="floatingSelect">Works with selects</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-ui-card>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class FloatingLabels {}
|
||||||
317
src/app/modules/components/input-fields.ts
Normal file
317
src/app/modules/components/input-fields.ts
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { UiCard } from '@app/components/ui-card'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-input-fields',
|
||||||
|
imports: [UiCard, NgIcon],
|
||||||
|
template: `
|
||||||
|
<app-ui-card title="Input Textfield Type">
|
||||||
|
<div class="row" card-body>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="simpleinput" class="col-form-label"
|
||||||
|
>Simple Input</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<input type="text" id="simpleinput" class="form-control" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label" for="floatingInput"
|
||||||
|
>Floating Input</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="floatingInput"
|
||||||
|
placeholder="name"
|
||||||
|
/>
|
||||||
|
<label>Name</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="validInput" class="col-form-label">Valid Input</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="validInput"
|
||||||
|
class="form-control is-valid"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="example-rounded" class="col-form-label"
|
||||||
|
>Rounded Input</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="example-rounded"
|
||||||
|
class="form-control rounded-pill"
|
||||||
|
placeholder="Rounded Input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="example-textarea" class="col-form-label"
|
||||||
|
>Text area</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<textarea
|
||||||
|
class="form-control"
|
||||||
|
id="example-textarea"
|
||||||
|
rows="5"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="example-disable" class="col-form-label"
|
||||||
|
>Disabled</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="example-disable"
|
||||||
|
disabled
|
||||||
|
value="Disabled value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="example-helping" class="col-form-label"
|
||||||
|
>Helping text</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="example-helping"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Helping text"
|
||||||
|
/>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
A block of help text that breaks onto a new line and may extend
|
||||||
|
beyond one line.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Select with Icon</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="app-search">
|
||||||
|
<select class="form-select form-control" id="discount">
|
||||||
|
<option selected>Choose Discount</option>
|
||||||
|
<option value="No Discount">No Discount</option>
|
||||||
|
<option value="Flat Discount">Flat Discount</option>
|
||||||
|
<option value="Percentage Discount">
|
||||||
|
Percentage Discount
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<ng-icon
|
||||||
|
name="lucidePercent"
|
||||||
|
class="app-search-icon text-muted"
|
||||||
|
></ng-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Label Input</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div>
|
||||||
|
<label for="labelInputInput1" class="form-label"
|
||||||
|
>Label Input</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
id="labelInputInput1"
|
||||||
|
placeholder="name@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="SearchInput" class="col-form-label"
|
||||||
|
>Search Style</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="app-search">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
class="form-control"
|
||||||
|
id="SearchInput"
|
||||||
|
placeholder="Search for something..."
|
||||||
|
/>
|
||||||
|
<ng-icon
|
||||||
|
name="lucideSearch"
|
||||||
|
class="app-search-icon text-muted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="inValidationInput" class="col-form-label"
|
||||||
|
>Invalid Input</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="inValidationInput"
|
||||||
|
class="form-control is-invalid"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="example-placeholder" class="col-form-label"
|
||||||
|
>Placeholder</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="example-placeholder"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="placeholder"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="example-readonly" class="col-form-label"
|
||||||
|
>Readonly</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="example-readonly"
|
||||||
|
class="form-control"
|
||||||
|
readonly
|
||||||
|
value="Readonly value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="example-static" class="col-form-label"
|
||||||
|
>Static control</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
class="form-control-plaintext"
|
||||||
|
id="example-static"
|
||||||
|
value="email@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Default Select</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<select class="form-select">
|
||||||
|
<option selected>Open this select menu</option>
|
||||||
|
<option value="1">One</option>
|
||||||
|
<option value="2">Two</option>
|
||||||
|
<option value="3">Three</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="example-multiselect" class="col-form-label"
|
||||||
|
>Multiple Select</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<select id="example-multiselect" multiple class="form-control">
|
||||||
|
<option>1</option>
|
||||||
|
<option>2</option>
|
||||||
|
<option>3</option>
|
||||||
|
<option>4</option>
|
||||||
|
<option>5</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-ui-card>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class InputFields {}
|
||||||
267
src/app/modules/components/input-groups.ts
Normal file
267
src/app/modules/components/input-groups.ts
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { UiCard } from '@app/components/ui-card'
|
||||||
|
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-input-groups',
|
||||||
|
imports: [UiCard, NgbDropdownModule],
|
||||||
|
template: `
|
||||||
|
<app-ui-card title="Input Group">
|
||||||
|
<div class="row" card-body>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Username</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text" id="basic-addon1">@</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Username"
|
||||||
|
aria-label="Username"
|
||||||
|
aria-describedby="basic-addon1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Amount</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">$</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
aria-label="Amount (to the nearest dollar)"
|
||||||
|
/>
|
||||||
|
<span class="input-group-text">.00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Textarea</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">With textarea</span>
|
||||||
|
<textarea
|
||||||
|
class="form-control"
|
||||||
|
aria-label="With textarea"
|
||||||
|
rows="2"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Wrapping</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="input-group flex-nowrap">
|
||||||
|
<span class="input-group-text" id="addon-wrapping"
|
||||||
|
>@</span
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Username"
|
||||||
|
aria-label="Username"
|
||||||
|
aria-describedby="addon-wrapping"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Input + Button</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Recipient's username"
|
||||||
|
aria-label="Recipient's username"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-dark" type="button">Button</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="formFileMultiple01" class="col-form-label"
|
||||||
|
>Multiple Files</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="file"
|
||||||
|
id="formFileMultiple01"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Recipient</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Recipient's username"
|
||||||
|
aria-label="Recipient's username"
|
||||||
|
aria-describedby="basic-addon2"
|
||||||
|
/>
|
||||||
|
<span class="input-group-text" id="basic-addon2"
|
||||||
|
>@example.com</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Email Login</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Username"
|
||||||
|
/>
|
||||||
|
<span class="input-group-text">@</span>
|
||||||
|
<input type="text" class="form-control" placeholder="Server" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="basic-url" class="col-form-label">Vanity URL</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text" id="basic-addon3"
|
||||||
|
>https://example.com/users/</span
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="basic-url"
|
||||||
|
aria-describedby="basic-addon3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Dropdown + Input</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="input-group" ngbDropdown>
|
||||||
|
<button
|
||||||
|
ngbDropdownToggle
|
||||||
|
class="btn btn-primary"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
Dropdown
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu>
|
||||||
|
<a ngbDropdownItem href="javascript:void(0);">Action</a>
|
||||||
|
<a ngbDropdownItem href="javascript:void(0);"
|
||||||
|
>Another action</a
|
||||||
|
>
|
||||||
|
<a ngbDropdownItem href="javascript:void(0);"
|
||||||
|
>Something else here</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder=""
|
||||||
|
aria-label=""
|
||||||
|
aria-describedby="basic-addon1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="inputGroupFile04" class="col-form-label"
|
||||||
|
>File Input</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<input class="form-control" type="file" id="inputGroupFile04" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="inputGroupSelect01" class="col-form-label"
|
||||||
|
>Input Group Select</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-group-text" for="inputGroupSelect01"
|
||||||
|
>Options</label
|
||||||
|
>
|
||||||
|
<select class="form-select" id="inputGroupSelect01">
|
||||||
|
<option selected>Choose...</option>
|
||||||
|
<option value="1">One</option>
|
||||||
|
<option value="2">Two</option>
|
||||||
|
<option value="3">Three</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-ui-card>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class InputGroups {}
|
||||||
125
src/app/modules/components/input-sizes.ts
Normal file
125
src/app/modules/components/input-sizes.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { UiCard } from '@app/components/ui-card'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-input-sizes',
|
||||||
|
imports: [UiCard],
|
||||||
|
template: `
|
||||||
|
<app-ui-card title="Input Sizes">
|
||||||
|
<div class="row" card-body>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="example-input-small" class="col-form-label"
|
||||||
|
>Small</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="example-input-small"
|
||||||
|
name="example-input-small"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
placeholder=".input-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="example-input-large" class="col-form-label"
|
||||||
|
>Large</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="example-input-large"
|
||||||
|
name="example-input-large"
|
||||||
|
class="form-control form-control-lg"
|
||||||
|
placeholder=".input-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Large Select</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<select class="form-select form-select-lg">
|
||||||
|
<option selected>Open this select menu</option>
|
||||||
|
<option value="1">One</option>
|
||||||
|
<option value="2">Two</option>
|
||||||
|
<option value="3">Three</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="example-input-normal" class="col-form-label"
|
||||||
|
>Normal</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="example-input-normal"
|
||||||
|
name="example-input-normal"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Normal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label for="example-gridsize" class="col-form-label"
|
||||||
|
>Grid Sizes</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="example-gridsize"
|
||||||
|
class="form-control"
|
||||||
|
placeholder=".col-sm-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-dashed my-3"></div>
|
||||||
|
|
||||||
|
<div class="row g-lg-4 g-2">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<label class="col-form-label">Small Select</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<select class="form-select form-select-sm">
|
||||||
|
<option selected>Open this select menu</option>
|
||||||
|
<option value="1">One</option>
|
||||||
|
<option value="2">Two</option>
|
||||||
|
<option value="3">Three</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-ui-card>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class InputSizes {}
|
||||||
484
src/app/modules/components/input-touchspin.ts
Normal file
484
src/app/modules/components/input-touchspin.ts
Normal file
@ -0,0 +1,484 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { UiCard } from '@app/components/ui-card'
|
||||||
|
import { NgIcon } from '@ng-icons/core'
|
||||||
|
import { CounterDirective } from '@core/directive/counter.directive'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-input-touchspin',
|
||||||
|
imports: [UiCard, NgIcon, CounterDirective],
|
||||||
|
template: `
|
||||||
|
<app-ui-card title="Input Touchspin">
|
||||||
|
<span badge-text class="badge badge-soft-success badge-label py-1 fs-xxs"
|
||||||
|
>Exclusive</span
|
||||||
|
>
|
||||||
|
<div card-body>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5 class="fw-semibold mb-1">Default Touchspin</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div
|
||||||
|
class="input-group touchspin"
|
||||||
|
appCounter
|
||||||
|
[(count)]="count"
|
||||||
|
#counter="appCounter"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-light floating"
|
||||||
|
(click)="counter.decrement()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerMinus" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
[value]="count"
|
||||||
|
class="form-control form-control-sm border-0"
|
||||||
|
max="800000"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
(click)="counter.increment()"
|
||||||
|
class="btn btn-light floating"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerPlus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5 class="fw-semibold mb-1">Sizes</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div
|
||||||
|
class="input-group input-group-sm touchspin"
|
||||||
|
appCounter
|
||||||
|
[(count)]="sizeCount"
|
||||||
|
#counter2="appCounter"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-light floating"
|
||||||
|
(click)="counter2.decrement()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerMinus" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
[value]="sizeCount"
|
||||||
|
class="form-control form-control border-0"
|
||||||
|
max="800000"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-light floating"
|
||||||
|
(click)="counter2.increment()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerPlus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="input-group input-group-lg mt-2 touchspin"
|
||||||
|
appCounter
|
||||||
|
[(count)]="sizeCount2"
|
||||||
|
#counter3="appCounter"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-light floating"
|
||||||
|
(click)="counter3.decrement()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerMinus" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
[value]="sizeCount2"
|
||||||
|
class="form-control form-control border-0"
|
||||||
|
max="800000"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-light floating"
|
||||||
|
(click)="counter3.increment()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerPlus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5 class="fw-semibold mb-1">Colors</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
@for (color of colors; track $index; let first = $first) {
|
||||||
|
<div
|
||||||
|
class="input-group touchspin {{ !first ? 'mt-2' : '' }}"
|
||||||
|
appCounter
|
||||||
|
[(count)]="colorCount"
|
||||||
|
#counter4="appCounter"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-{{ color }} floating"
|
||||||
|
(click)="counter4.decrement()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerMinus" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
[value]="colorCount"
|
||||||
|
class="form-control form-control-sm border-0"
|
||||||
|
value="100"
|
||||||
|
max="800000"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-{{ color }} floating"
|
||||||
|
(click)="counter4.increment()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerPlus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5 class="fw-semibold mb-1">Readonly</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="input-group touchspin">
|
||||||
|
<button type="button" class="btn btn-light floating">
|
||||||
|
<ng-icon name="tablerMinus" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control form-control-sm border-0"
|
||||||
|
value="1"
|
||||||
|
max="800000"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
<button type="button" class="btn btn-light floating">
|
||||||
|
<ng-icon name="tablerPlus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5 class="fw-semibold mb-1">Disabled</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="input-group touchspin">
|
||||||
|
<button type="button" class="btn btn-light floating" disabled>
|
||||||
|
<ng-icon name="tablerMinus" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control form-control-sm border-0"
|
||||||
|
value="1"
|
||||||
|
max="800000"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<button type="button" class="btn btn-light floating" disabled>
|
||||||
|
<ng-icon name="tablerPlus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5 class="fw-semibold mb-1">Style</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div
|
||||||
|
class="input-group touchspin"
|
||||||
|
appCounter
|
||||||
|
[(count)]="count5"
|
||||||
|
#counter5="appCounter"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary rounded-circle floating"
|
||||||
|
(click)="counter5.decrement()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerMinus" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control form-control-sm border-0"
|
||||||
|
[value]="count5"
|
||||||
|
min="0"
|
||||||
|
max="800000"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary rounded-circle floating"
|
||||||
|
(click)="counter5.increment()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerPlus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="input-group touchspin rounded-pill mt-2"
|
||||||
|
appCounter
|
||||||
|
[(count)]="count5"
|
||||||
|
#counter5="appCounter"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary rounded-circle floating"
|
||||||
|
(click)="counter5.decrement()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerMinus" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control form-control-sm border-0"
|
||||||
|
[value]="count5"
|
||||||
|
min="0"
|
||||||
|
max="800000"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary rounded-circle floating"
|
||||||
|
(click)="counter5.increment()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerPlus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="input-group touchspin border-0 mt-2"
|
||||||
|
appCounter
|
||||||
|
[(count)]="count6"
|
||||||
|
#counter6="appCounter"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
(click)="counter6.decrement()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerMinus" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control border-secondary"
|
||||||
|
[value]="count6"
|
||||||
|
min="0"
|
||||||
|
max="800000"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
(click)="counter6.increment()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerPlus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="input-group touchspin border-0 mt-2"
|
||||||
|
appCounter
|
||||||
|
[(count)]="count6"
|
||||||
|
#counter6="appCounter"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-soft-success"
|
||||||
|
(click)="counter6.decrement()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerMinus" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control border-success-subtle"
|
||||||
|
[value]="count6"
|
||||||
|
min="0"
|
||||||
|
max="800000"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-soft-success"
|
||||||
|
(click)="counter6.increment()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerPlus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 border-top border-dashed"></div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h5 class="fw-semibold mb-1">Vertical Style</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div
|
||||||
|
class="input-group input-group-sm touchspin"
|
||||||
|
appCounter
|
||||||
|
[max]="10"
|
||||||
|
[(count)]="count7"
|
||||||
|
#counter7="appCounter"
|
||||||
|
>
|
||||||
|
<div class="btn-group-vertical">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-soft-success"
|
||||||
|
(click)="counter7.increment()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerPlus" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-soft-danger"
|
||||||
|
(click)="counter7.decrement()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerMinus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control border-0"
|
||||||
|
[value]="count7"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="input-group mt-2 touchspin"
|
||||||
|
appCounter
|
||||||
|
[max]="10"
|
||||||
|
[(count)]="count7"
|
||||||
|
#counter7="appCounter"
|
||||||
|
>
|
||||||
|
<div class="btn-group-vertical">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-success"
|
||||||
|
(click)="counter7.increment()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerPlus" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-danger"
|
||||||
|
(click)="counter7.decrement()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerMinus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control border-0"
|
||||||
|
[value]="count7"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="input-group input-group-lg mt-2 touchspin"
|
||||||
|
appCounter
|
||||||
|
[max]="10"
|
||||||
|
[(count)]="count7"
|
||||||
|
#counter7="appCounter"
|
||||||
|
>
|
||||||
|
<div class="btn-group-vertical">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-dark"
|
||||||
|
(click)="counter7.increment()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerPlus" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-dark"
|
||||||
|
(click)="counter7.decrement()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerMinus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control border-0"
|
||||||
|
[value]="count7"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="input-group mt-2 touchspin"
|
||||||
|
appCounter
|
||||||
|
[max]="10"
|
||||||
|
[(count)]="count7"
|
||||||
|
#counter7="appCounter"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control border-0"
|
||||||
|
[value]="count7"
|
||||||
|
/>
|
||||||
|
<div class="btn-group-vertical">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-dark"
|
||||||
|
(click)="counter7.increment()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerPlus" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-dark"
|
||||||
|
(click)="counter7.decrement()"
|
||||||
|
>
|
||||||
|
<ng-icon name="tablerMinus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-ui-card>
|
||||||
|
`,
|
||||||
|
styles: ``,
|
||||||
|
})
|
||||||
|
export class InputTouchspin {
|
||||||
|
colors = [
|
||||||
|
'primary',
|
||||||
|
'secondary',
|
||||||
|
'info',
|
||||||
|
'success',
|
||||||
|
'danger',
|
||||||
|
'warning',
|
||||||
|
'primary',
|
||||||
|
'soft-primary',
|
||||||
|
]
|
||||||
|
count: number = 0
|
||||||
|
sizeCount: number = 0
|
||||||
|
sizeCount2: number = 0
|
||||||
|
colorCount: number = 100
|
||||||
|
count5: number = 100
|
||||||
|
count6: number = 100
|
||||||
|
count7: number = 0
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user