diff --git a/CHANGELOG.md b/CHANGELOG.md index 8879ea365..71d612833 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts new file mode 100644 index 000000000..e0f43ff08 --- /dev/null +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -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(); + + /** + * @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 = + 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(); + } +} diff --git a/apps/client/src/app/components/home-holdings/home-holdings.html b/apps/client/src/app/components/home-holdings/home-holdings.html new file mode 100644 index 000000000..f17d36355 --- /dev/null +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -0,0 +1,26 @@ +
+
+
+ + + + + + +
+
+
diff --git a/apps/client/src/app/components/home-holdings/home-holdings.module.ts b/apps/client/src/app/components/home-holdings/home-holdings.module.ts new file mode 100644 index 000000000..c4108673b --- /dev/null +++ b/apps/client/src/app/components/home-holdings/home-holdings.module.ts @@ -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 {} diff --git a/apps/client/src/app/components/home-holdings/home-holdings.scss b/apps/client/src/app/components/home-holdings/home-holdings.scss new file mode 100644 index 000000000..b97d286cc --- /dev/null +++ b/apps/client/src/app/components/home-holdings/home-holdings.scss @@ -0,0 +1,5 @@ +@import '~apps/client/src/styles/ghostfolio-style'; + +:host { + display: block; +} diff --git a/apps/client/src/app/components/home-market/home-market.component.ts b/apps/client/src/app/components/home-market/home-market.component.ts new file mode 100644 index 000000000..380ac9c8d --- /dev/null +++ b/apps/client/src/app/components/home-market/home-market.component.ts @@ -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(); + + /** + * @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(); + } +} diff --git a/apps/client/src/app/components/home-market/home-market.html b/apps/client/src/app/components/home-market/home-market.html new file mode 100644 index 000000000..c92d29950 --- /dev/null +++ b/apps/client/src/app/components/home-market/home-market.html @@ -0,0 +1,25 @@ +
+
+
+ + + + + +
+
+
diff --git a/apps/client/src/app/components/home-market/home-market.module.ts b/apps/client/src/app/components/home-market/home-market.module.ts new file mode 100644 index 000000000..436552e61 --- /dev/null +++ b/apps/client/src/app/components/home-market/home-market.module.ts @@ -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 {} diff --git a/apps/client/src/app/components/home-market/home-market.scss b/apps/client/src/app/components/home-market/home-market.scss new file mode 100644 index 000000000..b97d286cc --- /dev/null +++ b/apps/client/src/app/components/home-market/home-market.scss @@ -0,0 +1,5 @@ +@import '~apps/client/src/styles/ghostfolio-style'; + +:host { + display: block; +} diff --git a/apps/client/src/app/components/home-overview/home-overview.component.ts b/apps/client/src/app/components/home-overview/home-overview.component.ts new file mode 100644 index 000000000..daf034c2b --- /dev/null +++ b/apps/client/src/app/components/home-overview/home-overview.component.ts @@ -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(); + + /** + * @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 = + 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(); + } +} diff --git a/apps/client/src/app/components/home-overview/home-overview.html b/apps/client/src/app/components/home-overview/home-overview.html new file mode 100644 index 000000000..85e6a017c --- /dev/null +++ b/apps/client/src/app/components/home-overview/home-overview.html @@ -0,0 +1,55 @@ +
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+ +
+
+
+
diff --git a/apps/client/src/app/components/home-overview/home-overview.module.ts b/apps/client/src/app/components/home-overview/home-overview.module.ts new file mode 100644 index 000000000..1bbb3dbf5 --- /dev/null +++ b/apps/client/src/app/components/home-overview/home-overview.module.ts @@ -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 {} diff --git a/apps/client/src/app/components/home-overview/home-overview.scss b/apps/client/src/app/components/home-overview/home-overview.scss new file mode 100644 index 000000000..317773f76 --- /dev/null +++ b/apps/client/src/app/components/home-overview/home-overview.scss @@ -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; + } + } +} diff --git a/apps/client/src/app/components/home-summary/home-summary.component.ts b/apps/client/src/app/components/home-summary/home-summary.component.ts new file mode 100644 index 000000000..b2d8d91f4 --- /dev/null +++ b/apps/client/src/app/components/home-summary/home-summary.component.ts @@ -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(); + + /** + * @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(); + } +} diff --git a/apps/client/src/app/components/home-summary/home-summary.html b/apps/client/src/app/components/home-summary/home-summary.html new file mode 100644 index 000000000..fe0b99a2b --- /dev/null +++ b/apps/client/src/app/components/home-summary/home-summary.html @@ -0,0 +1,19 @@ +
+
+
+ + + Summary + + + + + +
+
+
diff --git a/apps/client/src/app/components/home-summary/home-summary.module.ts b/apps/client/src/app/components/home-summary/home-summary.module.ts new file mode 100644 index 000000000..06b6c4ea1 --- /dev/null +++ b/apps/client/src/app/components/home-summary/home-summary.module.ts @@ -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 {} diff --git a/apps/client/src/app/components/home-summary/home-summary.scss b/apps/client/src/app/components/home-summary/home-summary.scss new file mode 100644 index 000000000..b97d286cc --- /dev/null +++ b/apps/client/src/app/components/home-summary/home-summary.scss @@ -0,0 +1,5 @@ +@import '~apps/client/src/styles/ghostfolio-style'; + +:host { + display: block; +} diff --git a/apps/client/src/app/pages/home/home-page-routing.module.ts b/apps/client/src/app/pages/home/home-page-routing.module.ts index ab5a539aa..d2f1f04c7 100644 --- a/apps/client/src/app/pages/home/home-page-routing.module.ts +++ b/apps/client/src/app/pages/home/home-page-routing.module.ts @@ -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({ diff --git a/apps/client/src/app/pages/home/home-page.component.ts b/apps/client/src/app/pages/home/home-page.component.ts index cbf98470b..52b3c5d5a 100644 --- a/apps/client/src/app/pages/home/home-page.component.ts +++ b/apps/client/src/app/pages/home/home-page.component.ts @@ -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(); @@ -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 = - 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(); - } } diff --git a/apps/client/src/app/pages/home/home-page.html b/apps/client/src/app/pages/home/home-page.html index b21fbb3f6..38b7ccdb6 100644 --- a/apps/client/src/app/pages/home/home-page.html +++ b/apps/client/src/app/pages/home/home-page.html @@ -1,169 +1,14 @@ - - - - - -
-
-
- -
-
- -
-
-
-
-
-
- -
- -
-
-
-
-
- - - - -
-

Holdings

-
-
-
- -
- - - - - - -
-
-
-
- - - - -
-
-
- - - Summary - - - - - -
-
-
-
- - - - -
-
-
- - - - - -
-
-
-
-
+ + + diff --git a/apps/client/src/app/pages/home/home-page.module.ts b/apps/client/src/app/pages/home/home-page.module.ts index d6d27bb3f..d1ab78a08 100644 --- a/apps/client/src/app/pages/home/home-page.module.ts +++ b/apps/client/src/app/pages/home/home-page.module.ts @@ -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 ], diff --git a/apps/client/src/app/pages/home/home-page.scss b/apps/client/src/app/pages/home/home-page.scss index f26111455..fb2adc79c 100644 --- a/apps/client/src/app/pages/home/home-page.scss +++ b/apps/client/src/app/pages/home/home-page.scss @@ -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); - } - } - } - } } diff --git a/apps/client/src/app/pages/zen/zen-page-routing.module.ts b/apps/client/src/app/pages/zen/zen-page-routing.module.ts index 59d4cb062..54b0cd691 100644 --- a/apps/client/src/app/pages/zen/zen-page-routing.module.ts +++ b/apps/client/src/app/pages/zen/zen-page-routing.module.ts @@ -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({ diff --git a/apps/client/src/app/pages/zen/zen-page.component.ts b/apps/client/src/app/pages/zen/zen-page.component.ts index d4821ab37..15be6859d 100644 --- a/apps/client/src/app/pages/zen/zen-page.component.ts +++ b/apps/client/src/app/pages/zen/zen-page.component.ts @@ -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(); @@ -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(); - } } diff --git a/apps/client/src/app/pages/zen/zen-page.html b/apps/client/src/app/pages/zen/zen-page.html index c45a53af0..38b7ccdb6 100644 --- a/apps/client/src/app/pages/zen/zen-page.html +++ b/apps/client/src/app/pages/zen/zen-page.html @@ -1,94 +1,14 @@ - - - - - -
-
-
- -
- -
-
-
-
-
- -
-
-
-
+ - - - - -
-

Holdings

-
-
- - - - - - -
-
-
-
-
+ diff --git a/apps/client/src/app/pages/zen/zen-page.module.ts b/apps/client/src/app/pages/zen/zen-page.module.ts index 2106fba63..b9d7050d0 100644 --- a/apps/client/src/app/pages/zen/zen-page.module.ts +++ b/apps/client/src/app/pages/zen/zen-page.module.ts @@ -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 diff --git a/apps/client/src/app/pages/zen/zen-page.scss b/apps/client/src/app/pages/zen/zen-page.scss index bc2f6686c..5f26d0dfc 100644 --- a/apps/client/src/app/pages/zen/zen-page.scss +++ b/apps/client/src/app/pages/zen/zen-page.scss @@ -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); - } - } - } - } }