Feature/introduce tabs with routing to home page (#495)

* Introduce tabs with routing

* Update changelog
pull/496/head
Thomas Kaul 3 years ago committed by GitHub
parent a24a094407
commit 2f402c0c8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,7 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added tabs to the admin control panel
- Added tabs with routing to the admin control panel
### Changed
- Introduced tabs with routing to the home page
## 1.81.0 - 27.11.2021

@ -0,0 +1,84 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service';
import {
RANGE,
SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Position, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'gf-home-holdings',
styleUrls: ['./home-holdings.scss'],
templateUrl: './home-holdings.html'
})
export class HomeHoldingsComponent implements OnDestroy, OnInit {
public dateRange: DateRange;
public deviceType: string;
public hasPermissionToCreateOrder: boolean;
public positions: Position[];
public user: User;
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService,
private settingsStorageService: SettingsStorageService,
private userService: UserService
) {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.hasPermissionToCreateOrder = hasPermission(
this.user.permissions,
permissions.createOrder
);
this.changeDetectorRef.markForCheck();
}
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.dateRange =
<DateRange>this.settingsStorageService.getSetting(RANGE) || 'max';
this.update();
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private update() {
this.dataService
.fetchPositions({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.positions = response.positions;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
}
}

@ -0,0 +1,26 @@
<div class="container justify-content-center pb-3 px-3">
<div class="row">
<div class="align-items-center col-xs-12 col-md-8 offset-md-2">
<mat-card class="p-0">
<mat-card-content>
<gf-positions
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[locale]="user?.settings?.locale"
[positions]="positions"
[range]="dateRange"
></gf-positions>
</mat-card-content>
</mat-card>
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
<a
class="mt-3"
i18n
mat-button
[routerLink]="['/portfolio', 'transactions']"
>Manage Transactions...</a
>
</div>
</div>
</div>
</div>

@ -0,0 +1,23 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
import { HomeHoldingsComponent } from './home-holdings.component';
@NgModule({
declarations: [HomeHoldingsComponent],
exports: [],
imports: [
CommonModule,
GfPositionsModule,
MatButtonModule,
MatCardModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfHomeHoldingsModule {}

@ -0,0 +1,5 @@
@import '~apps/client/src/styles/ghostfolio-style';
:host {
display: block;
}

@ -0,0 +1,74 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DataSource } from '@prisma/client';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'gf-home-market',
styleUrls: ['./home-market.scss'],
templateUrl: './home-market.html'
})
export class HomeMarketComponent implements OnDestroy, OnInit {
public fearAndGreedIndex: number;
public hasPermissionToAccessFearAndGreedIndex: boolean;
public isLoading = true;
public user: User;
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private userService: UserService
) {
this.isLoading = true;
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.user.permissions,
permissions.accessFearAndGreedIndex
);
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.dataService
.fetchSymbolItem({
dataSource: DataSource.RAKUTEN,
symbol: ghostfolioFearAndGreedIndexSymbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ marketPrice }) => {
this.fearAndGreedIndex = marketPrice;
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
}
this.changeDetectorRef.markForCheck();
}
});
}
/**
* Initializes the controller
*/
public ngOnInit() {}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

@ -0,0 +1,25 @@
<div
class="
align-items-center
container
d-flex
flex-grow-1
h-100
justify-content-center
w-100
"
>
<div class="row w-100">
<div class="col-xs-12 col-md-8 offset-md-2">
<mat-card class="h-100">
<mat-card-content>
<gf-fear-and-greed-index
class="d-flex justify-content-center"
[fearAndGreedIndex]="fearAndGreedIndex"
[hidden]="isLoading"
></gf-fear-and-greed-index>
</mat-card-content>
</mat-card>
</div>
</div>
</div>

