From 40fde36bc68a6b3d4ab2e8c9e7bb18283cb4790b Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 21 Oct 2025 01:25:42 +0200 Subject: [PATCH] Add LinkComponent, integrate with Home and Shell components, and enhance navigation structure --- src/angular/frontend/package.json | 4 +- src/angular/frontend/src/app/app.routes.ts | 16 ++- .../app/components/home/home.component.html | 120 ++++++++++++++++++ .../src/app/components/home/home.component.ts | 5 +- .../app/components/link/link.component.html | 1 + .../app/components/link/link.component.scss | 0 .../components/link/link.component.spec.ts | 23 ++++ .../src/app/components/link/link.component.ts | 24 ++++ .../shell/drawer/drawer.component.html | 14 ++ .../shell/drawer/drawer.component.scss | 47 +++++++ .../shell/drawer/drawer.component.spec.ts | 23 ++++ .../shell/drawer/drawer.component.ts | 14 ++ .../nav-profile/nav-profile.component.html | 2 +- .../nav-profile/nav-profile.component.scss | 1 + .../app/components/shell/shell.component.html | 40 +++++- .../app/components/shell/shell.component.scss | 63 ++++++++- .../app/components/shell/shell.component.ts | 104 ++++++++++++++- 17 files changed, 480 insertions(+), 21 deletions(-) create mode 100644 src/angular/frontend/src/app/components/link/link.component.html create mode 100644 src/angular/frontend/src/app/components/link/link.component.scss create mode 100644 src/angular/frontend/src/app/components/link/link.component.spec.ts create mode 100644 src/angular/frontend/src/app/components/link/link.component.ts create mode 100644 src/angular/frontend/src/app/components/shell/drawer/drawer.component.html create mode 100644 src/angular/frontend/src/app/components/shell/drawer/drawer.component.scss create mode 100644 src/angular/frontend/src/app/components/shell/drawer/drawer.component.spec.ts create mode 100644 src/angular/frontend/src/app/components/shell/drawer/drawer.component.ts diff --git a/src/angular/frontend/package.json b/src/angular/frontend/package.json index 101fed7..8762983 100644 --- a/src/angular/frontend/package.json +++ b/src/angular/frontend/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "scripts": { "ng": "ng", - "start": "ng serve", + "start": "ng serve --host 0.0.0.0", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" @@ -38,4 +38,4 @@ "karma-jasmine-html-reporter": "~2.1.0", "typescript": "~5.9.3" } -} \ No newline at end of file +} diff --git a/src/angular/frontend/src/app/app.routes.ts b/src/angular/frontend/src/app/app.routes.ts index e340292..7e5f923 100644 --- a/src/angular/frontend/src/app/app.routes.ts +++ b/src/angular/frontend/src/app/app.routes.ts @@ -19,12 +19,26 @@ export const routes: Routes = [ title: 'Home', path: '', component: HomeComponent, + data: { showInNav: true } + }, + { + title: 'Dashboard', + path: 'dashboard', + component: HomeComponent, + data: { showInNav: true }, + children: [ + { + title: 'Test', + path: 'test', + component: HomeComponent, + } + ] }, ], canActivate: [authGuard], resolve: { user: subjectResolver, - } + }, }, { path: '**', diff --git a/src/angular/frontend/src/app/components/home/home.component.html b/src/angular/frontend/src/app/components/home/home.component.html index 5f2c53f..c4dd070 100644 --- a/src/angular/frontend/src/app/components/home/home.component.html +++ b/src/angular/frontend/src/app/components/home/home.component.html @@ -1 +1,121 @@ +test +

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

+

home works!

home works!

diff --git a/src/angular/frontend/src/app/components/home/home.component.ts b/src/angular/frontend/src/app/components/home/home.component.ts index 2692e8b..4101bca 100644 --- a/src/angular/frontend/src/app/components/home/home.component.ts +++ b/src/angular/frontend/src/app/components/home/home.component.ts @@ -1,8 +1,11 @@ import { Component } from '@angular/core'; +import {LinkComponent} from '../link/link.component'; @Component({ selector: 'app-home', - imports: [], + imports: [ + LinkComponent + ], templateUrl: './home.component.html', styleUrl: './home.component.scss', standalone: true, diff --git a/src/angular/frontend/src/app/components/link/link.component.html b/src/angular/frontend/src/app/components/link/link.component.html new file mode 100644 index 0000000..1a73114 --- /dev/null +++ b/src/angular/frontend/src/app/components/link/link.component.html @@ -0,0 +1 @@ + diff --git a/src/angular/frontend/src/app/components/link/link.component.scss b/src/angular/frontend/src/app/components/link/link.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/angular/frontend/src/app/components/link/link.component.spec.ts b/src/angular/frontend/src/app/components/link/link.component.spec.ts new file mode 100644 index 0000000..b106be3 --- /dev/null +++ b/src/angular/frontend/src/app/components/link/link.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LinkComponent } from './link.component'; + +describe('LinkComponent', () => { + let component: LinkComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LinkComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LinkComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/angular/frontend/src/app/components/link/link.component.ts b/src/angular/frontend/src/app/components/link/link.component.ts new file mode 100644 index 0000000..fb88ac6 --- /dev/null +++ b/src/angular/frontend/src/app/components/link/link.component.ts @@ -0,0 +1,24 @@ +import {Component, inject, input} from '@angular/core'; +import {Router} from '@angular/router'; + +@Component({ + selector: 'app-link', + imports: [], + templateUrl: './link.component.html', + styleUrl: './link.component.scss' +}) +export class LinkComponent { + + public href = input.required(); + public external = input(false); + + private router = inject(Router); + + click($event: PointerEvent) { + if (!this.external()) { + $event.preventDefault(); + this.router.navigate([this.href()]).then(); + } + } + +} diff --git a/src/angular/frontend/src/app/components/shell/drawer/drawer.component.html b/src/angular/frontend/src/app/components/shell/drawer/drawer.component.html new file mode 100644 index 0000000..6a1ac2a --- /dev/null +++ b/src/angular/frontend/src/app/components/shell/drawer/drawer.component.html @@ -0,0 +1,14 @@ +
+ +
+
+
+ +
+
+ +
+
+ +
+
diff --git a/src/angular/frontend/src/app/components/shell/drawer/drawer.component.scss b/src/angular/frontend/src/app/components/shell/drawer/drawer.component.scss new file mode 100644 index 0000000..e43a9de --- /dev/null +++ b/src/angular/frontend/src/app/components/shell/drawer/drawer.component.scss @@ -0,0 +1,47 @@ +:host { + display: flex; + + height: 100%; + overflow: auto; + + .drawer-container { + display: flex; + flex-direction: column; + padding: 1rem; + } + + .main-content-container{ + display: flex; + flex-direction: column; + padding: 1rem 0 0 1rem; + } + + .drawer-container { + background-color: var(--neutral-70); + } + + .main-content-container { + flex-grow: 1; + + .top-nav-breadcrumb-container, .top-nav-container { + margin-right: 1rem; + } + + .top-nav-breadcrumb-container { + margin-top: 1rem; + padding: 0.5rem 0; + border-bottom: 1px solid var(--neutral-70); + border-top: 1px solid var(--neutral-70); + } + + .content-container { + padding-top: 1rem; + padding-right: 1rem; + flex-grow: 1; + overflow: auto; + + } + } +} + + diff --git a/src/angular/frontend/src/app/components/shell/drawer/drawer.component.spec.ts b/src/angular/frontend/src/app/components/shell/drawer/drawer.component.spec.ts new file mode 100644 index 0000000..0ab22bd --- /dev/null +++ b/src/angular/frontend/src/app/components/shell/drawer/drawer.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DrawerComponent } from './drawer.component'; + +describe('DrawerComponent', () => { + let component: DrawerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DrawerComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DrawerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/angular/frontend/src/app/components/shell/drawer/drawer.component.ts b/src/angular/frontend/src/app/components/shell/drawer/drawer.component.ts new file mode 100644 index 0000000..735899f --- /dev/null +++ b/src/angular/frontend/src/app/components/shell/drawer/drawer.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import {RouterOutlet} from '@angular/router'; + +@Component({ + selector: 'app-drawer', + imports: [ + RouterOutlet + ], + templateUrl: './drawer.component.html', + styleUrl: './drawer.component.scss' +}) +export class DrawerComponent { + +} diff --git a/src/angular/frontend/src/app/components/shell/nav-profile/nav-profile.component.html b/src/angular/frontend/src/app/components/shell/nav-profile/nav-profile.component.html index 403d35c..aff10bb 100644 --- a/src/angular/frontend/src/app/components/shell/nav-profile/nav-profile.component.html +++ b/src/angular/frontend/src/app/components/shell/nav-profile/nav-profile.component.html @@ -2,4 +2,4 @@ {{user()}} Logout -
+
diff --git a/src/angular/frontend/src/app/components/shell/nav-profile/nav-profile.component.scss b/src/angular/frontend/src/app/components/shell/nav-profile/nav-profile.component.scss index 834910e..a458ae1 100644 --- a/src/angular/frontend/src/app/components/shell/nav-profile/nav-profile.component.scss +++ b/src/angular/frontend/src/app/components/shell/nav-profile/nav-profile.component.scss @@ -15,5 +15,6 @@ height: 3rem; overflow: hidden; background-color: var(--bg-color); + border-radius: 0.5rem; } } diff --git a/src/angular/frontend/src/app/components/shell/shell.component.html b/src/angular/frontend/src/app/components/shell/shell.component.html index 025c400..21e2c33 100644 --- a/src/angular/frontend/src/app/components/shell/shell.component.html +++ b/src/angular/frontend/src/app/components/shell/shell.component.html @@ -1,4 +1,36 @@ - - - - + +
+ + + + + +
+
+ {{ title() }} + +
+
+ + / + + @for (crumb of breadcrumbs(); let last = $last; track crumb.label) { + + @if (crumb.url && !last) { + {{ crumb.label }} + } @else { + {{ crumb.label }} + } + @if (!last) { + / + } + } + +
+
diff --git a/src/angular/frontend/src/app/components/shell/shell.component.scss b/src/angular/frontend/src/app/components/shell/shell.component.scss index f96a889..858d656 100644 --- a/src/angular/frontend/src/app/components/shell/shell.component.scss +++ b/src/angular/frontend/src/app/components/shell/shell.component.scss @@ -1,9 +1,62 @@ -.shell-nav-bar { - .logo { - $height: 3rem; - height: $height; - width: #{$height * 3.356876171875}; +:host { + height: 100vh; + width: 100vw; + display: block; + + .top-nav-container { + display: flex; + + .title { + font-size: 2rem; + font-weight: bold; + } + app-nav-profile { + margin-left: auto; + } } + + .drawer-content { + display: flex; + flex-direction: column; + .logo { + $height: 3rem; + height: $height; + width: #{$height * 3.356876171875}; + padding-bottom: 1rem; + border-bottom: 1px solid var(--neutral-60); + } + + .nav-links { + margin-top: 1rem; + display: flex; + flex-direction: column; + + .nav-link { + width: 100%; + + padding: 0.5rem; + border-radius: 0.25rem; + + &:hover, &.active { + background-color: var(--neutral-60); + } + } + + ::ng-deep app-link { + a { + display: flex; + width: 100%; + color: unset; + text-decoration: none; + } + + } + } + } + +} +.shell-nav-bar { + app-nav-profile { margin-left: auto; } diff --git a/src/angular/frontend/src/app/components/shell/shell.component.ts b/src/angular/frontend/src/app/components/shell/shell.component.ts index a117ff9..8a5c17f 100644 --- a/src/angular/frontend/src/app/components/shell/shell.component.ts +++ b/src/angular/frontend/src/app/components/shell/shell.component.ts @@ -1,21 +1,111 @@ -import {Component, computed, inject} from '@angular/core'; +import {Component, computed, OnInit, signal} from '@angular/core'; import {NavProfileComponent} from './nav-profile/nav-profile.component'; -import {ActivatedRoute, RouterOutlet} from '@angular/router'; -import {PanelComponent} from '../panel/panel.component'; +import {ActivatedRoute, NavigationEnd, Route, Router, RouterLink} from '@angular/router'; import {IconComponent} from '../icon/icon.component'; -import {toSignal} from '@angular/core/rxjs-interop'; +import {DrawerComponent} from './drawer/drawer.component'; +import {filter} from 'rxjs'; +import {NgClass} from '@angular/common'; +import {LinkComponent} from '../link/link.component'; @Component({ selector: 'app-shell', imports: [ NavProfileComponent, - PanelComponent, - IconComponent + IconComponent, + DrawerComponent, + RouterLink, + NgClass, + LinkComponent ], templateUrl: './shell.component.html', styleUrl: './shell.component.scss', standalone: true, }) -export class ShellComponent { +export class ShellComponent implements OnInit { + protected breadcrumbs = signal([]); + protected navLinks = signal([]); + + protected title = computed(() => { + return this.breadcrumbs()[this.breadcrumbs().length - 1]?.label || 'Undefined'; + }); + + protected activeTopLevelUrl = computed(() => { + const breadcrumbs = this.breadcrumbs(); + + if (breadcrumbs.length === 0) { + return null; + } + + return this.breadcrumbs()[0]?.url; + }); + + constructor(private router: Router, private route: ActivatedRoute) { + + } + + ngOnInit() { + + this.initNavigation(); + this.initBreadcrumbs(); + + + this.router.events + .pipe(filter(event => event instanceof NavigationEnd)) + .subscribe(() => { + this.initBreadcrumbs(); + }); + } + + private initBreadcrumbs() { + this.breadcrumbs.set(this.buildBreadcrumbs(this.route.root)); + console.log('breadcrumbs', this.breadcrumbs()); + } + + private initNavigation() { + this.navLinks.set(this.extractNavLinks(this.router.config)); + console.log('navLinks', this.navLinks()) + } + + private extractNavLinks(routes: Route[]): Breadcrumb[] { + return routes + .filter(r => r.path === '') + .flatMap(r => r.children ?? []) + .filter(r => r.data?.['showInNav']) + .map(r => ({ + label: r.data?.['title'] || r.title || '', + url: '/' + (r.path ?? '') + })); + } + + private buildBreadcrumbs(route: ActivatedRoute, url: string = '/', breadcrumbs: Breadcrumb[] = []): Breadcrumb[] { + const children = route.children; + + if (children.length === 0) { + return breadcrumbs; + } + + const child = children[0]; + + const routeURL = child.snapshot.url.map(segment => segment.path).join('/'); + if (routeURL) { + url += `${routeURL}`; + } + + const label = + child.snapshot.data['title'] || + child.snapshot.routeConfig?.title || + ''; + + if (label) { + breadcrumbs.push({ label, url }); + } + + return this.buildBreadcrumbs(child, url, breadcrumbs); + } +} + +export class Breadcrumb { + label: string = 'undefined'; + url: string = ''; }