Add LinkComponent, integrate with Home and Shell components, and enhance navigation structure
This commit is contained in:
parent
f391db4fba
commit
40fde36bc6
@ -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"
|
||||
|
||||
@ -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: '**',
|
||||
|
||||
@ -1 +1,121 @@
|
||||
<app-link href="/dashboard/test">test</app-link>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
<p>home works!</p>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -0,0 +1 @@
|
||||
<a [href]="href()" (click)="click($event)"><ng-content></ng-content></a>
|
||||
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LinkComponent } from './link.component';
|
||||
|
||||
describe('LinkComponent', () => {
|
||||
let component: LinkComponent;
|
||||
let fixture: ComponentFixture<LinkComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LinkComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LinkComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -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<string>();
|
||||
public external = input<boolean>(false);
|
||||
|
||||
private router = inject(Router);
|
||||
|
||||
click($event: PointerEvent) {
|
||||
if (!this.external()) {
|
||||
$event.preventDefault();
|
||||
this.router.navigate([this.href()]).then();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
<div class="drawer-container">
|
||||
<ng-content select="drawer-content"></ng-content>
|
||||
</div>
|
||||
<div class="main-content-container">
|
||||
<div class="top-nav-container">
|
||||
<ng-content select="top-nav-container"></ng-content>
|
||||
</div>
|
||||
<div class="top-nav-breadcrumb-container">
|
||||
<ng-content select="top-nav-breadcrumb"></ng-content>
|
||||
</div>
|
||||
<div class="content-container">
|
||||
<router-outlet/>
|
||||
</div>
|
||||
</div>
|
||||
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DrawerComponent } from './drawer.component';
|
||||
|
||||
describe('DrawerComponent', () => {
|
||||
let component: DrawerComponent;
|
||||
let fixture: ComponentFixture<DrawerComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DrawerComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DrawerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -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 {
|
||||
|
||||
}
|
||||
@ -2,4 +2,4 @@
|
||||
<span>{{user()}}</span>
|
||||
<a href="" (click)="logout($event)">Logout</a>
|
||||
</div>
|
||||
<div class="profile-picture neutral-30"></div>
|
||||
<div class="profile-picture neutral-70"></div>
|
||||
|
||||
@ -15,5 +15,6 @@
|
||||
height: 3rem;
|
||||
overflow: hidden;
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,36 @@
|
||||
<app-panel class="shell-nav-bar neutral-20">
|
||||
<app-drawer>
|
||||
<div class="drawer-content" ngProjectAs="drawer-content">
|
||||
|
||||
<app-icon path="/full_logo.svg" class="logo primary-icon-50"></app-icon>
|
||||
|
||||
<div class="nav-links">
|
||||
@for (link of navLinks(); track link.label) {
|
||||
<app-link [href]="link.url">
|
||||
<span [ngClass]="{'active': activeTopLevelUrl() === link.url}" class="nav-link">{{link.label}}</span>
|
||||
</app-link>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="top-nav-container" ngProjectAs="top-nav-container">
|
||||
<span class="title">{{ title() }}</span>
|
||||
<app-nav-profile></app-nav-profile>
|
||||
</app-panel>
|
||||
</div>
|
||||
<div class="top-nav-breadcrumb" ngProjectAs="top-nav-breadcrumb">
|
||||
|
||||
<span> / </span>
|
||||
|
||||
@for (crumb of breadcrumbs(); let last = $last; track crumb.label) {
|
||||
|
||||
@if (crumb.url && !last) {
|
||||
<a [routerLink]="crumb.url">{{ crumb.label }}</a>
|
||||
} @else {
|
||||
<span>{{ crumb.label }}</span>
|
||||
}
|
||||
@if (!last) {
|
||||
<span> / </span>
|
||||
}
|
||||
}
|
||||
|
||||
</div>
|
||||
</app-drawer>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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<Breadcrumb[]>([]);
|
||||
protected navLinks = signal<Breadcrumb[]>([]);
|
||||
|
||||
protected title = computed<string>(() => {
|
||||
return this.breadcrumbs()[this.breadcrumbs().length - 1]?.label || 'Undefined';
|
||||
});
|
||||
|
||||
protected activeTopLevelUrl = computed<string | null>(() => {
|
||||
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 = '';
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user