diff --git a/frontend/src/app/components/home/home.html b/frontend/src/app/components/home/home.html index 031d089..347bf56 100644 --- a/frontend/src/app/components/home/home.html +++ b/frontend/src/app/components/home/home.html @@ -3,10 +3,20 @@
- - - dashboard - + + {{ item.iconUrl }} + + + + + + dashboard + + + {{ item.displayName }}
diff --git a/frontend/src/app/components/home/home.scss b/frontend/src/app/components/home/home.scss index 492909d..65dd2e8 100644 --- a/frontend/src/app/components/home/home.scss +++ b/frontend/src/app/components/home/home.scss @@ -35,6 +35,14 @@ border-radius: 8px; object-fit: cover; flex-shrink: 0; + + &.material-icon { + display: flex; + justify-content: center; + align-items: center; + font-size: 48px; + color: #1976d2; + } } .card-title { diff --git a/frontend/src/app/components/home/home.spec.ts b/frontend/src/app/components/home/home.spec.ts index 70dd3ad..7961ce0 100644 --- a/frontend/src/app/components/home/home.spec.ts +++ b/frontend/src/app/components/home/home.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Home } from './home'; +import { HomeComponent } from './home'; -describe('Home', () => { - let component: Home; - let fixture: ComponentFixture; +describe('HomeComponent', () => { + let component: HomeComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [Home] + imports: [HomeComponent] }) .compileComponents(); - fixture = TestBed.createComponent(Home); + fixture = TestBed.createComponent(HomeComponent); component = fixture.componentInstance; await fixture.whenStable(); }); diff --git a/frontend/src/app/components/home/home.ts b/frontend/src/app/components/home/home.ts index 6e1fcd6..2350e2a 100644 --- a/frontend/src/app/components/home/home.ts +++ b/frontend/src/app/components/home/home.ts @@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common'; import { MaterialModule } from '../../material.module'; import { Api } from '../../services/api'; import { Item } from '../../models/item'; +import { IconService } from '../../services/icon.service'; import { Observable } from 'rxjs'; @Component({ @@ -16,7 +17,10 @@ export class HomeComponent implements OnInit { items$!: Observable; imgErrorMap = new Map(); - constructor(private apiService: Api) {} + constructor( + private apiService: Api, + private iconService: IconService + ) {} ngOnInit(): void { this.items$ = this.apiService.getItems(); @@ -25,4 +29,8 @@ export class HomeComponent implements OnInit { onImgError(itemId: number) { this.imgErrorMap.set(itemId, true); } + + isMaterialIcon(iconUrl: string): boolean { + return this.iconService.isValidIcon(iconUrl); + } } \ No newline at end of file diff --git a/frontend/src/app/components/item-form/item-form.html b/frontend/src/app/components/item-form/item-form.html index c042c9c..e39daab 100644 --- a/frontend/src/app/components/item-form/item-form.html +++ b/frontend/src/app/components/item-form/item-form.html @@ -2,6 +2,13 @@ {{ isEditMode ? 'Edit Item' : 'Create Item' }} + +
+ + Custom Icon URL verwenden + +
+
Name @@ -18,7 +25,34 @@ - + +
+ + Icon auswählen + + search + + + {{ icon.name }} + {{ icon.name }} + {{ icon.category }} + + + + + +
+ Vorschau: + {{ getSelectedIconName() }} +
+
+ + + Icon URL diff --git a/frontend/src/app/components/item-form/item-form.scss b/frontend/src/app/components/item-form/item-form.scss index a2ff0e0..2ef71ce 100644 --- a/frontend/src/app/components/item-form/item-form.scss +++ b/frontend/src/app/components/item-form/item-form.scss @@ -19,3 +19,46 @@ mat-form-field { gap: 10px; margin-top: 20px; } + +.icon-mode-toggle { + margin: 16px 0; +} + +.icon-picker-section { + .icon-preview { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + background-color: #f5f5f5; + border-radius: 4px; + margin-bottom: 16px; + + .preview-label { + font-weight: 500; + color: #666; + } + + .preview-icon { + font-size: 48px; + width: 48px; + height: 48px; + color: #1976d2; + } + } +} + +::ng-deep { + .mat-mdc-option { + .icon-option-text { + margin-left: 12px; + margin-right: auto; + } + + .icon-category { + font-size: 12px; + color: #666; + margin-left: 8px; + } + } +} diff --git a/frontend/src/app/components/item-form/item-form.spec.ts b/frontend/src/app/components/item-form/item-form.spec.ts index 6d2d060..f798db6 100644 --- a/frontend/src/app/components/item-form/item-form.spec.ts +++ b/frontend/src/app/components/item-form/item-form.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ItemForm } from './item-form'; +import { ItemFormComponent } from './item-form'; -describe('ItemForm', () => { - let component: ItemForm; - let fixture: ComponentFixture; +describe('ItemFormComponent', () => { + let component: ItemFormComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ItemForm] + imports: [ItemFormComponent] }) .compileComponents(); - fixture = TestBed.createComponent(ItemForm); + fixture = TestBed.createComponent(ItemFormComponent); component = fixture.componentInstance; await fixture.whenStable(); }); diff --git a/frontend/src/app/components/item-form/item-form.ts b/frontend/src/app/components/item-form/item-form.ts index 66b9d45..a7853af 100644 --- a/frontend/src/app/components/item-form/item-form.ts +++ b/frontend/src/app/components/item-form/item-form.ts @@ -1,51 +1,121 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms'; import { Api } from '../../services/api'; import { Item } from '../../models/item'; import { MaterialModule } from '../../material.module'; import { CommonModule } from '@angular/common'; +import { IconService, MaterialIcon } from '../../services/icon.service'; +import { Observable } from 'rxjs'; +import { map, startWith } from 'rxjs/operators'; @Component({ selector: 'app-item-form', templateUrl: './item-form.html', styleUrls: ['./item-form.scss'], standalone: true, - imports: [MaterialModule, CommonModule, ReactiveFormsModule], + imports: [MaterialModule, CommonModule, ReactiveFormsModule, FormsModule], }) export class ItemFormComponent implements OnInit { itemForm: FormGroup; isEditMode = false; itemId: number | null = null; + + // Icon-Auswahl Eigenschaften + filteredIcons$!: Observable; + allIcons: MaterialIcon[] = []; + useCustomUrl = false; constructor( private fb: FormBuilder, private apiService: Api, private router: Router, - private route: ActivatedRoute + private route: ActivatedRoute, + private iconService: IconService ) { this.itemForm = this.fb.group({ name: ['', Validators.required], displayName: ['', Validators.required], target: ['', Validators.required], - iconUrl: [''] + iconUrl: [''], + iconName: [''] }); } ngOnInit(): void { + // Icons laden + this.allIcons = this.iconService.getAllIcons(); + + // Autocomplete für Icons einrichten + this.filteredIcons$ = this.itemForm.get('iconName')!.valueChanges.pipe( + startWith(''), + map(value => this._filterIcons(value || '')) + ); + const idParam = this.route.snapshot.params['id']; if (idParam) { this.isEditMode = true; this.itemId = +idParam; // Convert string to number this.apiService.getItem(this.itemId).subscribe(item => { this.itemForm.patchValue(item); + + // Prüfen ob Custom URL oder Material Icon verwendet wird + if (item.iconUrl) { + if (this.iconService.isValidIcon(item.iconUrl)) { + // Es ist ein Material Icon Name + this.itemForm.patchValue({ iconName: item.iconUrl, iconUrl: '' }); + this.useCustomUrl = false; + } else { + // Es ist eine Custom URL + this.useCustomUrl = true; + } + } }); } } + + private _filterIcons(value: string): MaterialIcon[] { + const filterValue = value.toLowerCase(); + return this.allIcons.filter(icon => + icon.name.toLowerCase().includes(filterValue) || + icon.category.toLowerCase().includes(filterValue) + ); + } + + onToggleChange(event: any): void { + this.useCustomUrl = event.checked; + if (this.useCustomUrl) { + this.itemForm.patchValue({ iconName: '' }); + } else { + this.itemForm.patchValue({ iconUrl: '' }); + } + } + + getSelectedIconName(): string { + return this.itemForm.get('iconName')?.value || 'dashboard'; + } + + displayFn(iconName: string): string { + return iconName; + } onSubmit(): void { if (this.itemForm.valid) { - const itemData: Item = { id: this.itemId, ...this.itemForm.value }; + // Bestimme welches Icon-Feld verwendet werden soll + let finalIconUrl = this.itemForm.value.iconUrl; + if (!this.useCustomUrl && this.itemForm.value.iconName) { + // Verwende Material Icon Namen als iconUrl + finalIconUrl = this.itemForm.value.iconName; + } + + const itemData: Item = { + id: this.itemId ?? 0, + name: this.itemForm.value.name, + displayName: this.itemForm.value.displayName, + target: this.itemForm.value.target, + iconUrl: finalIconUrl + }; + if (this.isEditMode) { this.apiService.updateItem(this.itemId!, itemData).subscribe(() => { this.router.navigate(['/']); diff --git a/frontend/src/app/components/item-list/item-list.spec.ts b/frontend/src/app/components/item-list/item-list.spec.ts index ac4a40a..92a11ce 100644 --- a/frontend/src/app/components/item-list/item-list.spec.ts +++ b/frontend/src/app/components/item-list/item-list.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ItemList } from './item-list'; +import { ItemListComponent } from './item-list'; -describe('ItemList', () => { - let component: ItemList; - let fixture: ComponentFixture; +describe('ItemListComponent', () => { + let component: ItemListComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ItemList] + imports: [ItemListComponent] }) .compileComponents(); - fixture = TestBed.createComponent(ItemList); + fixture = TestBed.createComponent(ItemListComponent); component = fixture.componentInstance; await fixture.whenStable(); }); diff --git a/frontend/src/app/material.module.ts b/frontend/src/app/material.module.ts index 25eab33..ec69cc5 100644 --- a/frontend/src/app/material.module.ts +++ b/frontend/src/app/material.module.ts @@ -7,6 +7,8 @@ import { MatTableModule } from '@angular/material/table'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; @NgModule({ exports: [ @@ -18,6 +20,8 @@ import { MatTooltipModule } from '@angular/material/tooltip'; MatToolbarModule, MatIconModule, MatTooltipModule, + MatSlideToggleModule, + MatAutocompleteModule, ] }) export class MaterialModule { } diff --git a/frontend/src/app/services/icon.service.ts b/frontend/src/app/services/icon.service.ts new file mode 100644 index 0000000..c882087 --- /dev/null +++ b/frontend/src/app/services/icon.service.ts @@ -0,0 +1,161 @@ +import { Injectable } from '@angular/core'; + +export interface MaterialIcon { + name: string; + category: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class IconService { + // Häufig verwendete Material Icons, gruppiert nach Kategorien + private icons: MaterialIcon[] = [ + // Navigation + { name: 'home', category: 'Navigation' }, + { name: 'dashboard', category: 'Navigation' }, + { name: 'menu', category: 'Navigation' }, + { name: 'apps', category: 'Navigation' }, + { name: 'arrow_back', category: 'Navigation' }, + { name: 'arrow_forward', category: 'Navigation' }, + { name: 'expand_more', category: 'Navigation' }, + { name: 'expand_less', category: 'Navigation' }, + + // Web & Browser + { name: 'web', category: 'Web' }, + { name: 'language', category: 'Web' }, + { name: 'public', category: 'Web' }, + { name: 'link', category: 'Web' }, + { name: 'http', category: 'Web' }, + { name: 'https', category: 'Web' }, + { name: 'vpn_lock', category: 'Web' }, + + // Kommunikation + { name: 'email', category: 'Kommunikation' }, + { name: 'mail', category: 'Kommunikation' }, + { name: 'chat', category: 'Kommunikation' }, + { name: 'message', category: 'Kommunikation' }, + { name: 'forum', category: 'Kommunikation' }, + { name: 'phone', category: 'Kommunikation' }, + + // Content + { name: 'create', category: 'Content' }, + { name: 'edit', category: 'Content' }, + { name: 'add', category: 'Content' }, + { name: 'delete', category: 'Content' }, + { name: 'content_copy', category: 'Content' }, + { name: 'save', category: 'Content' }, + + // Dateien & Ordner + { name: 'folder', category: 'Dateien' }, + { name: 'folder_open', category: 'Dateien' }, + { name: 'description', category: 'Dateien' }, + { name: 'insert_drive_file', category: 'Dateien' }, + { name: 'cloud', category: 'Dateien' }, + { name: 'cloud_upload', category: 'Dateien' }, + { name: 'cloud_download', category: 'Dateien' }, + + // Medien + { name: 'image', category: 'Medien' }, + { name: 'photo', category: 'Medien' }, + { name: 'video_library', category: 'Medien' }, + { name: 'movie', category: 'Medien' }, + { name: 'music_note', category: 'Medien' }, + { name: 'audiotrack', category: 'Medien' }, + + // Business + { name: 'work', category: 'Business' }, + { name: 'business', category: 'Business' }, + { name: 'store', category: 'Business' }, + { name: 'shopping_cart', category: 'Business' }, + { name: 'payment', category: 'Business' }, + { name: 'account_balance', category: 'Business' }, + + // Sozial + { name: 'person', category: 'Sozial' }, + { name: 'people', category: 'Sozial' }, + { name: 'group', category: 'Sozial' }, + { name: 'account_circle', category: 'Sozial' }, + { name: 'favorite', category: 'Sozial' }, + { name: 'share', category: 'Sozial' }, + + // System + { name: 'settings', category: 'System' }, + { name: 'build', category: 'System' }, + { name: 'search', category: 'System' }, + { name: 'info', category: 'System' }, + { name: 'help', category: 'System' }, + { name: 'warning', category: 'System' }, + { name: 'error', category: 'System' }, + { name: 'check_circle', category: 'System' }, + { name: 'notifications', category: 'System' }, + + // Geräte + { name: 'computer', category: 'Geräte' }, + { name: 'phone_android', category: 'Geräte' }, + { name: 'tablet', category: 'Geräte' }, + { name: 'laptop', category: 'Geräte' }, + { name: 'tv', category: 'Geräte' }, + { name: 'watch', category: 'Geräte' }, + + // Entwicklung + { name: 'code', category: 'Entwicklung' }, + { name: 'developer_mode', category: 'Entwicklung' }, + { name: 'bug_report', category: 'Entwicklung' }, + { name: 'terminal', category: 'Entwicklung' }, + { name: 'api', category: 'Entwicklung' }, + + // Sicherheit + { name: 'lock', category: 'Sicherheit' }, + { name: 'lock_open', category: 'Sicherheit' }, + { name: 'security', category: 'Sicherheit' }, + { name: 'verified_user', category: 'Sicherheit' }, + { name: 'vpn_key', category: 'Sicherheit' }, + + // Verschiedenes + { name: 'star', category: 'Verschiedenes' }, + { name: 'bookmark', category: 'Verschiedenes' }, + { name: 'label', category: 'Verschiedenes' }, + { name: 'lightbulb', category: 'Verschiedenes' }, + { name: 'extension', category: 'Verschiedenes' }, + { name: 'widgets', category: 'Verschiedenes' }, + ]; + + constructor() { } + + /** + * Gibt alle verfügbaren Icons zurück + */ + getAllIcons(): MaterialIcon[] { + return this.icons; + } + + /** + * Filtert Icons nach Suchbegriff + */ + filterIcons(searchTerm: string): MaterialIcon[] { + if (!searchTerm) { + return this.icons; + } + + const term = searchTerm.toLowerCase(); + return this.icons.filter(icon => + icon.name.toLowerCase().includes(term) || + icon.category.toLowerCase().includes(term) + ); + } + + /** + * Gibt Icon-Namen zurück (für Autocomplete-Display) + */ + getIconNames(): string[] { + return this.icons.map(icon => icon.name); + } + + /** + * Prüft, ob ein Icon-Name in der Liste existiert + */ + isValidIcon(iconName: string): boolean { + return this.icons.some(icon => icon.name === iconName); + } +}