@ -0,0 +1,15 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module';
import { HomeMarketComponent } from './home-market.component';
@NgModule({
declarations: [HomeMarketComponent],
exports: [],
imports: [CommonModule, GfFearAndGreedIndexModule, MatCardModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfHomeMarketModule {}

@ -0,0 +1,5 @@
@import '~apps/client/src/styles/ghostfolio-style';
:host {
display: block;
}

@ -0,0 +1,122 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/toggle-option.type';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import {
RANGE,
SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { PortfolioPerformance, User } from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'gf-home-overview',
styleUrls: ['./home-overview.scss'],
templateUrl: './home-overview.html'
})
export class HomeOverviewComponent implements OnDestroy, OnInit {
public dateRange: DateRange;
public dateRangeOptions: ToggleOption[] = [
{ label: 'Today', value: '1d' },
{ label: 'YTD', value: 'ytd' },
{ label: '1Y', value: '1y' },
{ label: '5Y', value: '5y' },
{ label: 'Max', value: 'max' }
];
public hasImpersonationId: boolean;
public historicalDataItems: LineChartItem[];
public isAllTimeHigh: boolean;
public isAllTimeLow: boolean;
public isLoadingPerformance = true;
public performance: PortfolioPerformance;
public user: User;
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private impersonationStorageService: ImpersonationStorageService,
private settingsStorageService: SettingsStorageService,
private userService: UserService
) {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.changeDetectorRef.markForCheck();
}
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((aId) => {
this.hasImpersonationId = !!aId;
this.changeDetectorRef.markForCheck();
});
this.dateRange =
<DateRange>this.settingsStorageService.getSetting(RANGE) || 'max';
this.update();
}
public onChangeDateRange(aDateRange: DateRange) {
this.dateRange = aDateRange;
this.settingsStorageService.setSetting(RANGE, this.dateRange);
this.update();
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private update() {
this.isLoadingPerformance = true;
this.dataService
.fetchChart({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
return {
date: chartDataItem.date,
value: chartDataItem.value
};
});
this.isAllTimeHigh = chartData.isAllTimeHigh;
this.isAllTimeLow = chartData.isAllTimeLow;
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchPortfolioPerformance({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.performance = response;
this.isLoadingPerformance = false;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
}
}

@ -0,0 +1,55 @@
<div
class="
align-items-center
container
d-flex
flex-column
h-100
justify-content-center
overview
position-relative
"
>
<div class="row w-100">
<div class="chart-container col">
<gf-line-chart
symbol="Performance"
[historicalDataItems]="historicalDataItems"
[showGradient]="true"
[showLoader]="false"
[showXAxis]="false"
[showYAxis]="false"
></gf-line-chart>
<div
*ngIf="historicalDataItems?.length === 0"
class="align-items-center d-flex h-100 justify-content-center w-100"
>
<div class="d-flex justify-content-center">
<gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
</div>
</div>
</div>
</div>
<div class="overview-container row mt-1">
<div class="col">
<gf-portfolio-performance
class="pb-4"
[baseCurrency]="user?.settings?.baseCurrency"
[isAllTimeHigh]="isAllTimeHigh"
[isAllTimeLow]="isAllTimeLow"
[isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale"
[performance]="performance"
[showDetails]="!hasImpersonationId && !user.settings.isRestrictedView"
></gf-portfolio-performance>
<div class="text-center">
<gf-toggle
[defaultValue]="dateRange"
[isLoading]="isLoadingPerformance"
[options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)"
></gf-toggle>
</div>
</div>
</div>
</div>

@ -0,0 +1,25 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
import { HomeOverviewComponent } from './home-overview.component';
@NgModule({
declarations: [HomeOverviewComponent],
exports: [],
imports: [
CommonModule,
GfLineChartModule,
GfNoTransactionsInfoModule,
GfPortfolioPerformanceModule,
GfToggleModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfHomeOverviewModule {}

@ -0,0 +1,34 @@
@import '~apps/client/src/styles/ghostfolio-style';
:host {
display: block;
.chart-container {
aspect-ratio: 16 / 9;
max-height: 50vh;
// Fallback for aspect-ratio (using padding hack)
@supports not (aspect-ratio: 16 / 9) {
&::before {
float: left;
padding-top: 56.25%;
content: '';
}
&::after {
display: block;
content: '';
clear: both;
}
}
gf-line-chart {
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: -1;
}
}
}

@ -0,0 +1,66 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { PortfolioSummary, User } from '@ghostfolio/common/interfaces';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'gf-home-summary',
styleUrls: ['./home-summary.scss'],
templateUrl: './home-summary.html'
})
export class HomeSummaryComponent implements OnDestroy, OnInit {
public isLoading = true;
public summary: PortfolioSummary;
public user: User;
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private userService: UserService
) {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.changeDetectorRef.markForCheck();
}
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.update();
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private update() {
this.isLoading = true;
this.dataService
.fetchPortfolioSummary()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.summary = response;
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
}
}

@ -0,0 +1,19 @@
<div class="container pb-3 px-3">
<div class="row">
<div class="col-xs-12 col-md-8 offset-md-2">
<mat-card class="h-100">
<mat-card-header>
<mat-card-title i18n>Summary</mat-card-title>
</mat-card-header>
<mat-card-content>
<gf-portfolio-summary
[baseCurrency]="user?.settings?.baseCurrency"
[isLoading]="isLoading"
[locale]="user?.settings?.locale"
[summary]="summary"
></gf-portfolio-summary>
</mat-card-content>
</mat-card>
</div>
</div>
</div>

@ -0,0 +1,21 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
import { HomeSummaryComponent } from './home-summary.component';
@NgModule({
declarations: [HomeSummaryComponent],
exports: [],
imports: [
CommonModule,
GfPortfolioSummaryModule,
MatCardModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfHomeSummaryModule {}

@ -0,0 +1,5 @@
@import '~apps/client/src/styles/ghostfolio-style';
:host {
display: block;
}

@ -1,11 +1,26 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeHoldingsComponent } from '@ghostfolio/client/components/home-holdings/home-holdings.component';
import { HomeMarketComponent } from '@ghostfolio/client/components/home-market/home-market.component';
import { HomeOverviewComponent } from '@ghostfolio/client/components/home-overview/home-overview.component';
import { HomeSummaryComponent } from '@ghostfolio/client/components/home-summary/home-summary.component';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { HomePageComponent } from './home-page.component';
const routes: Routes = [
{ path: '', component: HomePageComponent, canActivate: [AuthGuard] }
{
path: '',
component: HomePageComponent,
canActivate: [AuthGuard],
children: [
{ path: '', redirectTo: 'overview', pathMatch: 'full' },
{ path: 'overview', component: HomeOverviewComponent },
{ path: 'holdings', component: HomeHoldingsComponent },
{ path: 'summary', component: HomeSummaryComponent },
{ path: 'market', component: HomeMarketComponent }
]
}
];
@NgModule({

@ -1,35 +1,17 @@
import {
ChangeDetectorRef,
Component,
ElementRef,
HostBinding,
OnDestroy,
OnInit,
ViewChild
OnInit
} from '@angular/core';
import { MatTabChangeEvent } from '@angular/material/tabs';
import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/toggle-option.type';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import {
RANGE,
SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
import {
PortfolioPerformance,
PortfolioSummary,
Position,
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
import { DataSource } from '@prisma/client';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject, Subscription } from 'rxjs';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { User } from '@ghostfolio/common/interfaces';
@Component({
selector: 'gf-home-page',
@ -41,32 +23,9 @@ export class HomePageComponent implements OnDestroy, OnInit {
return this.canCreateAccount;
}
@ViewChild('positionsContainer') positionsContainer: ElementRef;
public canCreateAccount: boolean;
public currentTabIndex = 0;
public dateRange: DateRange;
public dateRangeOptions: ToggleOption[] = [
{ label: 'Today', value: '1d' },
{ label: 'YTD', value: 'ytd' },
{ label: '1Y', value: '1y' },
{ label: '5Y', value: '5y' },
{ label: 'Max', value: 'max' }
];
public deviceType: string;
public fearAndGreedIndex: number;
public hasImpersonationId: boolean;
public hasPermissionToAccessFearAndGreedIndex: boolean;
public hasPermissionToCreateOrder: boolean;
public historicalDataItems: LineChartItem[];
public isAllTimeHigh: boolean;
public isAllTimeLow: boolean;
public isLoadingPerformance = true;
public isLoadingSummary = true;
public performance: PortfolioPerformance;
public positions: Position[];
public routeQueryParams: Subscription;
public summary: PortfolioSummary;
public tabs: { iconName: string; path: string }[] = [];
public user: User;
private unsubscribeSubject = new Subject<void>();
@ -76,16 +35,19 @@ export class HomePageComponent implements OnDestroy, OnInit {
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService,
private settingsStorageService: SettingsStorageService,
private userService: UserService
) {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.tabs = [
{ iconName: 'analytics-outline', path: 'overview' },
{ iconName: 'wallet-outline', path: 'holdings' },
{ iconName: 'reader-outline', path: 'summary' }
];
this.user = state.user;
this.canCreateAccount = hasPermission(
@ -99,24 +61,9 @@ export class HomePageComponent implements OnDestroy, OnInit {
);
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.dataService
.fetchSymbolItem({
dataSource: DataSource.RAKUTEN,
symbol: ghostfolioFearAndGreedIndexSymbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ marketPrice }) => {
this.fearAndGreedIndex = marketPrice;
this.changeDetectorRef.markForCheck();
});
this.tabs.push({ iconName: 'newspaper-outline', path: 'market' });
}
this.hasPermissionToCreateOrder = hasPermission(
this.user.permissions,
permissions.createOrder
);
this.changeDetectorRef.markForCheck();
}
});
@ -125,93 +72,10 @@ export class HomePageComponent implements OnDestroy, OnInit {
/**
* Initializes the controller
*/
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((aId) => {
this.hasImpersonationId = !!aId;
this.changeDetectorRef.markForCheck();
});
this.dateRange =
<DateRange>this.settingsStorageService.getSetting(RANGE) || 'max';
this.update();
}
public onChangeDateRange(aDateRange: DateRange) {
this.dateRange = aDateRange;
this.settingsStorageService.setSetting(RANGE, this.dateRange);
this.update();
}
public onTabChanged(event: MatTabChangeEvent) {
this.currentTabIndex = event.index;
this.update();
}
public ngOnInit() {}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private update() {
if (this.currentTabIndex === 0) {
this.isLoadingPerformance = true;
this.dataService
.fetchChart({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
return {
date: chartDataItem.date,
value: chartDataItem.value
};
});
this.isAllTimeHigh = chartData.isAllTimeHigh;
this.isAllTimeLow = chartData.isAllTimeLow;
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchPortfolioPerformance({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.performance = response;
this.isLoadingPerformance = false;
this.changeDetectorRef.markForCheck();
});
} else if (this.currentTabIndex === 1) {
this.dataService
.fetchPositions({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.positions = response.positions;
this.changeDetectorRef.markForCheck();
});
} else if (this.currentTabIndex === 2) {
this.isLoadingSummary = true;
this.dataService
.fetchPortfolioSummary()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.summary = response;
this.isLoadingSummary = false;
this.changeDetectorRef.markForCheck();
});
}
this.changeDetectorRef.markForCheck();
}
}

@ -1,169 +1,14 @@
<mat-tab-group
animationDuration="0ms"
class="position-absolute"
headerPosition="below"
mat-align-tabs="center"
[disablePagination]="true"
(selectedTabChange)="onTabChanged($event)"
>
<mat-tab>
<ng-template mat-tab-label>
<ion-icon name="analytics-outline" size="large"></ion-icon>
</ng-template>
<div
class="
align-items-center
container
d-flex
flex-column
h-100
justify-content-center
overview
position-relative
"
>
<div class="row w-100">
<div class="chart-container col">
<gf-line-chart
class="mr-3"
symbol="Performance"
[historicalDataItems]="historicalDataItems"
[showGradient]="true"
[showLoader]="false"
[showXAxis]="false"
[showYAxis]="false"
></gf-line-chart>
<div
*ngIf="historicalDataItems?.length === 0"
class="
align-items-center
chart-container
d-flex
justify-content-center
w-100
"
>
<div class="d-flex justify-content-center">
<gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
</div>
</div>
</div>
</div>
<div class="overview-container row mt-1">
<div class="col">
<gf-portfolio-performance
class="pb-4"
[baseCurrency]="user?.settings?.baseCurrency"
[isAllTimeHigh]="isAllTimeHigh"
[isAllTimeLow]="isAllTimeLow"
[isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale"
[performance]="performance"
[showDetails]="!hasImpersonationId && !user.settings.isRestrictedView"
></gf-portfolio-performance>
<div class="text-center">
<gf-toggle
[defaultValue]="dateRange"
[isLoading]="isLoadingPerformance"
[options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)"
></gf-toggle>
</div>
</div>
</div>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<ion-icon name="wallet-outline" size="large"></ion-icon>
</ng-template>
<div class="container justify-content-center pb-3 px-3 positions">
<h3 class="d-flex justify-content-center mb-3" i18n>Holdings</h3>
<div class="row">
<div class="align-items-center col-xs-12 col-md-8 offset-md-2">
<div class="pb-2 text-center">
<gf-toggle
[defaultValue]="dateRange"
[isLoading]="isLoadingPerformance"
[options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)"
></gf-toggle>
</div>
<mat-card class="p-0">
<mat-card-content>
<gf-positions
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[locale]="user?.settings?.locale"
[positions]="positions"
[range]="dateRange"
></gf-positions>
</mat-card-content>
</mat-card>
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
<a
class="mt-3"
i18n
mat-button
[routerLink]="['/portfolio', 'transactions']"
>Manage Transactions...</a
>
</div>
</div>
</div>
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<ion-icon name="reader-outline" size="large"></ion-icon>
</ng-template>
<div class="container pb-3 px-3 positions">
<div class="row">
<div class="col-xs-12 col-md-8 offset-md-2">
<mat-card class="h-100">
<mat-card-header>
<mat-card-title i18n>Summary</mat-card-title>
</mat-card-header>
<mat-card-content>
<gf-portfolio-summary
[baseCurrency]="user?.settings?.baseCurrency"
[isLoading]="isLoadingSummary"
[locale]="user?.settings?.locale"
[summary]="summary"
></gf-portfolio-summary>
</mat-card-content>
</mat-card>
</div>
</div>
</div>
</mat-tab>
<mat-tab *ngIf="hasPermissionToAccessFearAndGreedIndex">
<ng-template mat-tab-label>
<ion-icon name="newspaper-outline" size="large"></ion-icon>
</ng-template>
<div
class="
align-items-center
container
d-flex
flex-grow-1
h-100
justify-content-center
w-100
"
>
<div class="row w-100">
<div class="col-xs-12 col-md-8 offset-md-2">
<mat-card class="h-100">
<mat-card-content>
<gf-fear-and-greed-index
class="d-flex justify-content-center"
[fearAndGreedIndex]="fearAndGreedIndex"
></gf-fear-and-greed-index>
</mat-card-content>
</mat-card>
</div>
</div>
</div>
</mat-tab>
</mat-tab-group>
<router-outlet></router-outlet>
<nav mat-align-tabs="center" mat-tab-nav-bar>
<a
*ngFor="let tab of tabs"
#rla="routerLinkActive"
mat-tab-link
routerLinkActive
[active]="rla.isActive"
[routerLink]="tab.path"
>
<ion-icon size="large" [name]="tab.iconName"></ion-icon>
</a>
</nav>

@ -1,17 +1,11 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module';
import { GfPerformanceChartDialogModule } from '@ghostfolio/client/components/performance-chart-dialog/performance-chart-dialog.module';
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
import { GfHomeHoldingsModule } from '@ghostfolio/client/components/home-holdings/home-holdings.module';
import { GfHomeMarketModule } from '@ghostfolio/client/components/home-market/home-market.module';
import { GfHomeOverviewModule } from '@ghostfolio/client/components/home-overview/home-overview.module';
import { GfHomeSummaryModule } from '@ghostfolio/client/components/home-summary/home-summary.module';
import { HomePageRoutingModule } from './home-page-routing.module';
import { HomePageComponent } from './home-page.component';
@ -21,17 +15,11 @@ import { HomePageComponent } from './home-page.component';
exports: [],
imports: [
CommonModule,
GfFearAndGreedIndexModule,
GfLineChartModule,
GfNoTransactionsInfoModule,
GfPerformanceChartDialogModule,
GfPortfolioPerformanceModule,
GfPortfolioSummaryModule,
GfPositionsModule,
GfToggleModule,
GfHomeHoldingsModule,
GfHomeMarketModule,
GfHomeOverviewModule,
GfHomeSummaryModule,
HomePageRoutingModule,
MatButtonModule,
MatCardModule,
MatTabsModule,
RouterModule
],

@ -2,109 +2,42 @@
:host {
color: rgb(var(--dark-primary-text));
display: block;
min-height: calc(100vh - 5rem);
position: relative;
display: flex;
flex-direction: column;
height: calc(100vh - 5rem);
overflow-y: auto;
&.with-create-account-container {
min-height: calc(100vh - 5rem - 3.5rem);
}
.mat-tab-group {
bottom: 0;
left: 0;
right: 0;
top: 0;
margin-bottom: env(safe-area-inset-bottom);
margin-bottom: constant(safe-area-inset-bottom);
::ng-deep {
.mat-tab-body-wrapper {
height: 100%;
.container {
&.overview {
.chart-container {
aspect-ratio: 16 / 9;
max-height: 50vh;
// Fallback for aspect-ratio (using padding hack)
@supports not (aspect-ratio: 16 / 9) {
&::before {
float: left;
padding-top: 56.25%;
content: '';
}
&::after {
display: block;
content: '';
clear: both;
}
}
gf-line-chart {
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: -1;
}
}
}
&.positions {
min-height: 100%;
}
}
}
.mat-tab-header {
border-top: 0;
padding-bottom: env(safe-area-inset-bottom);
padding-bottom: constant(safe-area-inset-bottom);
.mat-ink-bar {
visibility: hidden !important;
}
.mat-tab-label-active {
color: rgba(var(--palette-primary-500), 1);
opacity: 1;
}
}
}
&.with-create-account-container {
height: calc(100vh - 5rem - 3.5rem);
}
::ng-deep {
.mat-form-field-infix {
border-top: 0 solid transparent !important;
gf-home-holdings,
gf-home-market,
gf-home-overview,
gf-home-summary {
flex: 1 1 auto;
overflow-y: auto;
}
.mat-form-field-wrapper {
padding-bottom: 0 !important;
}
.mat-tab-header {
border-bottom: 0;
.mat-form-field-underline {
bottom: 0 !important;
}
.mat-ink-bar {
visibility: hidden !important;
}
.mat-form-field-appearance-outline .mat-select-arrow-wrapper {
transform: translateY(0);
.mat-tab-label-active {
color: rgba(var(--palette-primary-500), 1);
opacity: 1;
}
}
}
}
:host-context(.is-dark-theme) {
color: rgb(var(--light-primary-text));
.container {
&.overview {
.button-container {
.mat-flat-button {
background-color: rgba(255, 255, 255, $alpha-hover);
}
}
}
}
}

@ -1,11 +1,22 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeHoldingsComponent } from '@ghostfolio/client/components/home-holdings/home-holdings.component';
import { HomeOverviewComponent } from '@ghostfolio/client/components/home-overview/home-overview.component';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { ZenPageComponent } from './zen-page.component';
const routes: Routes = [
{ path: '', component: ZenPageComponent, canActivate: [AuthGuard] }
{
path: '',
component: ZenPageComponent,
canActivate: [AuthGuard],
children: [
{ path: '', redirectTo: 'overview', pathMatch: 'full' },
{ path: 'overview', component: HomeOverviewComponent },
{ path: 'holdings', component: HomeHoldingsComponent }
]
}
];
@NgModule({

@ -3,25 +3,12 @@ import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
OnDestroy,
OnInit,
ViewChild
OnInit
} from '@angular/core';
import { MatTabChangeEvent } from '@angular/material/tabs';
import { ActivatedRoute } from '@angular/router';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
PortfolioPerformance,
Position,
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
import { DeviceDetectorService } from 'ngx-device-detector';
import { User } from '@ghostfolio/common/interfaces';
import { Subject } from 'rxjs';
import { first, takeUntil } from 'rxjs/operators';
@ -31,19 +18,7 @@ import { first, takeUntil } from 'rxjs/operators';
styleUrls: ['./zen-page.scss']
})
export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
@ViewChild('positionsContainer') positionsContainer: ElementRef;
public currentTabIndex = 0;
public dateRange: DateRange = 'max';
public deviceType: string;
public hasImpersonationId: boolean;
public hasPermissionToCreateOrder: boolean;
public historicalDataItems: LineChartItem[];
public isAllTimeHigh: boolean;
public isAllTimeLow: boolean;
public isLoadingPerformance = true;
public performance: PortfolioPerformance;
public positions: Position[];
public tabs: { iconName: string; path: string }[] = [];
public user: User;
private unsubscribeSubject = new Subject<void>();
@ -54,9 +29,6 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
public constructor(
private route: ActivatedRoute,
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService,
private userService: UserService,
private viewportScroller: ViewportScroller
) {
@ -64,32 +36,18 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.tabs = [
{ iconName: 'analytics-outline', path: 'overview' },
{ iconName: 'wallet-outline', path: 'holdings' }
];
this.user = state.user;
this.hasPermissionToCreateOrder = hasPermission(
this.user.permissions,
permissions.createOrder
);
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((aId) => {
this.hasImpersonationId = !!aId;
this.changeDetectorRef.markForCheck();
});
this.update();
}
public ngOnInit() {}
public ngAfterViewInit(): void {
this.route.fragment
@ -97,57 +55,8 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
.subscribe((fragment) => this.viewportScroller.scrollToAnchor(fragment));
}
public onTabChanged(event: MatTabChangeEvent) {
this.currentTabIndex = event.index;
this.update();
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private update() {
if (this.currentTabIndex === 0) {
this.isLoadingPerformance = true;
this.dataService
.fetchChart({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
return {
date: chartDataItem.date,
value: chartDataItem.value
};
});
this.isAllTimeHigh = chartData.isAllTimeHigh;
this.isAllTimeLow = chartData.isAllTimeLow;
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchPortfolioPerformance({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.performance = response;
this.isLoadingPerformance = false;
this.changeDetectorRef.markForCheck();
});
} else if (this.currentTabIndex === 1) {
this.dataService
.fetchPositions({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.positions = response.positions;
this.changeDetectorRef.markForCheck();
});
}
this.changeDetectorRef.markForCheck();
}
}

@ -1,94 +1,14 @@
<mat-tab-group
animationDuration="0ms"
class="position-absolute"
headerPosition="below"
mat-align-tabs="center"
[disablePagination]="true"
(selectedTabChange)="onTabChanged($event)"
>
<mat-tab>
<ng-template mat-tab-label>
<ion-icon name="analytics-outline" size="large"></ion-icon>
</ng-template>
<div
class="
container
d-flex
flex-column
h-100
justify-content-center
overview
position-relative
"
>
<div class="row">
<div
class="chart-container d-flex flex-column col justify-content-center"
>
<gf-line-chart
class="mr-3"
symbol="Performance"
[historicalDataItems]="historicalDataItems"
[showGradient]="true"
[showLoader]="false"
[showXAxis]="false"
[showYAxis]="false"
></gf-line-chart>
<div
*ngIf="historicalDataItems?.length === 0"
class="d-flex justify-content-center"
>
<gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
</div>
</div>
</div>
<div class="overview-container row mb-5 mt-1">
<div class="col">
<gf-portfolio-performance
class="pb-4"
[baseCurrency]="user?.settings?.baseCurrency"
[isAllTimeHigh]="isAllTimeHigh"
[isAllTimeLow]="isAllTimeLow"
[isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale"
[performance]="performance"
[showDetails]="!hasImpersonationId && !user.settings.isRestrictedView"
></gf-portfolio-performance>
</div>
</div>
</div>
</mat-tab>
<router-outlet></router-outlet>
<mat-tab>
<ng-template mat-tab-label>
<ion-icon name="wallet-outline" size="large"></ion-icon>
</ng-template>
<div class="container justify-content-center pb-3 px-3 positions">
<h3 class="d-flex justify-content-center mb-3" i18n>Holdings</h3>
<div class="row">
<div class="align-items-center col">
<mat-card class="p-0">
<mat-card-content>
<gf-positions
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[locale]="user?.settings?.locale"
[positions]="positions"
[range]="dateRange"
></gf-positions>
</mat-card-content>
</mat-card>
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
<a
class="mt-3"
i18n
mat-button
[routerLink]="['/portfolio', 'transactions']"
>Manage Transactions...</a
>
</div>
</div>
</div>
</div>
</mat-tab>
</mat-tab-group>
<nav mat-align-tabs="center" mat-tab-nav-bar>
<a
*ngFor="let tab of tabs"
#rla="routerLinkActive"
mat-tab-link
routerLinkActive
[active]="rla.isActive"
[routerLink]="tab.path"
>
<ion-icon size="large" [name]="tab.iconName"></ion-icon>
</a>
</nav>

@ -1,13 +1,9 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
import { GfHomeHoldingsModule } from '@ghostfolio/client/components/home-holdings/home-holdings.module';
import { GfHomeOverviewModule } from '@ghostfolio/client/components/home-overview/home-overview.module';
import { ZenPageRoutingModule } from './zen-page-routing.module';
import { ZenPageComponent } from './zen-page.component';
@ -17,12 +13,8 @@ import { ZenPageComponent } from './zen-page.component';
exports: [],
imports: [
CommonModule,
GfLineChartModule,
GfNoTransactionsInfoModule,
GfPortfolioPerformanceModule,
GfPositionsModule,
MatButtonModule,
MatCardModule,
GfHomeHoldingsModule,
GfHomeOverviewModule,
MatTabsModule,
RouterModule,
ZenPageRoutingModule

@ -2,72 +2,31 @@
:host {
color: rgb(var(--dark-primary-text));
display: block;
min-height: calc(100vh - 5rem);
position: relative;
.mat-tab-group {
bottom: 0;
left: 0;
right: 0;
top: 0;
margin-bottom: env(safe-area-inset-bottom);
margin-bottom: constant(safe-area-inset-bottom);
::ng-deep {
.mat-tab-body-wrapper {
height: 100%;
.container {
&.overview {
.chart-container {
aspect-ratio: 16 / 9;
max-height: 50vh;
// Fallback for aspect-ratio (using padding hack)
@supports not (aspect-ratio: 16 / 9) {
&::before {
float: left;
padding-top: 56.25%;
content: '';
}
&::after {
display: block;
content: '';
clear: both;
}
}
display: flex;
flex-direction: column;
height: calc(100vh - 5rem);
overflow-y: auto;
padding-bottom: env(safe-area-inset-bottom);
padding-bottom: constant(safe-area-inset-bottom);
::ng-deep {
gf-home-holdings,
gf-home-overview {
flex: 1 1 auto;
overflow-y: auto;
}
gf-line-chart {
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: -1;
}
}
}
.mat-tab-header {
border-bottom: 0;
&.positions {
min-height: 100%;
}
}
.mat-ink-bar {
visibility: hidden !important;
}
.mat-tab-header {
border-top: 0;
.mat-ink-bar {
visibility: hidden !important;
}
.mat-tab-label-active {
color: rgba(var(--palette-primary-500), 1);
opacity: 1;
}
.mat-tab-label-active {
color: rgba(var(--palette-primary-500), 1);
opacity: 1;
}
}
}
@ -75,14 +34,4 @@
:host-context(.is-dark-theme) {
color: rgb(var(--light-primary-text));
.container {
&.overview {
.button-container {
.mat-flat-button {
background-color: rgba(255, 255, 255, $alpha-hover);
}
}
}
}
}

Loading…
Cancel
Save