Browse Source

Task/extract page tabs to dedicated component (#6797)

* Extract page tabs

* Update changelog
pull/1685/merge
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
ceea375227
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 29
      apps/client/src/app/pages/about/about-page.component.ts
  3. 31
      apps/client/src/app/pages/about/about-page.html
  4. 16
      apps/client/src/app/pages/admin/admin-page.component.ts
  5. 31
      apps/client/src/app/pages/admin/admin-page.html
  6. 26
      apps/client/src/app/pages/faq/faq-page.component.ts
  7. 31
      apps/client/src/app/pages/faq/faq-page.html
  8. 18
      apps/client/src/app/pages/home/home-page.component.ts
  9. 31
      apps/client/src/app/pages/home/home-page.html
  10. 26
      apps/client/src/app/pages/portfolio/portfolio-page.component.ts
  11. 31
      apps/client/src/app/pages/portfolio/portfolio-page.html
  12. 22
      apps/client/src/app/pages/resources/resources-page.component.ts
  13. 31
      apps/client/src/app/pages/resources/resources-page.html
  14. 29
      apps/client/src/app/pages/user-account/user-account-page.component.ts
  15. 31
      apps/client/src/app/pages/user-account/user-account-page.html
  16. 27
      apps/client/src/app/pages/zen/zen-page.component.ts
  17. 31
      apps/client/src/app/pages/zen/zen-page.html
  18. 49
      apps/client/src/styles.scss
  19. 27
      apps/client/src/styles/theme.scss
  20. 2
      libs/common/src/lib/interfaces/index.ts
  21. 2
      libs/ui/src/lib/page-tabs/index.ts
  22. 0
      libs/ui/src/lib/page-tabs/interfaces/interfaces.ts
  23. 30
      libs/ui/src/lib/page-tabs/page-tabs.component.html
  24. 70
      libs/ui/src/lib/page-tabs/page-tabs.component.scss
  25. 30
      libs/ui/src/lib/page-tabs/page-tabs.component.ts

4
CHANGELOG.md

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Changed
- Extracted the page tabs to a reusable component
### Fixed ### Fixed
- Resolved an issue with the cash balance calculation of an account for `SELL` activities to ensure fees are correctly subtracted - Resolved an issue with the cash balance calculation of an account for `SELL` activities to ensure fees are correctly subtracted

29
apps/client/src/app/pages/about/about-page.component.ts

@ -1,20 +1,15 @@
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { TabConfiguration, User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { publicRoutes } from '@ghostfolio/common/routes/routes';
import {
GfPageTabsComponent,
TabConfiguration
} from '@ghostfolio/ui/page-tabs';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core';
ChangeDetectorRef,
Component,
CUSTOM_ELEMENTS_SCHEMA,
DestroyRef,
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
documentTextOutline, documentTextOutline,
@ -24,18 +19,15 @@ import {
shieldCheckmarkOutline, shieldCheckmarkOutline,
sparklesOutline sparklesOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
@Component({ @Component({
host: { class: 'page has-tabs' }, host: { class: 'page has-tabs' },
imports: [IonIcon, MatTabsModule, RouterModule], imports: [GfPageTabsComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-about-page', selector: 'gf-about-page',
styleUrls: ['./about-page.scss'], styleUrls: ['./about-page.scss'],
templateUrl: './about-page.html' templateUrl: './about-page.html'
}) })
export class AboutPageComponent implements OnInit { export class AboutPageComponent {
public deviceType: string;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
public tabs: TabConfiguration[] = []; public tabs: TabConfiguration[] = [];
public user: User; public user: User;
@ -44,7 +36,6 @@ export class AboutPageComponent implements OnInit {
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private deviceDetectorService: DeviceDetectorService,
private userService: UserService private userService: UserService
) { ) {
const { globalPermissions } = this.dataService.fetchInfo(); const { globalPermissions } = this.dataService.fetchInfo();
@ -112,8 +103,4 @@ export class AboutPageComponent implements OnInit {
sparklesOutline sparklesOutline
}); });
} }
public ngOnInit() {
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
}
} }

