feat: add DCB BO admin dashboard - Authentication system with JWT - Modular architecture with services for each feature

This commit is contained in:
diallolatoile 2025-10-24 16:10:19 +00:00
commit ab55d6c59c
709 changed files with 58900 additions and 0 deletions

17
.editorconfig Normal file
View 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
View 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

1
.npmrc Normal file
View File

@ -0,0 +1 @@
legacy-peer-deps=true

10
.prettierrc Normal file
View File

@ -0,0 +1,10 @@
{
"bracketSpacing": true,
"semi": false,
"singleQuote": true,
"trailingComma": "es5",
"useTabs": false,
"tabWidth": 2,
"arrowParens": "always"
}

59
README.md Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

87
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

20
src/app/app.config.ts Normal file
View 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
View File

@ -0,0 +1 @@
<router-outlet />

28
src/app/app.routes.ts Normal file
View 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
View File

25
src/app/app.spec.ts Normal file
View 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
View 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`);
}
});
}
}

View 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 {}

View 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)
}
}

View 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)
}
}

View 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>

View 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()
})
})

View 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: '',
}
}

View 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)
}
}
}

View 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
}
}

View 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()
}
}

View 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 deffectuer 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 = ''

View 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)
}
})
}
}

View 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)
}
}
}

View 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;
};

View 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 cest 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 cest 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')
);
}

View 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`);
}
}

View 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)
}
}

View 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 = ''
}
}
}

View 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>

View 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()
})
})

View 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
}

View 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 dInté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' },
]

View 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>

View 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()
})
})

View 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
}

View File

@ -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>

View File

@ -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()
})
})

View File

@ -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)
}
}
}

View File

@ -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>

View File

@ -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()
})
})

View File

@ -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();
}
});
}
}

View 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>

View 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()
})
})

View 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'
)
}
}

View File

@ -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>

View File

@ -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()
})
})

View File

@ -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) {}
}

View File

@ -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>

View File

@ -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()
})
})

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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()
})
})

View File

@ -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: '#;' },
],
},
]
}

View File

@ -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>

View File

@ -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()
})
})

View File

@ -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',
},
]
}

View File

@ -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>

View File

@ -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()
})
})

View File

@ -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
}
}

View File

@ -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>

View File

@ -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()
})
})

View File

@ -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')
}
}
}

View File

@ -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>

View File

@ -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()
})
})

View File

@ -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
}
}

View 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>

View 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()
})
})

View 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'
)
}
}
}

View 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>

View 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()
})
})

View 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()
}
}

View File

@ -0,0 +1 @@
<p>About</p>

View File

@ -0,0 +1,2 @@
import { About } from './about';
describe('About', () => {});

View File

@ -0,0 +1,7 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-about',
templateUrl: './about.html',
})
export class About {}

View File

@ -0,0 +1,8 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AboutService {
constructor() {}
}

View 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' },
},
]

View 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
}

View 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">
Dont 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
}

View 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
}

View 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);
}
}

View 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">
Lets 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
}

View 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">
Dont 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
}

View 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
}

View 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">&#64;</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 {}

View 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 {}

View 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]=&quot;&lbrace;searchEnabled:false&rbrace;&quot;</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]=&quot;&lbrace;searchEnabled:false&rbrace;&quot;</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]=&quot;&lbrace;searchEnabled:false,removeItemButton:true&rbrace;&quot;</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]=&quot;&lbrace;shouldSortItems:false&rbrace;&quot;</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]=&quot;&lbrace;removeItemButton:true&rbrace;&quot;</code
>
<code>[multiple]=&quot;true&quot;</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]=&quot;&lbrace;uniqueItemText:&apos;
&apos;t;&quo&rbrace;</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 {}

View 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">&#64;</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
}
}

View 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',
},
]

View 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]=&quot;dateFormat: &#39;d M, Y&#39;&quot;</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]=&quot;dateFormat: &#39;d M, Y
H:i;,enableTime:true&quot;</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]=&quot;dateFormat: &#39;F d, Y&#39;&quot;</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]=&quot;minDate:
&#39;...&#39;,maxDate:&#39;...&#39;&quot;</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]=&quot;dateFormat: &#39;d M,
Y&#39;,disable:&#39;[...]&#39;&quot;</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]=&quot;&lbrace;dateFormat: &#39;d M,
Y&#39;,mode:&apos;multiple&apos;&rbrace;&quot;</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]=&quot;&lbrace;dateFormat: &#39;d M,
Y&#39;,mode:&apos;range&apos;&rbrace;&quot;</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]=&quot;&lbrace;dateFormat: &#39;d M,
Y&#39;,inline:true&rbrace;&quot;</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]=&quot;&lbrace;noCalendar:
true,enableTime:true&rbrace;&quot;</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]=&quot;&lbrace;noCalendar:
true,enableTime:true,time24hr:true&rbrace;&quot;</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]=&quot;&lbrace;noCalendar:
true,enableTime:true,maxTime:&apos;...&apos;,minTime:&apos;...&apos;&rbrace;&quot;</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]=&quot;&lbrace;noCalendar:
true,enableTime:true,dateFormat:&apos;H:i&apos;&rbrace;&quot;</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]=&quot;&lbrace;noCalendar:
true,enableTime:true,inline:true&rbrace;&quot;</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'
}

View 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 {}

View 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 {}

View 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">&commat;</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"
>&commat;</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"
>&commat;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">&commat;</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 {}

View 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 {}

View 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