From 47edd92577abe75d3398d182f73a3b98e85eff0f Mon Sep 17 00:00:00 2001 From: PhantomDave Date: Mon, 24 Nov 2025 15:43:13 +0000 Subject: [PATCH 1/2] feat: Refactor routing and layout components for improved navigation and structure --- frontend/src/app/app.routes.ts | 55 ++++-- .../side-nav-component/side-nav-component.css | 126 +++++++++---- .../side-nav-component.html | 165 ++++++++++-------- .../side-nav-component/side-nav-component.ts | 67 +++++-- .../breadcrumb/breadcrumb.component.css | 0 .../breadcrumb/breadcrumb.component.html | 16 ++ .../breadcrumb/breadcrumb.component.ts | 62 +++++++ .../analytics-layout.component.ts | 9 + .../home-layout/home-layout.component.ts | 9 + .../movements-layout.component.ts | 9 + 10 files changed, 388 insertions(+), 130 deletions(-) create mode 100644 frontend/src/app/components/ui-library/breadcrumb/breadcrumb.component.css create mode 100644 frontend/src/app/components/ui-library/breadcrumb/breadcrumb.component.html create mode 100644 frontend/src/app/components/ui-library/breadcrumb/breadcrumb.component.ts create mode 100644 frontend/src/app/layouts/analytics-layout/analytics-layout.component.ts create mode 100644 frontend/src/app/layouts/home-layout/home-layout.component.ts create mode 100644 frontend/src/app/layouts/movements-layout/movements-layout.component.ts diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index c2a2a4d..364dfd7 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,6 +1,5 @@ import { Routes } from '@angular/router'; -import { HomeComponent } from './components/home-component/home-component'; import { LoginComponent } from './components/welcome-layout/login-component/login-component'; import { RegisterComponent } from './components/welcome-layout/register-component/register-component'; import { authenticateGuard } from './guards/authenticate-guard'; @@ -9,20 +8,56 @@ import { SettingsComponent } from './components/settings-component/settings-comp import { ImportWizardComponent } from './components/import/import-wizard-component/import-wizard-component'; import { MonthlyComparisonComponent } from './components/analytics/monthly-comparison-component/monthly-comparison-component'; import { TrackingComponent } from './components/tracking/tracking/tracking.component'; +import { DashboardComponent } from './components/dashboard-component/dashboard-component.component'; +import { HomeLayoutComponent } from './layouts/home-layout/home-layout.component'; +import { MovementsLayoutComponent } from './layouts/movements-layout/movements-layout.component'; +import { AnalyticsLayoutComponent } from './layouts/analytics-layout/analytics-layout.component'; export const routes: Routes = [ { path: '', redirectTo: 'login', pathMatch: 'full' }, - { path: 'login', component: LoginComponent }, - { path: 'register', component: RegisterComponent }, - { path: 'home', component: HomeComponent, canActivate: [authenticateGuard] }, - { path: 'movements', component: TrackingComponent, canActivate: [authenticateGuard] }, - { path: 'balance', component: BalanceComponent, canActivate: [authenticateGuard] }, - { path: 'settings', component: SettingsComponent, canActivate: [authenticateGuard] }, - { path: 'import', component: ImportWizardComponent, canActivate: [authenticateGuard] }, + { path: 'login', component: LoginComponent, data: { breadcrumb: 'Login' } }, + { path: 'register', component: RegisterComponent, data: { breadcrumb: 'Register' } }, { - path: 'analytics/monthly-comparison', - component: MonthlyComparisonComponent, + path: 'home', + component: HomeLayoutComponent, canActivate: [authenticateGuard], + data: { breadcrumb: 'Home' }, + children: [ + { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, + { path: 'dashboard', component: DashboardComponent, data: { breadcrumb: 'Dashboard' } }, + { path: 'balance', component: BalanceComponent, data: { breadcrumb: 'Balance' } }, + ], + }, + { + path: 'movements', + component: MovementsLayoutComponent, + canActivate: [authenticateGuard], + data: { breadcrumb: 'Movements' }, + children: [ + { path: '', redirectTo: 'tracking', pathMatch: 'full' }, + { path: 'tracking', component: TrackingComponent, data: { breadcrumb: 'Tracking' } }, + { path: 'import', component: ImportWizardComponent, data: { breadcrumb: 'Import' } }, + ], + }, + { + path: 'analytics', + component: AnalyticsLayoutComponent, + canActivate: [authenticateGuard], + data: { breadcrumb: 'Analytics' }, + children: [ + { path: '', redirectTo: 'monthly-comparison', pathMatch: 'full' }, + { + path: 'monthly-comparison', + component: MonthlyComparisonComponent, + data: { breadcrumb: 'Monthly Comparison' }, + }, + ], + }, + { + path: 'settings', + component: SettingsComponent, + canActivate: [authenticateGuard], + data: { breadcrumb: 'Settings' }, }, { path: '**', redirectTo: 'login' }, ]; diff --git a/frontend/src/app/components/side-nav-component/side-nav-component.css b/frontend/src/app/components/side-nav-component/side-nav-component.css index b3673c0..02bdc6e 100644 --- a/frontend/src/app/components/side-nav-component/side-nav-component.css +++ b/frontend/src/app/components/side-nav-component/side-nav-component.css @@ -7,66 +7,126 @@ height: 100%; } -.mat-list { - align-items: center; -} - .sidenav { - width: 13dvw; - background-color: var(--mat-sys-surface-container); - color: var(--mat-sys-on-surface); + width: 280px; border-right: 1px solid var(--mat-sys-outline-variant); } .sidenav-header { + position: sticky; + top: 0; + z-index: 2; + background-color: var(--mat-sys-surface); + border-bottom: 1px solid var(--mat-sys-outline-variant); + min-height: 64px; + padding: 0 8px; +} + +.app-title { + font-size: 20px; + font-weight: 500; + margin-left: 8px; +} + +.sidenav-content { + display: flex; + flex-direction: column; + height: calc(100% - 64px); +} + +nav { + flex: 1; + overflow-y: auto; +} + +.nav-accordion { + display: block; +} + +.nav-panel { + box-shadow: none !important; + border-radius: 0 !important; +} + +.nav-panel .mat-expansion-panel-header { + padding: 0 16px; + height: 48px; +} + +.nav-panel .mat-expansion-panel-body { + padding: 0; +} + +.panel-title { display: flex; align-items: center; - gap: 8px; - padding: 12px 16px; - color: var(--mat-sys-on-surface); + gap: 12px; } -.app-title { - font-weight: 600; - letter-spacing: 0.2px; +.panel-icon { + font-size: 24px; + width: 24px; + height: 24px; } -.nav-item { - color: var(--mat-sys-on-surface); +mat-nav-list { + padding: 0; } -.nav-item [matListItemLine] { - color: var(--mat-sys-on-surface-variant); +mat-nav-list a[mat-list-item] { + height: 40px; } -.nav-item[aria-current='page'], -.nav-item.active { +.active-link { background-color: var(--mat-sys-secondary-container); color: var(--mat-sys-on-secondary-container); } -.nav-item[aria-current='page'] [matListItemLine], -.nav-item.active [matListItemLine] { +.active-link mat-icon { color: var(--mat-sys-on-secondary-container); - opacity: 0.9; } -.nav-item:hover { - background-color: var(--mat-sys-surface-container-high); +mat-divider { + margin: 8px 0; } -.nav-item:focus-visible { - outline: 2px solid var(--mat-sys-outline); - outline-offset: -2px; +.account-actions { + padding: 8px 0; } -.content { - padding: 16px; +.account-actions a[mat-list-item] { + cursor: pointer; + padding: 0 16px; +} + +.sidenav-main-content { + display: flex; + flex-direction: column; height: 100%; - overflow: auto; - box-sizing: border-box; } -.mat-mdc-list-base { - width: 100%; +.mobile-toolbar { + position: sticky; + top: 0; + z-index: 1; + background-color: var(--mat-sys-surface); + border-bottom: 1px solid var(--mat-sys-outline-variant); +} + +.content-wrapper { + padding: 16px; + flex: 1; + overflow-y: auto; +} + +.breadcrumb { + margin-bottom: 8px; +} + +.breadcrumb.closed { + margin-left: 0; +} + +.breadcrumb.open { + margin-bottom: 16px; } diff --git a/frontend/src/app/components/side-nav-component/side-nav-component.html b/frontend/src/app/components/side-nav-component/side-nav-component.html index 5ca4686..2835caa 100644 --- a/frontend/src/app/components/side-nav-component/side-nav-component.html +++ b/frontend/src/app/components/side-nav-component/side-nav-component.html @@ -1,96 +1,109 @@ - - -
-
- - Bank Tracker -
+ + + + Bank Tracker + - - - + + + @if (themeService.isDarkMode()) { light_mode @@ -109,25 +122,24 @@ settings Settings - + logout Logout - +
- + @if (!drawer.opened) { -
+ -
+ + } - - +
+ @if (drawer.opened) { + + } + +
diff --git a/frontend/src/app/components/side-nav-component/side-nav-component.ts b/frontend/src/app/components/side-nav-component/side-nav-component.ts index f8c1865..8993cbf 100644 --- a/frontend/src/app/components/side-nav-component/side-nav-component.ts +++ b/frontend/src/app/components/side-nav-component/side-nav-component.ts @@ -1,19 +1,22 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; import { MatIconButton } from '@angular/material/button'; import { MatIcon } from '@angular/material/icon'; -import { - MatListItem, - MatListItemIcon, - MatListItemLine, - MatListItemTitle, - MatNavList, -} from '@angular/material/list'; +import { MatListItem, MatListItemIcon, MatListItemTitle, MatNavList } from '@angular/material/list'; import { MatSidenav, MatSidenavContainer, MatSidenavContent } from '@angular/material/sidenav'; -import { Router, RouterLink, RouterLinkActive } from '@angular/router'; +import { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router'; +import { + MatAccordion, + MatExpansionPanel, + MatExpansionPanelHeader, + MatExpansionPanelTitle, +} from '@angular/material/expansion'; +import { MatToolbar } from '@angular/material/toolbar'; +import { MatDivider } from '@angular/material/divider'; +import { filter } from 'rxjs'; import { AccountService } from '../../models/account/account-service'; import { ThemeService } from '../../services/theme.service'; -import { FlexComponent } from '../ui-library/flex-component/flex-component'; +import { BreadcrumbComponent } from '../ui-library/breadcrumb/breadcrumb.component'; @Component({ selector: 'app-side-nav-component', @@ -25,22 +28,60 @@ import { FlexComponent } from '../ui-library/flex-component/flex-component'; MatListItem, MatListItemIcon, MatListItemTitle, - MatListItemLine, MatIcon, MatIconButton, RouterLink, RouterLinkActive, - FlexComponent, + BreadcrumbComponent, + MatAccordion, + MatExpansionPanel, + MatExpansionPanelHeader, + MatExpansionPanelTitle, + MatToolbar, + MatDivider, ], templateUrl: './side-nav-component.html', styleUrl: './side-nav-component.css', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SideNavComponent { +export class SideNavComponent implements OnInit { private readonly accountService = inject(AccountService); private readonly router = inject(Router); protected readonly themeService = inject(ThemeService); + // Track which panels are expanded + protected readonly homeExpanded = signal(false); + protected readonly movementsExpanded = signal(false); + protected readonly analyticsExpanded = signal(false); + + ngOnInit(): void { + // Set initial state based on current route + this.updateExpandedPanels(this.router.url); + + // Listen to route changes and update expanded panels + this.router.events + .pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd)) + .subscribe((event: NavigationEnd) => { + this.updateExpandedPanels(event.urlAfterRedirects); + }); + } + + private updateExpandedPanels(url: string): void { + // Reset all panels + this.homeExpanded.set(false); + this.movementsExpanded.set(false); + this.analyticsExpanded.set(false); + + // Expand the appropriate panel based on the current route + if (url.startsWith('/home')) { + this.homeExpanded.set(true); + } else if (url.startsWith('/movements')) { + this.movementsExpanded.set(true); + } else if (url.startsWith('/analytics')) { + this.analyticsExpanded.set(true); + } + } + protected async onLogout(): Promise { this.accountService.logout(); await this.router.navigate(['/login']); diff --git a/frontend/src/app/components/ui-library/breadcrumb/breadcrumb.component.css b/frontend/src/app/components/ui-library/breadcrumb/breadcrumb.component.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/components/ui-library/breadcrumb/breadcrumb.component.html b/frontend/src/app/components/ui-library/breadcrumb/breadcrumb.component.html new file mode 100644 index 0000000..b7c44b2 --- /dev/null +++ b/frontend/src/app/components/ui-library/breadcrumb/breadcrumb.component.html @@ -0,0 +1,16 @@ + + @for (crumb of breadcrumbs(); track crumb.url; let isLast = $last) { + @if (!isLast) { + + {{ + crumb.label + }} + + / + } @else { + + {{ crumb.label }} + + } + } + diff --git a/frontend/src/app/components/ui-library/breadcrumb/breadcrumb.component.ts b/frontend/src/app/components/ui-library/breadcrumb/breadcrumb.component.ts new file mode 100644 index 0000000..80aa071 --- /dev/null +++ b/frontend/src/app/components/ui-library/breadcrumb/breadcrumb.component.ts @@ -0,0 +1,62 @@ +import { Component, inject, signal } from '@angular/core'; +import { MatChipsModule } from '@angular/material/chips'; +import { FlexComponent } from '../flex-component/flex-component'; +import { Router, ActivatedRoute, NavigationEnd, RouterModule } from '@angular/router'; +import { filter } from 'rxjs'; + +interface Breadcrumb { + label: string; + url: string; +} + +@Component({ + selector: 'app-breadcrumb', + templateUrl: './breadcrumb.component.html', + styleUrls: ['./breadcrumb.component.css'], + imports: [MatChipsModule, FlexComponent, RouterModule], +}) +export class BreadcrumbComponent { + private router = inject(Router); + private activatedRoute = inject(ActivatedRoute); + + breadcrumbs = signal([]); + + constructor() { + // Update breadcrumbs on navigation + this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe(() => { + this.breadcrumbs.set(this.createBreadcrumbs(this.activatedRoute.root)); + }); + + // Initial load + this.breadcrumbs.set(this.createBreadcrumbs(this.activatedRoute.root)); + } + + private createBreadcrumbs( + route: ActivatedRoute, + url: string = '', + breadcrumbs: Breadcrumb[] = [], + ): Breadcrumb[] { + const children: ActivatedRoute[] = route.children; + + if (children.length === 0) { + return breadcrumbs; + } + + for (const child of children) { + const routeURL: string = child.snapshot.url.map((segment) => segment.path).join('/'); + + if (routeURL !== '') { + url += `/${routeURL}`; + } + + const label = child.snapshot.data['breadcrumb']; + if (label) { + breadcrumbs.push({ label, url }); + } + + return this.createBreadcrumbs(child, url, breadcrumbs); + } + + return breadcrumbs; + } +} diff --git a/frontend/src/app/layouts/analytics-layout/analytics-layout.component.ts b/frontend/src/app/layouts/analytics-layout/analytics-layout.component.ts new file mode 100644 index 0000000..a206ac7 --- /dev/null +++ b/frontend/src/app/layouts/analytics-layout/analytics-layout.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-analytics-layout', + imports: [RouterOutlet], + template: '', +}) +export class AnalyticsLayoutComponent {} diff --git a/frontend/src/app/layouts/home-layout/home-layout.component.ts b/frontend/src/app/layouts/home-layout/home-layout.component.ts new file mode 100644 index 0000000..1de1d9a --- /dev/null +++ b/frontend/src/app/layouts/home-layout/home-layout.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-home-layout', + imports: [RouterOutlet], + template: '', +}) +export class HomeLayoutComponent {} diff --git a/frontend/src/app/layouts/movements-layout/movements-layout.component.ts b/frontend/src/app/layouts/movements-layout/movements-layout.component.ts new file mode 100644 index 0000000..6ee29e7 --- /dev/null +++ b/frontend/src/app/layouts/movements-layout/movements-layout.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-movements-layout', + imports: [RouterOutlet], + template: '', +}) +export class MovementsLayoutComponent {} From 4fac87674b5f6bee19acc0879c0e335000c505f6 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:02:44 +0100 Subject: [PATCH 2/2] refactor: Remove duplicate layout components and fix memory leaks (#95) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: PhantomDave <34485699+PhantomDave@users.noreply.github.com> --- frontend/src/app/app.routes.ts | 6 ------ .../side-nav-component/side-nav-component.html | 4 ++-- .../side-nav-component/side-nav-component.ts | 12 ++++++++---- .../ui-library/breadcrumb/breadcrumb.component.ts | 12 +++++++++--- .../analytics-layout/analytics-layout.component.ts | 9 --------- .../app/layouts/home-layout/home-layout.component.ts | 9 --------- .../movements-layout/movements-layout.component.ts | 9 --------- 7 files changed, 19 insertions(+), 42 deletions(-) delete mode 100644 frontend/src/app/layouts/analytics-layout/analytics-layout.component.ts delete mode 100644 frontend/src/app/layouts/home-layout/home-layout.component.ts delete mode 100644 frontend/src/app/layouts/movements-layout/movements-layout.component.ts diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 364dfd7..3261775 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -9,9 +9,6 @@ import { ImportWizardComponent } from './components/import/import-wizard-compone import { MonthlyComparisonComponent } from './components/analytics/monthly-comparison-component/monthly-comparison-component'; import { TrackingComponent } from './components/tracking/tracking/tracking.component'; import { DashboardComponent } from './components/dashboard-component/dashboard-component.component'; -import { HomeLayoutComponent } from './layouts/home-layout/home-layout.component'; -import { MovementsLayoutComponent } from './layouts/movements-layout/movements-layout.component'; -import { AnalyticsLayoutComponent } from './layouts/analytics-layout/analytics-layout.component'; export const routes: Routes = [ { path: '', redirectTo: 'login', pathMatch: 'full' }, @@ -19,7 +16,6 @@ export const routes: Routes = [ { path: 'register', component: RegisterComponent, data: { breadcrumb: 'Register' } }, { path: 'home', - component: HomeLayoutComponent, canActivate: [authenticateGuard], data: { breadcrumb: 'Home' }, children: [ @@ -30,7 +26,6 @@ export const routes: Routes = [ }, { path: 'movements', - component: MovementsLayoutComponent, canActivate: [authenticateGuard], data: { breadcrumb: 'Movements' }, children: [ @@ -41,7 +36,6 @@ export const routes: Routes = [ }, { path: 'analytics', - component: AnalyticsLayoutComponent, canActivate: [authenticateGuard], data: { breadcrumb: 'Analytics' }, children: [ diff --git a/frontend/src/app/components/side-nav-component/side-nav-component.html b/frontend/src/app/components/side-nav-component/side-nav-component.html index 2835caa..ca11f05 100644 --- a/frontend/src/app/components/side-nav-component/side-nav-component.html +++ b/frontend/src/app/components/side-nav-component/side-nav-component.html @@ -103,7 +103,7 @@