31
apps/client/src/app/pages/about/about-page.html

@ -1,30 +1 @@
<mat-tab-nav-panel #tabPanel class="flex-grow-1 overflow-auto"> <gf-page-tabs [tabs]="tabs" />
<router-outlet />
</mat-tab-nav-panel>
<nav
mat-align-tabs="center"
mat-tab-nav-bar
[disablePagination]="true"
[tabPanel]="tabPanel"
>
@for (tab of tabs; track tab) {
@if (tab.showCondition !== false) {
<a
#rla="routerLinkActive"
class="no-min-width px-3"
mat-tab-link
routerLinkActive
[active]="rla.isActive"
[routerLink]="tab.routerLink"
[routerLinkActiveOptions]="{ exact: true }"
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a>
}
}
</nav>

16
apps/client/src/app/pages/admin/admin-page.component.ts

@ -1,10 +1,10 @@
import { TabConfiguration } from '@ghostfolio/common/interfaces';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import {
GfPageTabsComponent,
TabConfiguration
} from '@ghostfolio/ui/page-tabs';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
flashOutline, flashOutline,
@ -13,20 +13,18 @@ import {
serverOutline, serverOutline,
settingsOutline settingsOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
@Component({ @Component({
host: { class: 'page has-tabs' }, host: { class: 'page has-tabs' },
imports: [IonIcon, MatTabsModule, RouterModule], imports: [GfPageTabsComponent],
selector: 'gf-admin-page', selector: 'gf-admin-page',
styleUrls: ['./admin-page.scss'], styleUrls: ['./admin-page.scss'],
templateUrl: './admin-page.html' templateUrl: './admin-page.html'
}) })
export class AdminPageComponent implements OnInit { export class AdminPageComponent implements OnInit {
public deviceType: string;
public tabs: TabConfiguration[] = []; public tabs: TabConfiguration[] = [];
public constructor(private deviceDetectorService: DeviceDetectorService) { public constructor() {
addIcons({ addIcons({
flashOutline, flashOutline,
peopleOutline, peopleOutline,
@ -37,8 +35,6 @@ export class AdminPageComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
this.tabs = [ this.tabs = [
{ {
iconName: 'reader-outline', iconName: 'reader-outline',

31
apps/client/src/app/pages/admin/admin-page.html

@ -1,30 +1 @@
<mat-tab-nav-panel #tabPanel class="flex-grow-1 overflow-auto"> <gf-page-tabs [tabs]="tabs" />
<router-outlet />
</mat-tab-nav-panel>
<nav
mat-align-tabs="center"
mat-tab-nav-bar
[disablePagination]="true"
[tabPanel]="tabPanel"
>
@for (tab of tabs; track tab) {
@if (tab.showCondition !== false) {
<a
#rla="routerLinkActive"
class="no-min-width px-3"
mat-tab-link
routerLinkActive
[active]="rla.isActive"
[routerLink]="tab.routerLink"
[routerLinkActiveOptions]="{ exact: true }"
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2" [innerHTML]="tab.label"></div>
</a>
}
}
</nav>

26
apps/client/src/app/pages/faq/faq-page.component.ts

@ -1,33 +1,27 @@
import { TabConfiguration } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { publicRoutes } from '@ghostfolio/common/routes/routes';
import {
GfPageTabsComponent,
TabConfiguration
} from '@ghostfolio/ui/page-tabs';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { CUSTOM_ELEMENTS_SCHEMA, Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { cloudyOutline, readerOutline, serverOutline } from 'ionicons/icons'; import { cloudyOutline, readerOutline, serverOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
@Component({ @Component({
host: { class: 'page has-tabs' }, host: { class: 'page has-tabs' },
imports: [IonIcon, MatTabsModule, RouterModule], imports: [GfPageTabsComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-faq-page', selector: 'gf-faq-page',
styleUrls: ['./faq-page.scss'], styleUrls: ['./faq-page.scss'],
templateUrl: './faq-page.html' templateUrl: './faq-page.html'
}) })
export class GfFaqPageComponent implements OnInit { export class GfFaqPageComponent {
public deviceType: string;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
public tabs: TabConfiguration[] = []; public tabs: TabConfiguration[] = [];
public constructor( public constructor(private dataService: DataService) {
private dataService: DataService,
private deviceDetectorService: DeviceDetectorService
) {
const { globalPermissions } = this.dataService.fetchInfo(); const { globalPermissions } = this.dataService.fetchInfo();
this.hasPermissionForSubscription = hasPermission( this.hasPermissionForSubscription = hasPermission(
@ -56,8 +50,4 @@ export class GfFaqPageComponent implements OnInit {
addIcons({ cloudyOutline, readerOutline, serverOutline }); addIcons({ cloudyOutline, readerOutline, serverOutline });
} }
public ngOnInit() {
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
}
} }

31
apps/client/src/app/pages/faq/faq-page.html

@ -1,30 +1 @@
<mat-tab-nav-panel #tabPanel class="flex-grow-1 overflow-auto"> <gf-page-tabs [tabs]="tabs" />
<router-outlet />
</mat-tab-nav-panel>
<nav
mat-align-tabs="center"
mat-tab-nav-bar
[disablePagination]="true"
[tabPanel]="tabPanel"
>
@for (tab of tabs; track tab) {
@if (tab.showCondition !== false) {
<a
#rla="routerLinkActive"
class="no-min-width px-3"
mat-tab-link
routerLinkActive
[active]="rla.isActive"
[routerLink]="tab.routerLink"
[routerLinkActiveOptions]="{ exact: true }"
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a>
}
}
</nav>

18
apps/client/src/app/pages/home/home-page.component.ts

@ -1,20 +1,20 @@
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { TabConfiguration, User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import {
GfPageTabsComponent,
TabConfiguration
} from '@ghostfolio/ui/page-tabs';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
CUSTOM_ELEMENTS_SCHEMA,
DestroyRef, DestroyRef,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
albumsOutline, albumsOutline,
@ -23,18 +23,15 @@ import {
newspaperOutline, newspaperOutline,
readerOutline readerOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
@Component({ @Component({
host: { class: 'page has-tabs' }, host: { class: 'page has-tabs' },
imports: [IonIcon, MatTabsModule, RouterModule], imports: [GfPageTabsComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-home-page', selector: 'gf-home-page',
styleUrls: ['./home-page.scss'], styleUrls: ['./home-page.scss'],
templateUrl: './home-page.html' templateUrl: './home-page.html'
}) })
export class GfHomePageComponent implements OnInit { export class GfHomePageComponent implements OnInit {
public deviceType: string;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public tabs: TabConfiguration[] = []; public tabs: TabConfiguration[] = [];
public user: User; public user: User;
@ -42,7 +39,6 @@ export class GfHomePageComponent implements OnInit {
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private deviceDetectorService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private userService: UserService private userService: UserService
) { ) {
@ -104,8 +100,6 @@ export class GfHomePageComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))

31
apps/client/src/app/pages/home/home-page.html

@ -1,30 +1 @@
<mat-tab-nav-panel #tabPanel class="flex-grow-1 overflow-auto"> <gf-page-tabs [tabs]="tabs" />
<router-outlet />
</mat-tab-nav-panel>
<nav
mat-align-tabs="center"
mat-tab-nav-bar
[disablePagination]="true"
[tabPanel]="tabPanel"
>
@for (tab of tabs; track tab) {
@if (tab.showCondition !== false) {
<a
#rla="routerLinkActive"
class="no-min-width px-3"
mat-tab-link
routerLinkActive
[active]="rla.isActive"
[routerLink]="tab.routerLink"
[routerLinkActiveOptions]="{ exact: true }"
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a>
}
}
</nav>

26
apps/client/src/app/pages/portfolio/portfolio-page.component.ts

@ -1,16 +1,13 @@
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { TabConfiguration, User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { import {
ChangeDetectorRef, GfPageTabsComponent,
Component, TabConfiguration
DestroyRef, } from '@ghostfolio/ui/page-tabs';
OnInit
} from '@angular/core'; import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
analyticsOutline, analyticsOutline,
@ -19,24 +16,21 @@ import {
scanOutline, scanOutline,
swapVerticalOutline swapVerticalOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
@Component({ @Component({
host: { class: 'page has-tabs' }, host: { class: 'page has-tabs' },
imports: [MatTabsModule, RouterModule], imports: [GfPageTabsComponent],
selector: 'gf-portfolio-page', selector: 'gf-portfolio-page',
styleUrls: ['./portfolio-page.scss'], styleUrls: ['./portfolio-page.scss'],
templateUrl: './portfolio-page.html' templateUrl: './portfolio-page.html'
}) })
export class PortfolioPageComponent implements OnInit { export class PortfolioPageComponent {
public deviceType: string;
public tabs: TabConfiguration[] = []; public tabs: TabConfiguration[] = [];
public user: User; public user: User;
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private deviceDetectorService: DeviceDetectorService,
private userService: UserService private userService: UserService
) { ) {
this.userService.stateChanged this.userService.stateChanged
@ -86,8 +80,4 @@ export class PortfolioPageComponent implements OnInit {
swapVerticalOutline swapVerticalOutline
}); });
} }
public ngOnInit() {
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
}
} }

31
apps/client/src/app/pages/portfolio/portfolio-page.html

@ -1,30 +1 @@
<mat-tab-nav-panel #tabPanel class="flex-grow-1 overflow-auto"> <gf-page-tabs [tabs]="tabs" />
<router-outlet />
</mat-tab-nav-panel>
<nav
mat-align-tabs="center"
mat-tab-nav-bar
[disablePagination]="true"
[tabPanel]="tabPanel"
>
@for (tab of tabs; track tab) {
@if (tab.showCondition !== false) {
<a
#rla="routerLinkActive"
class="no-min-width px-3"
mat-tab-link
routerLinkActive
[active]="rla.isActive"
[routerLink]="tab.routerLink"
[routerLinkActiveOptions]="{ exact: true }"
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a>
}
}
</nav>

22
apps/client/src/app/pages/resources/resources-page.component.ts

@ -1,10 +1,10 @@
import { TabConfiguration } from '@ghostfolio/common/interfaces';
import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { publicRoutes } from '@ghostfolio/common/routes/routes';
import {
GfPageTabsComponent,
TabConfiguration
} from '@ghostfolio/ui/page-tabs';
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
bookOutline, bookOutline,
@ -12,17 +12,15 @@ import {
newspaperOutline, newspaperOutline,
readerOutline readerOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
@Component({ @Component({
host: { class: 'page has-tabs' }, host: { class: 'page has-tabs' },
imports: [IonIcon, MatTabsModule, RouterModule], imports: [GfPageTabsComponent],
selector: 'gf-resources-page', selector: 'gf-resources-page',
styleUrls: ['./resources-page.scss'], styleUrls: ['./resources-page.scss'],
templateUrl: './resources-page.html' templateUrl: './resources-page.html'
}) })
export class ResourcesPageComponent implements OnInit { export class ResourcesPageComponent {
public deviceType: string;
public tabs: TabConfiguration[] = [ public tabs: TabConfiguration[] = [
{ {
iconName: 'reader-outline', iconName: 'reader-outline',
@ -46,11 +44,7 @@ export class ResourcesPageComponent implements OnInit {
} }
]; ];
public constructor(private deviceDetectorService: DeviceDetectorService) { public constructor() {
addIcons({ bookOutline, libraryOutline, newspaperOutline, readerOutline }); addIcons({ bookOutline, libraryOutline, newspaperOutline, readerOutline });
} }
public ngOnInit() {
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
}
} }

31
apps/client/src/app/pages/resources/resources-page.html

@ -1,30 +1 @@
<mat-tab-nav-panel #tabPanel class="flex-grow-1 overflow-auto"> <gf-page-tabs [tabs]="tabs" />
<router-outlet />
</mat-tab-nav-panel>
<nav
mat-align-tabs="center"
mat-tab-nav-bar
[disablePagination]="true"
[tabPanel]="tabPanel"
>
@for (tab of tabs; track tab) {
@if (tab.showCondition !== false) {
<a
#rla="routerLinkActive"
class="no-min-width px-3"
mat-tab-link
routerLinkActive
[active]="rla.isActive"
[routerLink]="tab.routerLink"
[routerLinkActiveOptions]="{ exact: true }"
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a>
}
}
</nav>

29
apps/client/src/app/pages/user-account/user-account-page.component.ts

@ -1,39 +1,30 @@
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { TabConfiguration, User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { import {
ChangeDetectorRef, GfPageTabsComponent,
Component, TabConfiguration
CUSTOM_ELEMENTS_SCHEMA, } from '@ghostfolio/ui/page-tabs';
DestroyRef,
OnInit import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core';
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { diamondOutline, keyOutline, settingsOutline } from 'ionicons/icons'; import { diamondOutline, keyOutline, settingsOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
@Component({ @Component({
host: { class: 'page has-tabs' }, host: { class: 'page has-tabs' },
imports: [IonIcon, MatTabsModule, RouterModule], imports: [GfPageTabsComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-user-account-page', selector: 'gf-user-account-page',
styleUrls: ['./user-account-page.scss'], styleUrls: ['./user-account-page.scss'],
templateUrl: './user-account-page.html' templateUrl: './user-account-page.html'
}) })
export class GfUserAccountPageComponent implements OnInit { export class GfUserAccountPageComponent {
public deviceType: string;
public tabs: TabConfiguration[] = []; public tabs: TabConfiguration[] = [];
public user: User; public user: User;
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private deviceDetectorService: DeviceDetectorService,
private userService: UserService private userService: UserService
) { ) {
this.userService.stateChanged this.userService.stateChanged
@ -68,8 +59,4 @@ export class GfUserAccountPageComponent implements OnInit {
addIcons({ diamondOutline, keyOutline, settingsOutline }); addIcons({ diamondOutline, keyOutline, settingsOutline });
} }
public ngOnInit() {
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
}
} }

31
apps/client/src/app/pages/user-account/user-account-page.html

@ -1,30 +1 @@
<mat-tab-nav-panel #tabPanel class="flex-grow-1 overflow-auto"> <gf-page-tabs [tabs]="tabs" />
<router-outlet />
</mat-tab-nav-panel>
<nav
mat-align-tabs="center"
mat-tab-nav-bar
[disablePagination]="true"
[tabPanel]="tabPanel"
>
@for (tab of tabs; track tab) {
@if (tab.showCondition !== false) {
<a
#rla="routerLinkActive"
class="no-min-width px-3"
mat-tab-link
routerLinkActive
[active]="rla.isActive"
[routerLink]="tab.routerLink"
[routerLinkActiveOptions]="{ exact: true }"
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a>
}
}
</nav>

27
apps/client/src/app/pages/zen/zen-page.component.ts

@ -1,37 +1,30 @@
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { TabConfiguration, User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { import {
ChangeDetectorRef, GfPageTabsComponent,
Component, TabConfiguration
DestroyRef, } from '@ghostfolio/ui/page-tabs';
OnInit
} from '@angular/core'; import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { albumsOutline, analyticsOutline } from 'ionicons/icons'; import { albumsOutline, analyticsOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
@Component({ @Component({
host: { class: 'page has-tabs' }, host: { class: 'page has-tabs' },
imports: [IonIcon, MatTabsModule, RouterModule], imports: [GfPageTabsComponent],
selector: 'gf-zen-page', selector: 'gf-zen-page',
styleUrls: ['./zen-page.scss'], styleUrls: ['./zen-page.scss'],
templateUrl: './zen-page.html' templateUrl: './zen-page.html'
}) })
export class GfZenPageComponent implements OnInit { export class GfZenPageComponent {
public deviceType: string;
public tabs: TabConfiguration[] = []; public tabs: TabConfiguration[] = [];
public user: User; public user: User;
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private deviceDetectorService: DeviceDetectorService,
private userService: UserService private userService: UserService
) { ) {
this.userService.stateChanged this.userService.stateChanged
@ -58,8 +51,4 @@ export class GfZenPageComponent implements OnInit {
addIcons({ albumsOutline, analyticsOutline }); addIcons({ albumsOutline, analyticsOutline });
} }
public ngOnInit() {
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
}
} }

31
apps/client/src/app/pages/zen/zen-page.html

@ -1,30 +1 @@
<mat-tab-nav-panel #tabPanel class="flex-grow-1 overflow-auto"> <gf-page-tabs [tabs]="tabs" />
<router-outlet />
</mat-tab-nav-panel>
<nav
mat-align-tabs="center"
mat-tab-nav-bar
[disablePagination]="true"
[tabPanel]="tabPanel"
>
@for (tab of tabs; track tab) {
@if (tab.showCondition !== false) {
<a
#rla="routerLinkActive"
class="no-min-width px-3"
mat-tab-link
routerLinkActive
[active]="rla.isActive"
[routerLink]="tab.routerLink"
[routerLinkActiveOptions]="{ exact: true }"
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a>
}
}
</nav>

49
apps/client/src/styles.scss

@ -265,16 +265,6 @@ body {
} }
} }
.page {
&.has-tabs {
@media (min-width: 576px) {
.mat-mdc-tab-header {
background-color: rgba(var(--palette-foreground-base-dark), 0.02);
}
}
}
}
.svgMap-tooltip { .svgMap-tooltip {
background: var(--dark-background); background: var(--dark-background);
@ -489,7 +479,6 @@ ngx-skeleton-loader {
.page { .page {
display: flex; display: flex;
flex-direction: column;
overflow-y: auto; overflow-y: auto;
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
padding-bottom: constant(safe-area-inset-bottom); padding-bottom: constant(safe-area-inset-bottom);
@ -506,44 +495,6 @@ ngx-skeleton-loader {
padding: 2rem 0; padding: 2rem 0;
} }
} }
&.has-tabs {
height: calc(100svh - var(--mat-toolbar-standard-height));
.fab-container {
@media (max-width: 575.98px) {
bottom: 5rem;
}
}
.mat-mdc-tab-nav-panel {
padding: 2rem 0;
}
@include mat.tabs-overrides(
(
divider-height: 0
)
);
@media (min-width: 576px) {
flex-direction: row-reverse;
.mat-mdc-tab-header {
background-color: rgba(var(--palette-foreground-base), 0.02);
padding: 2rem 0;
width: 14rem;
.mat-mdc-tab-links {
flex-direction: column;
.mat-mdc-tab-link {
justify-content: flex-start;
}
}
}
}
}
} }
.svgMap-tooltip { .svgMap-tooltip {

27
apps/client/src/styles/theme.scss

@ -293,24 +293,6 @@ $gf-typography: (
container-shape: 4px container-shape: 4px
) )
); );
.page.has-tabs {
@include mat.tabs-overrides(
(
container-height: 3rem
)
);
}
}
@media (min-width: 576px) {
.page.has-tabs {
@include mat.tabs-overrides(
(
container-height: 2rem
)
);
}
} }
@include mat.badge-overrides( @include mat.badge-overrides(
@ -436,15 +418,6 @@ $gf-typography: (
) )
); );
} }
.page.has-tabs {
@include mat.tabs-overrides(
(
active-indicator-height: 0,
label-text-tracking: normal
)
);
}
} }
:root { :root {

2
libs/common/src/lib/interfaces/index.ts

@ -91,7 +91,6 @@ import type { SubscriptionOffer } from './subscription-offer.interface';
import type { SymbolItem } from './symbol-item.interface'; import type { SymbolItem } from './symbol-item.interface';
import type { SymbolMetrics } from './symbol-metrics.interface'; import type { SymbolMetrics } from './symbol-metrics.interface';
import type { SystemMessage } from './system-message.interface'; import type { SystemMessage } from './system-message.interface';
import type { TabConfiguration } from './tab-configuration.interface';
import type { ToggleOption } from './toggle-option.interface'; import type { ToggleOption } from './toggle-option.interface';
import type { UserItem } from './user-item.interface'; import type { UserItem } from './user-item.interface';
import type { UserSettings } from './user-settings.interface'; import type { UserSettings } from './user-settings.interface';
@ -186,7 +185,6 @@ export {
SymbolItem, SymbolItem,
SymbolMetrics, SymbolMetrics,
SystemMessage, SystemMessage,
TabConfiguration,
ToggleOption, ToggleOption,
User, User,
UserItem, UserItem,

2
libs/ui/src/lib/page-tabs/index.ts

@ -0,0 +1,2 @@
export * from './interfaces/interfaces';
export * from './page-tabs.component';

0
libs/common/src/lib/interfaces/tab-configuration.interface.ts → libs/ui/src/lib/page-tabs/interfaces/interfaces.ts

30
libs/ui/src/lib/page-tabs/page-tabs.component.html

@ -0,0 +1,30 @@
<mat-tab-nav-panel #tabPanel class="flex-grow-1 overflow-auto">
<router-outlet />
</mat-tab-nav-panel>
<nav
mat-align-tabs="center"
mat-tab-nav-bar
[disablePagination]="true"
[tabPanel]="tabPanel"
>
@for (tab of tabs(); track tab) {
@if (tab.showCondition !== false) {
<a
#rla="routerLinkActive"
class="no-min-width px-3"
mat-tab-link
routerLinkActive
[active]="rla.isActive"
[routerLink]="tab.routerLink"
[routerLinkActiveOptions]="{ exact: true }"
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2" [innerHTML]="tab.label"></div>
</a>
}
}
</nav>

70
libs/ui/src/lib/page-tabs/page-tabs.component.scss

@ -0,0 +1,70 @@
@use '@angular/material' as mat;
:host {
display: flex;
flex-direction: column;
height: calc(100svh - var(--mat-toolbar-standard-height));
width: 100%;
@include mat.tabs-overrides(
(
active-indicator-height: 0,
divider-height: 0,
label-text-tracking: normal
)
);
.fab-container {
@media (max-width: 575.98px) {
bottom: 5rem;
}
}
::ng-deep {
.mat-mdc-tab-nav-panel {
padding: 2rem 0;
}
}
@media (max-width: 575.98px) {
@include mat.tabs-overrides(
(
container-height: 3rem
)
);
}
@media (min-width: 576px) {
flex-direction: row-reverse;
@include mat.tabs-overrides(
(
container-height: 2rem
)
);
::ng-deep {
.mat-mdc-tab-header {
background-color: rgba(var(--palette-foreground-base), 0.02);
padding: 2rem 0;
width: 14rem;
.mat-mdc-tab-links {
flex-direction: column;
.mat-mdc-tab-link {
justify-content: flex-start;
}
}
}
}
}
}
:host-context(.theme-dark) {
@media (min-width: 576px) {
.mat-mdc-tab-header {
background-color: rgba(var(--palette-foreground-base-dark), 0.02);
}
}
}

30
libs/ui/src/lib/page-tabs/page-tabs.component.ts

@ -0,0 +1,30 @@
import {
ChangeDetectionStrategy,
Component,
inject,
input
} from '@angular/core';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { DeviceDetectorService } from 'ngx-device-detector';
import { TabConfiguration } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [IonIcon, MatTabsModule, RouterModule],
selector: 'gf-page-tabs',
styleUrls: ['./page-tabs.component.scss'],
templateUrl: './page-tabs.component.html'
})
export class GfPageTabsComponent {
public deviceType: string;
public readonly tabs = input.required<TabConfiguration[]>();
private readonly deviceService = inject(DeviceDetectorService);
public constructor() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
}
}
Loading…
Cancel
Save