@lukfel/ng-scaffold 20.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/.eslintrc.json +37 -0
  2. package/README.md +382 -0
  3. package/ng-package.json +8 -0
  4. package/package.json +28 -0
  5. package/src/lib/components/bottom-bar/bottom-bar.component.html +29 -0
  6. package/src/lib/components/bottom-bar/bottom-bar.component.scss +33 -0
  7. package/src/lib/components/bottom-bar/bottom-bar.component.spec.ts +24 -0
  8. package/src/lib/components/bottom-bar/bottom-bar.component.ts +31 -0
  9. package/src/lib/components/content-title-card/content-title-card.component.html +25 -0
  10. package/src/lib/components/content-title-card/content-title-card.component.scss +17 -0
  11. package/src/lib/components/content-title-card/content-title-card.component.spec.ts +108 -0
  12. package/src/lib/components/content-title-card/content-title-card.component.ts +24 -0
  13. package/src/lib/components/drawer/drawer.component.html +33 -0
  14. package/src/lib/components/drawer/drawer.component.scss +10 -0
  15. package/src/lib/components/drawer/drawer.component.spec.ts +52 -0
  16. package/src/lib/components/drawer/drawer.component.ts +30 -0
  17. package/src/lib/components/floating-button/floating-button.component.html +32 -0
  18. package/src/lib/components/floating-button/floating-button.component.scss +20 -0
  19. package/src/lib/components/floating-button/floating-button.component.spec.ts +84 -0
  20. package/src/lib/components/floating-button/floating-button.component.ts +57 -0
  21. package/src/lib/components/footer/footer.component.html +38 -0
  22. package/src/lib/components/footer/footer.component.scss +39 -0
  23. package/src/lib/components/footer/footer.component.spec.ts +118 -0
  24. package/src/lib/components/footer/footer.component.ts +14 -0
  25. package/src/lib/components/header/header.component.html +170 -0
  26. package/src/lib/components/header/header.component.scss +102 -0
  27. package/src/lib/components/header/header.component.spec.ts +134 -0
  28. package/src/lib/components/header/header.component.ts +53 -0
  29. package/src/lib/components/loading-overlay/loading-overlay.component.html +3 -0
  30. package/src/lib/components/loading-overlay/loading-overlay.component.scss +16 -0
  31. package/src/lib/components/loading-overlay/loading-overlay.component.spec.ts +24 -0
  32. package/src/lib/components/loading-overlay/loading-overlay.component.ts +10 -0
  33. package/src/lib/components/navbar/navbar.component.html +43 -0
  34. package/src/lib/components/navbar/navbar.component.scss +71 -0
  35. package/src/lib/components/navbar/navbar.component.spec.ts +43 -0
  36. package/src/lib/components/navbar/navbar.component.ts +35 -0
  37. package/src/lib/components/scaffold/scaffold.component.html +74 -0
  38. package/src/lib/components/scaffold/scaffold.component.scss +48 -0
  39. package/src/lib/components/scaffold/scaffold.component.spec.ts +119 -0
  40. package/src/lib/components/scaffold/scaffold.component.ts +191 -0
  41. package/src/lib/interceptors/loading.interceptor.ts +51 -0
  42. package/src/lib/models/bottom-bar-config.model.ts +8 -0
  43. package/src/lib/models/confirm-dialog-config.model.ts +6 -0
  44. package/src/lib/models/content-title-card-config.model.ts +6 -0
  45. package/src/lib/models/drawer-config.model.ts +6 -0
  46. package/src/lib/models/floating-button-config.model.ts +13 -0
  47. package/src/lib/models/footer-config.model.ts +10 -0
  48. package/src/lib/models/header-config.model.ts +26 -0
  49. package/src/lib/models/index.ts +15 -0
  50. package/src/lib/models/library-config.model.ts +4 -0
  51. package/src/lib/models/menu-button.model.ts +10 -0
  52. package/src/lib/models/navbar-config.model.ts +8 -0
  53. package/src/lib/models/navigation-link.model.ts +6 -0
  54. package/src/lib/models/placeholder-config.model.ts +7 -0
  55. package/src/lib/models/scaffold-config.model.ts +21 -0
  56. package/src/lib/models/seo-config.model.ts +6 -0
  57. package/src/lib/scaffold.module.ts +54 -0
  58. package/src/lib/services/breakpoint.service.spec.ts +15 -0
  59. package/src/lib/services/breakpoint.service.ts +16 -0
  60. package/src/lib/services/dialog.service.spec.ts +18 -0
  61. package/src/lib/services/dialog.service.ts +58 -0
  62. package/src/lib/services/index.ts +9 -0
  63. package/src/lib/services/local-storage.service.spec.ts +15 -0
  64. package/src/lib/services/local-storage.service.ts +125 -0
  65. package/src/lib/services/logger.service.spec.ts +15 -0
  66. package/src/lib/services/logger.service.ts +46 -0
  67. package/src/lib/services/router.service.spec.ts +15 -0
  68. package/src/lib/services/router.service.ts +91 -0
  69. package/src/lib/services/scaffold.service.spec.ts +15 -0
  70. package/src/lib/services/scaffold.service.ts +77 -0
  71. package/src/lib/services/seo.service.spec.ts +15 -0
  72. package/src/lib/services/seo.service.ts +75 -0
  73. package/src/lib/services/snackbar.service.spec.ts +18 -0
  74. package/src/lib/services/snackbar.service.ts +38 -0
  75. package/src/lib/services/theme.service.spec.ts +20 -0
  76. package/src/lib/services/theme.service.ts +71 -0
  77. package/src/lib/shared/components/dialogs/confirm-dialog/confirm-dialog.component.html +24 -0
  78. package/src/lib/shared/components/dialogs/confirm-dialog/confirm-dialog.component.scss +0 -0
  79. package/src/lib/shared/components/dialogs/confirm-dialog/confirm-dialog.component.spec.ts +85 -0
  80. package/src/lib/shared/components/dialogs/confirm-dialog/confirm-dialog.component.ts +14 -0
  81. package/src/lib/shared/components/file-upload/file-upload.component.html +21 -0
  82. package/src/lib/shared/components/file-upload/file-upload.component.scss +5 -0
  83. package/src/lib/shared/components/file-upload/file-upload.component.spec.ts +25 -0
  84. package/src/lib/shared/components/file-upload/file-upload.component.ts +43 -0
  85. package/src/lib/shared/components/icon/icon.component.html +17 -0
  86. package/src/lib/shared/components/icon/icon.component.scss +9 -0
  87. package/src/lib/shared/components/icon/icon.component.spec.ts +22 -0
  88. package/src/lib/shared/components/icon/icon.component.ts +17 -0
  89. package/src/lib/shared/components/input/input.component.html +38 -0
  90. package/src/lib/shared/components/input/input.component.scss +31 -0
  91. package/src/lib/shared/components/input/input.component.spec.ts +62 -0
  92. package/src/lib/shared/components/input/input.component.ts +72 -0
  93. package/src/lib/shared/components/placeholder/placeholder.component.html +21 -0
  94. package/src/lib/shared/components/placeholder/placeholder.component.scss +30 -0
  95. package/src/lib/shared/components/placeholder/placeholder.component.spec.ts +24 -0
  96. package/src/lib/shared/components/placeholder/placeholder.component.ts +16 -0
  97. package/src/lib/shared/modules/material.module.ts +77 -0
  98. package/src/lib/shared/shared.module.ts +18 -0
  99. package/src/public-api.ts +16 -0
  100. package/styles/_classes.scss +34 -0
  101. package/styles/_theme.scss +97 -0
  102. package/styles/_variables.scss +50 -0
  103. package/styles/fonts/icons/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2 +0 -0
  104. package/styles/fonts/icons/gok-H7zzDkdnRel8-DQ6KAXJ69wP1tGnf4ZGhUce.woff2 +0 -0
  105. package/styles/fonts/material-icons.scss +48 -0
  106. package/styles/fonts/roboto/roboto-v30-latin-100.woff +0 -0
  107. package/styles/fonts/roboto/roboto-v30-latin-100.woff2 +0 -0
  108. package/styles/fonts/roboto/roboto-v30-latin-100italic.woff +0 -0
  109. package/styles/fonts/roboto/roboto-v30-latin-100italic.woff2 +0 -0
  110. package/styles/fonts/roboto/roboto-v30-latin-300.woff +0 -0
  111. package/styles/fonts/roboto/roboto-v30-latin-300.woff2 +0 -0
  112. package/styles/fonts/roboto/roboto-v30-latin-300italic.woff +0 -0
  113. package/styles/fonts/roboto/roboto-v30-latin-300italic.woff2 +0 -0
  114. package/styles/fonts/roboto/roboto-v30-latin-500.woff +0 -0
  115. package/styles/fonts/roboto/roboto-v30-latin-500.woff2 +0 -0
  116. package/styles/fonts/roboto/roboto-v30-latin-500italic.woff +0 -0
  117. package/styles/fonts/roboto/roboto-v30-latin-500italic.woff2 +0 -0
  118. package/styles/fonts/roboto/roboto-v30-latin-700.woff +0 -0
  119. package/styles/fonts/roboto/roboto-v30-latin-700.woff2 +0 -0
  120. package/styles/fonts/roboto/roboto-v30-latin-700italic.woff +0 -0
  121. package/styles/fonts/roboto/roboto-v30-latin-700italic.woff2 +0 -0
  122. package/styles/fonts/roboto/roboto-v30-latin-italic.woff +0 -0
  123. package/styles/fonts/roboto/roboto-v30-latin-italic.woff2 +0 -0
  124. package/styles/fonts/roboto/roboto-v30-latin-regular.woff +0 -0
  125. package/styles/fonts/roboto/roboto-v30-latin-regular.woff2 +0 -0
  126. package/styles/fonts/roboto-font.scss +109 -0
  127. package/styles/style.scss +54 -0
  128. package/tsconfig.lib.json +14 -0
  129. package/tsconfig.lib.prod.json +10 -0
  130. package/tsconfig.spec.json +14 -0
@@ -0,0 +1,75 @@
1
+
2
+ import { DOCUMENT, inject, Injectable } from '@angular/core';
3
+ import { Meta, Title } from '@angular/platform-browser';
4
+ import { SeoConfig } from '../models';
5
+ import { Logger } from './logger.service';
6
+
7
+ @Injectable({
8
+ providedIn: 'root'
9
+ })
10
+ export class SeoService {
11
+ private metaTitle = inject(Title);
12
+ private metaTags = inject(Meta);
13
+ private document = inject<Document>(DOCUMENT);
14
+ private logger = inject(Logger);
15
+
16
+
17
+ /**
18
+ * Pass a configuration to set meta tags such as title, description and image for search results and social media
19
+ *
20
+ * @param seoConfig config that contains all the meta information
21
+ *
22
+ */
23
+ public setMetaTags(seoConfig: SeoConfig): void {
24
+ const autoTrim: boolean = seoConfig.autoTrim || false;
25
+ const title: string = seoConfig.metaPageTitle || '';
26
+ const description: string = seoConfig.metaPageDescription || '';
27
+ const imagePath: string = seoConfig.metaImagePath || '';
28
+ const titleLimit: number = 60;
29
+ const descriptionLimit: number = 160;
30
+
31
+ // Set meta title
32
+ if (title) {
33
+ if (autoTrim && title.length > titleLimit) {
34
+ this.logger.error(`[SeoService] The set meta title is too long. Recommended length is ${titleLimit}. The title will be trimmed.`);
35
+ }
36
+ const titleTrim: string = (title.length > titleLimit) ? title.substring(0, titleLimit - 3) + '...' : title;
37
+ this._setMetaTitle(autoTrim ? titleTrim : title);
38
+ }
39
+
40
+ // Set meta description
41
+ if (description) {
42
+ if (autoTrim && description.length > descriptionLimit) {
43
+ this.logger.error(`[SeoService] The set meta description is too long. Recommended length is ${descriptionLimit}. The description will be trimmed.`);
44
+ }
45
+ const descriptionTrim: string = (description.length > descriptionLimit) ? description.substring(0, descriptionLimit - 3) + '...' : description;
46
+ this._setMetaDescription(autoTrim ? descriptionTrim : description);
47
+ }
48
+
49
+ // Set meta image
50
+ const host: string = this.document.location.origin;
51
+ if (imagePath) {
52
+ this._setMetaImage(`${host}/${imagePath}`);
53
+ }
54
+ }
55
+
56
+ // Set all meta titles
57
+ private _setMetaTitle(title: string): void {
58
+ this.metaTitle.setTitle(title);
59
+ this.metaTags.updateTag({ property: 'og:title', content: title });
60
+ this.metaTags.updateTag({ name: 'twitter:title', content: title });
61
+ }
62
+
63
+ // Set all meta descriptions
64
+ private _setMetaDescription(description: string): void {
65
+ this.metaTags.updateTag({ name: 'description', content: description });
66
+ this.metaTags.updateTag({ property: 'og:description', content: description });
67
+ this.metaTags.updateTag({ name: 'twitter:description', content: description });
68
+ }
69
+
70
+ // Set all meta images
71
+ private _setMetaImage(image: string): void {
72
+ this.metaTags.updateTag({ property: 'og:image', content: image });
73
+ this.metaTags.updateTag({ name: 'twitter:image', content: image });
74
+ }
75
+ }
@@ -0,0 +1,18 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { SharedModule } from '../shared/shared.module';
3
+ import { SnackbarService } from './snackbar.service';
4
+
5
+ describe('SnackbarService', () => {
6
+ let service: SnackbarService;
7
+
8
+ beforeEach(() => {
9
+ TestBed.configureTestingModule({
10
+ imports: [SharedModule]
11
+ });
12
+ service = TestBed.inject(SnackbarService);
13
+ });
14
+
15
+ it('should be created', () => {
16
+ expect(service).toBeTruthy();
17
+ });
18
+ });
@@ -0,0 +1,38 @@
1
+ import { Injectable, inject } from '@angular/core';
2
+ import { MatSnackBar, MatSnackBarConfig, MatSnackBarHorizontalPosition, MatSnackBarVerticalPosition } from '@angular/material/snack-bar';
3
+ import { firstValueFrom } from 'rxjs';
4
+
5
+ @Injectable({
6
+ providedIn: 'root'
7
+ })
8
+ export class SnackbarService {
9
+ private snackbar = inject(MatSnackBar);
10
+
11
+
12
+ private readonly SNACKBAR_DURATION: number = 5000;
13
+ private readonly SNACKBAR_POSITION_HORIZONTAL: MatSnackBarHorizontalPosition = 'center';
14
+ private readonly SNACKBAR_POSITION_VERTICAL: MatSnackBarVerticalPosition = 'bottom';
15
+
16
+ private readonly actionConfig: MatSnackBarConfig = {
17
+ horizontalPosition: this.SNACKBAR_POSITION_HORIZONTAL,
18
+ verticalPosition: this.SNACKBAR_POSITION_VERTICAL
19
+ };
20
+
21
+ private readonly defaultConfig: MatSnackBarConfig = {
22
+ duration: this.SNACKBAR_DURATION,
23
+ horizontalPosition: this.SNACKBAR_POSITION_HORIZONTAL,
24
+ verticalPosition: this.SNACKBAR_POSITION_VERTICAL
25
+ };
26
+
27
+ // Opens a snackbar with an action to wait for
28
+ public openSnackbarWithAction(message: string, action: string, config?: MatSnackBarConfig): Promise<void> {
29
+ const snackbarRef = this.snackbar.open(message, action, config ? config : this.actionConfig);
30
+ return firstValueFrom(snackbarRef.onAction());
31
+ }
32
+
33
+ // Opens a generic snackbar with a message
34
+ public openSnackbar(message: string, config?: MatSnackBarConfig): void {
35
+ this.snackbar.open(message, '', config ? config : this.defaultConfig);
36
+ }
37
+
38
+ }
@@ -0,0 +1,20 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { ThemeService } from './theme.service';
3
+
4
+ describe('ThemeService', () => {
5
+ let service: ThemeService;
6
+ // let mockDocument: any;
7
+
8
+ beforeEach(() => {
9
+ TestBed.configureTestingModule({
10
+ // providers: [
11
+ // { provide: DOCUMENT, useValue: mockDocument }
12
+ // ]
13
+ });
14
+ service = TestBed.inject(ThemeService);
15
+ });
16
+
17
+ it('should be created', () => {
18
+ expect(service).toBeTruthy();
19
+ });
20
+ });
@@ -0,0 +1,71 @@
1
+
2
+ import { Injectable, DOCUMENT, inject } from '@angular/core';
3
+ import { BehaviorSubject, Observable } from 'rxjs';
4
+ import { LocalStorageService } from './local-storage.service';
5
+
6
+ @Injectable({
7
+ providedIn: 'root'
8
+ })
9
+ export class ThemeService {
10
+ private storageService = inject(LocalStorageService);
11
+ private document = inject<Document>(DOCUMENT);
12
+
13
+
14
+ private readonly THEME_KEY: string = 'THEME';
15
+
16
+ private _currentTheme$: BehaviorSubject<string> = new BehaviorSubject<string>('');
17
+
18
+ get currentTheme$(): Observable<string> {
19
+ return this._currentTheme$.asObservable();
20
+ }
21
+
22
+ constructor() {
23
+ this.loadTheme();
24
+ }
25
+
26
+ /**
27
+ * Set one of the defined themes by passing its class name
28
+ *
29
+ * @param newTheme class name of the theme (pass empty string for default theme)
30
+ * @param useLocalStorage persist the current theme in the LocalStorage
31
+ */
32
+ public setTheme(newTheme: string, useLocalStorage?: boolean): void {
33
+ const currentTheme: string = this._currentTheme$.value;
34
+
35
+ if (newTheme === currentTheme) {
36
+ return;
37
+ }
38
+
39
+ if (currentTheme) {
40
+ this.document.body.classList.remove(currentTheme);
41
+ }
42
+
43
+ this._currentTheme$.next(newTheme);
44
+
45
+ if (newTheme) {
46
+ this.document.body.classList.add(newTheme);
47
+
48
+ if (useLocalStorage) {
49
+ this.storageService.setItem(this.THEME_KEY, JSON.stringify(newTheme));
50
+ }
51
+ } else {
52
+ if (useLocalStorage) {
53
+ this.storageService.removeItem(this.THEME_KEY);
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Loads the current theme stored in the local storage if available
60
+ *
61
+ */
62
+ private loadTheme(): void {
63
+ const theme: string | null = this.storageService.getItem(this.THEME_KEY);
64
+
65
+ if (!theme) {
66
+ return;
67
+ }
68
+
69
+ this.setTheme(theme.replace(/"/g, ''));
70
+ }
71
+ }
@@ -0,0 +1,24 @@
1
+ @if (config) {
2
+ @if (config.title) {
3
+ <h2 mat-dialog-title>{{ config.title }}</h2>
4
+ }
5
+
6
+ <mat-dialog-content>
7
+ @if (config.message) {
8
+ <p>{{ config.message }}</p>
9
+ }
10
+ </mat-dialog-content>
11
+
12
+ <mat-dialog-actions align="end">
13
+ @if (config.closeLabel) {
14
+ <button mat-button [mat-dialog-close]="false">
15
+ {{ config.closeLabel }}
16
+ </button>
17
+ }
18
+ @if (config.confirmLabel) {
19
+ <button mat-button color="primary" [mat-dialog-close]="true">
20
+ {{ config.confirmLabel }}
21
+ </button>
22
+ }
23
+ </mat-dialog-actions>
24
+ }
@@ -0,0 +1,85 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { MAT_DIALOG_DATA } from '@angular/material/dialog';
3
+ import { By } from '@angular/platform-browser';
4
+ import { SharedModule } from '../../../shared.module';
5
+ import { ConfirmDialogComponent } from './confirm-dialog.component';
6
+
7
+ describe('ConfirmDialogComponent', () => {
8
+ let component: ConfirmDialogComponent;
9
+ let fixture: ComponentFixture<ConfirmDialogComponent>;
10
+
11
+ beforeEach(async () => {
12
+ await TestBed.configureTestingModule({
13
+ declarations: [ConfirmDialogComponent],
14
+ imports: [SharedModule],
15
+ providers: [
16
+ { provide: MAT_DIALOG_DATA, useValue: {} }
17
+ ]
18
+ })
19
+ .compileComponents();
20
+
21
+ fixture = TestBed.createComponent(ConfirmDialogComponent);
22
+ component = fixture.componentInstance;
23
+ fixture.detectChanges();
24
+ });
25
+
26
+ it('should create', () => {
27
+ expect(component).toBeTruthy();
28
+ });
29
+
30
+ it('should display the title if provided', () => {
31
+ component.config = { title: 'Test Title' };
32
+ fixture.detectChanges();
33
+ const titleEl = fixture.debugElement.query(By.css('h2'));
34
+ expect(titleEl.nativeElement.textContent.trim()).toEqual('Test Title');
35
+ });
36
+
37
+ it('should not display the title if not provided', () => {
38
+ component.config = {};
39
+ fixture.detectChanges();
40
+ const titleEl = fixture.debugElement.query(By.css('h2'));
41
+ expect(titleEl).toBeFalsy();
42
+ });
43
+
44
+ it('should display the message if provided', () => {
45
+ component.config = { message: 'Test Message' };
46
+ fixture.detectChanges();
47
+ const messageEl = fixture.debugElement.query(By.css('mat-dialog-content p'));
48
+ expect(messageEl.nativeElement.textContent.trim()).toEqual('Test Message');
49
+ });
50
+
51
+ it('should not display the message if not provided', () => {
52
+ component.config = {};
53
+ fixture.detectChanges();
54
+ const messageEl = fixture.debugElement.query(By.css('mat-dialog-content p'));
55
+ expect(messageEl).toBeFalsy();
56
+ });
57
+
58
+ it('should display the close button label if provided', () => {
59
+ component.config = { closeLabel: 'Close' };
60
+ fixture.detectChanges();
61
+ const closeButtonEl = fixture.debugElement.query(By.css('mat-dialog-actions button:first-child'));
62
+ expect(closeButtonEl.nativeElement.textContent.trim()).toEqual('Close');
63
+ });
64
+
65
+ it('should not display the close button if not provided', () => {
66
+ component.config = {};
67
+ fixture.detectChanges();
68
+ const closeButtonEl = fixture.debugElement.query(By.css('mat-dialog-actions button:first-child'));
69
+ expect(closeButtonEl).toBeFalsy();
70
+ });
71
+
72
+ it('should display the confirm button label if provided', () => {
73
+ component.config = { confirmLabel: 'Confirm' };
74
+ fixture.detectChanges();
75
+ const confirmButtonEl = fixture.debugElement.query(By.css('mat-dialog-actions button:last-child'));
76
+ expect(confirmButtonEl.nativeElement.textContent.trim()).toEqual('Confirm');
77
+ });
78
+
79
+ it('should not display the confirm button if not provided', () => {
80
+ component.config = {};
81
+ fixture.detectChanges();
82
+ const confirmButtonEl = fixture.debugElement.query(By.css('mat-dialog-actions button:last-child'));
83
+ expect(confirmButtonEl).toBeFalsy();
84
+ });
85
+ });
@@ -0,0 +1,14 @@
1
+ import { Component, inject } from '@angular/core';
2
+ import { MAT_DIALOG_DATA } from '@angular/material/dialog';
3
+ import { ConfirmDialogConfig } from '../../../../models';
4
+
5
+ @Component({
6
+ selector: 'lf-confirm-dialog',
7
+ templateUrl: './confirm-dialog.component.html',
8
+ styleUrls: ['./confirm-dialog.component.scss'],
9
+ standalone: false
10
+ })
11
+ export class ConfirmDialogComponent {
12
+ public config = inject<ConfirmDialogConfig>(MAT_DIALOG_DATA);
13
+
14
+ }
@@ -0,0 +1,21 @@
1
+ <input
2
+ hidden
3
+ #file
4
+ type="file"
5
+ [accept]="accept"
6
+ onclick="this.value=null"
7
+ (change)="selectFile($event)" />
8
+
9
+ <button
10
+ mat-flat-button
11
+ class="lf-button"
12
+ type="button"
13
+ [color]="color"
14
+ [disabled]="disabled"
15
+ (click)="triggerInput()"
16
+ [matTooltip]="tooltip">
17
+ @if (matIcon) {
18
+ <mat-icon>{{ matIcon }}</mat-icon>
19
+ }
20
+ {{ label }}
21
+ </button>
@@ -0,0 +1,5 @@
1
+ @use '../../../../../styles/variables' as *;
2
+
3
+ .lf-button {
4
+ width: inherit;
5
+ }
@@ -0,0 +1,25 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { SharedModule } from '../../shared.module';
3
+ import { FileUploadComponent } from './file-upload.component';
4
+
5
+ describe('FileUploadComponent', () => {
6
+ let component: FileUploadComponent;
7
+ let fixture: ComponentFixture<FileUploadComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [
12
+ FileUploadComponent,
13
+ SharedModule
14
+ ]
15
+ }).compileComponents();
16
+
17
+ fixture = TestBed.createComponent(FileUploadComponent);
18
+ component = fixture.componentInstance;
19
+ fixture.detectChanges();
20
+ });
21
+
22
+ it('should create', () => {
23
+ expect(component).toBeTruthy();
24
+ });
25
+ });
@@ -0,0 +1,43 @@
1
+ import { Component, ElementRef, EventEmitter, inject, Input, Output, ViewChild } from '@angular/core';
2
+ import { Logger } from '../../../services';
3
+ import { SharedModule } from '../../shared.module';
4
+
5
+ @Component({
6
+ selector: 'lf-file-upload',
7
+ templateUrl: './file-upload.component.html',
8
+ styleUrls: ['./file-upload.component.scss'],
9
+ standalone: true,
10
+ imports: [SharedModule]
11
+ })
12
+ export class FileUploadComponent {
13
+
14
+ private logger: Logger = inject(Logger);
15
+
16
+ @ViewChild('file') public fileElement: ElementRef;
17
+
18
+ @Input() public color: 'primary' | 'accent' | 'warn' = 'primary';
19
+ @Input() public label: string;
20
+ @Input() public matIcon: string;
21
+ @Input() public disabled: boolean = false;
22
+ @Input() public accept: string;
23
+ @Input() public tooltip: string;
24
+
25
+ @Output() public fileChange: EventEmitter<File> = new EventEmitter<File>();
26
+
27
+ public selectFile(event: Event): void {
28
+ const input: HTMLInputElement = event.target as HTMLInputElement;
29
+
30
+ if (!input || !input.files) return;
31
+ const file: File = input.files[0];
32
+
33
+ if (!file) return;
34
+
35
+ this.logger.log('[FileUploadComponent]', file);
36
+ this.fileChange.emit(file);
37
+ }
38
+
39
+ public triggerInput(): void {
40
+ this.fileElement.nativeElement.click();
41
+ }
42
+
43
+ }
@@ -0,0 +1,17 @@
1
+ <!-- mat icon -->
2
+ @if (matIcon) {
3
+ <mat-icon
4
+ class="lf-icon"
5
+ [class.material-icons-outlined]="outlineIcon"
6
+ [class.lf-icon-align-middle]="alignMiddle">
7
+ {{ matIcon }}
8
+ </mat-icon>
9
+ } @else {
10
+ @if (svgIcon) {
11
+ <mat-icon
12
+ class="lf-icon"
13
+ [svgIcon]="svgIcon"
14
+ [class.lf-icon-align-middle]="alignMiddle"></mat-icon>
15
+ }
16
+ }
17
+ <!-- svg icon -->
@@ -0,0 +1,9 @@
1
+ @use '../../../../../styles/variables' as *;
2
+
3
+ :host {
4
+ display: contents;
5
+ }
6
+
7
+ .lf-icon-align-middle {
8
+ vertical-align: bottom;
9
+ }
@@ -0,0 +1,22 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { SharedModule } from '../../shared.module';
3
+ import { IconComponent } from './icon.component';
4
+
5
+ describe('IconComponent', () => {
6
+ let component: IconComponent;
7
+ let fixture: ComponentFixture<IconComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [SharedModule]
12
+ }).compileComponents();
13
+
14
+ fixture = TestBed.createComponent(IconComponent);
15
+ component = fixture.componentInstance;
16
+ fixture.detectChanges();
17
+ });
18
+
19
+ it('should create', () => {
20
+ expect(component).toBeTruthy();
21
+ });
22
+ });
@@ -0,0 +1,17 @@
1
+ import { Component, Input } from '@angular/core';
2
+ import { SharedModule } from '../../shared.module';
3
+
4
+ @Component({
5
+ selector: 'lf-icon',
6
+ templateUrl: './icon.component.html',
7
+ styleUrls: ['./icon.component.scss'],
8
+ standalone: true,
9
+ imports: [SharedModule]
10
+ })
11
+ export class IconComponent {
12
+
13
+ @Input() public matIcon: string;
14
+ @Input() public svgIcon: string;
15
+ @Input() public outlineIcon: boolean = false;
16
+ @Input() public alignMiddle: boolean = false;
17
+ }
@@ -0,0 +1,38 @@
1
+ @if (inputConfig) {
2
+ <mat-form-field class="lf-input" appearance="fill" [class.lf-hide-hint]="!inputConfig.hint">
3
+ <mat-label>{{ inputConfig.label || "Search" }}</mat-label>
4
+ @if (inputConfig.hint) {
5
+ <mat-hint>{{ inputConfig.hint }}</mat-hint>
6
+ }
7
+ <input
8
+ matInput
9
+ type="text"
10
+ [(ngModel)]="inputValue"
11
+ [disabled]="inputConfig.disabled!"
12
+ (keyup.enter)="inputSubmitted(inputValue)"
13
+ (ngModelChange)="inputChanged($event)"
14
+ #input />
15
+ @if (isMobile) {
16
+ <button mat-icon-button matPrefix color="accent" (click)="inputPrefixAction()">
17
+ <mat-icon>arrow_back</mat-icon>
18
+ </button>
19
+ }
20
+ @if (inputValue) {
21
+ <button class="lf-input-close-button" mat-icon-button matSuffix (click)="clearInput()">
22
+ <mat-icon>close</mat-icon>
23
+ </button>
24
+ }
25
+ @if (inputConfig.matIconSubmit) {
26
+ <button
27
+ mat-icon-button
28
+ matSuffix
29
+ color="accent"
30
+ type="submit"
31
+ (click)="inputSubmitted(inputValue)">
32
+ <mat-icon>
33
+ {{ inputConfig.matIconSubmit }}
34
+ </mat-icon>
35
+ </button>
36
+ }
37
+ </mat-form-field>
38
+ }
@@ -0,0 +1,31 @@
1
+ @use '../../../../../styles/variables' as *;
2
+
3
+ .lf-input {
4
+ width: 100%;
5
+
6
+ ::ng-deep .mdc-text-field {
7
+ border-radius: $border-radius !important;
8
+ }
9
+
10
+ ::ng-deep .mdc-line-ripple {
11
+ &::after {
12
+ border-bottom-width: 0 !important;
13
+ }
14
+
15
+ &::before {
16
+ border-bottom-width: 0 !important;
17
+ }
18
+ }
19
+
20
+ .lf-input-close-button {
21
+ color: $color-gray;
22
+ }
23
+
24
+ &.lf-hide-hint{
25
+ ::ng-deep {
26
+ .mat-mdc-form-field-subscript-wrapper {
27
+ display: none !important;
28
+ }
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,62 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { By } from '@angular/platform-browser';
3
+ import { SharedModule } from '../../shared.module';
4
+ import { InputComponent } from './input.component';
5
+
6
+ describe('InputComponent', () => {
7
+ let component: InputComponent;
8
+ let fixture: ComponentFixture<InputComponent>;
9
+
10
+ beforeEach(async () => {
11
+ await TestBed.configureTestingModule({
12
+ imports: [SharedModule]
13
+ })
14
+ .compileComponents();
15
+
16
+ fixture = TestBed.createComponent(InputComponent);
17
+ component = fixture.componentInstance;
18
+ fixture.detectChanges();
19
+ });
20
+
21
+ it('should create', () => {
22
+ expect(component).toBeTruthy();
23
+ });
24
+
25
+ it('should set the label to "Search" if no label is provided', () => {
26
+ const labelElement = fixture.debugElement.query(By.css('mat-label')).nativeElement;
27
+ expect(labelElement.textContent.trim()).toBe('Search');
28
+ });
29
+
30
+ it('should set the label to the provided value if a label is provided', () => {
31
+ component.inputConfig.label = 'Test';
32
+ fixture.detectChanges();
33
+ const labelElement = fixture.debugElement.query(By.css('mat-label')).nativeElement;
34
+ expect(labelElement.textContent.trim()).toBe('Test');
35
+ });
36
+
37
+ it('should display the hint if a hint is provided', () => {
38
+ component.inputConfig.hint = 'Test hint';
39
+ fixture.detectChanges();
40
+ const hintElement = fixture.debugElement.query(By.css('mat-hint')).nativeElement;
41
+ expect(hintElement.textContent.trim()).toBe('Test hint');
42
+ });
43
+
44
+ it('should hide the hint if no hint is provided', () => {
45
+ const hintElement = fixture.debugElement.query(By.css('mat-hint'));
46
+ expect(hintElement).toBeFalsy();
47
+ });
48
+
49
+ it('should enable the input if not disabled', () => {
50
+ const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
51
+ expect(inputElement.disabled).toBeFalsy();
52
+ });
53
+
54
+ it('should call inputChanged when input value is changed', () => {
55
+ spyOn(component, 'inputChanged');
56
+ const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
57
+ inputElement.value = 'test';
58
+ inputElement.dispatchEvent(new Event('input'));
59
+ fixture.detectChanges();
60
+ expect(component.inputChanged).toHaveBeenCalledWith('test');
61
+ });
62
+ });