Browse Source

Consolidate markets pages

pull/7471/head
Thomas Kaul 1 month ago
parent
commit
411d06c69e
  1. 5
      apps/api/src/app/user/user.service.ts
  2. 80
      apps/client/src/app/components/home-market/home-market.component.ts
  3. 35
      apps/client/src/app/components/home-market/home-market.html
  4. 3
      apps/client/src/app/components/home-market/home-market.scss
  5. 115
      apps/client/src/app/components/markets/markets.component.ts
  6. 81
      apps/client/src/app/components/markets/markets.html
  7. 15
      apps/client/src/app/pages/home/home-page.component.ts
  8. 8
      apps/client/src/app/pages/home/home-page.routes.ts
  9. 4
      apps/client/src/app/pages/markets/markets-page.component.ts
  10. 2
      apps/client/src/app/pages/markets/markets-page.html
  11. 5
      libs/common/src/lib/routes/routes.ts

5
apps/api/src/app/user/user.service.ts

@ -555,7 +555,10 @@ export class UserService {
} }
} else { } else {
if ( if (
await this.propertyService.getByKey<string>(PROPERTY_API_KEY_GHOSTFOLIO) this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX') ||
(await this.propertyService.getByKey<string>(
PROPERTY_API_KEY_GHOSTFOLIO
))
) { ) {
currentPermissions.push(permissions.readMarketDataOfMarkets); currentPermissions.push(permissions.readMarketDataOfMarkets);
} }

80
apps/client/src/app/components/home-market/home-market.component.ts

@ -1,80 +0,0 @@
import { GfFearAndGreedIndexComponent } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.component';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Benchmark, InfoItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
import { DataService } from '@ghostfolio/ui/services';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
computed,
CUSTOM_ELEMENTS_SCHEMA,
DestroyRef,
inject,
OnInit,
signal
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DeviceDetectorService } from 'ngx-device-detector';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [GfBenchmarkComponent, GfFearAndGreedIndexComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-home-market',
styleUrls: ['./home-market.scss'],
templateUrl: './home-market.html'
})
export class GfHomeMarketComponent implements OnInit {
protected readonly benchmarks = signal<Benchmark[]>([]);
protected readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType
);
protected fearAndGreedIndex: number | undefined;
protected hasPermissionToAccessFearAndGreedIndex: boolean;
protected user: User;
private readonly info: InfoItem;
private readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly userService = inject(UserService);
public constructor() {
this.info = this.dataService.fetchInfo();
this.userService.stateChanged
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnInit() {
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.info?.globalPermissions,
permissions.enableFearAndGreedIndex
);
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.fearAndGreedIndex = this.info.fearAndGreedStocksMarketPrice;
}
this.dataService
.fetchBenchmarks()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ benchmarks }) => {
this.benchmarks.set(benchmarks);
});
}
}

35
apps/client/src/app/components/home-market/home-market.html

@ -1,35 +0,0 @@
<div class="container">
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Markets</h1>
@if (hasPermissionToAccessFearAndGreedIndex) {
<div class="mb-4 row">
<div class="col-xs-12 col-md-10 offset-md-1">
<gf-fear-and-greed-index
class="d-flex justify-content-center"
[fearAndGreedIndex]="fearAndGreedIndex"
/>
</div>
</div>
}
<div class="mb-3 row">
<div class="col-xs-12 col-md-10 offset-md-1">
<gf-benchmark
[benchmarks]="benchmarks()"
[deviceType]="deviceType()"
[locale]="user?.settings?.locale || undefined"
[showSymbol]="false"
[user]="user"
/>
@if (benchmarks()?.length > 0) {
<div
class="gf-text-wrap-balance line-height-1 mt-3 text-center text-muted"
>
<small i18n>
Calculations are based on delayed market data and may not be
displayed in real-time.</small
>
</div>
}
</div>
</div>
</div>

3
apps/client/src/app/components/home-market/home-market.scss

@ -1,3 +0,0 @@
:host {
display: block;
}

115
apps/client/src/app/components/markets/markets.component.ts

@ -4,10 +4,12 @@ import { resetHours } from '@ghostfolio/common/helper';
import { import {
Benchmark, Benchmark,
HistoricalDataItem, HistoricalDataItem,
InfoItem,
MarketDataOfMarketsResponse, MarketDataOfMarketsResponse,
ToggleOption, ToggleOption,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { FearAndGreedIndexMode } from '@ghostfolio/common/types'; import { FearAndGreedIndexMode } from '@ghostfolio/common/types';
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark'; import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
@ -18,9 +20,12 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
computed,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
DestroyRef, DestroyRef,
OnInit inject,
OnInit,
signal
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
@ -39,29 +44,49 @@ import { DeviceDetectorService } from 'ngx-device-detector';
templateUrl: './markets.html' templateUrl: './markets.html'
}) })
export class GfMarketsComponent implements OnInit { export class GfMarketsComponent implements OnInit {
public benchmarks: Benchmark[]; protected readonly benchmarks = signal<Benchmark[]>([]);
public deviceType: string;
public fearAndGreedIndex: number; protected readonly deviceType = computed(
public fearAndGreedIndexData: MarketDataOfMarketsResponse['fearAndGreedIndex']; () => this.deviceDetectorService.deviceInfo().deviceType
public fearLabel = $localize`Fear`; );
public greedLabel = $localize`Greed`;
public historicalDataItems: HistoricalDataItem[]; protected readonly fearAndGreedIndexModeOptions: ToggleOption[] = [
public fearAndGreedIndexMode: FearAndGreedIndexMode = 'STOCKS';
public fearAndGreedIndexModeOptions: ToggleOption[] = [
{ label: $localize`Stocks`, value: 'STOCKS' }, { label: $localize`Stocks`, value: 'STOCKS' },
{ label: $localize`Cryptocurrencies`, value: 'CRYPTOCURRENCIES' } { label: $localize`Cryptocurrencies`, value: 'CRYPTOCURRENCIES' }
]; ];
public readonly numberOfDays = 365;
public user: User; protected readonly fearLabel = $localize`Fear`;
protected readonly greedLabel = $localize`Greed`;
public constructor( protected readonly numberOfDays = 365;
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, protected fearAndGreedIndex: number | undefined;
private destroyRef: DestroyRef, protected fearAndGreedIndexMode: FearAndGreedIndexMode = 'STOCKS';
private deviceDetectorService: DeviceDetectorService, protected hasPermissionToAccessFearAndGreedIndex: boolean;
private userService: UserService protected hasPermissionToReadMarketDataOfMarkets: boolean;
) { protected historicalDataItems: HistoricalDataItem[];
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; protected user: User;
private fearAndGreedIndexData: MarketDataOfMarketsResponse['fearAndGreedIndex'];
private readonly info: InfoItem;
private readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly userService = inject(UserService);
public constructor() {
this.info = this.dataService.fetchInfo();
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.info?.globalPermissions,
permissions.enableFearAndGreedIndex
);
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.fearAndGreedIndex = this.info.fearAndGreedStocksMarketPrice;
}
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@ -69,6 +94,18 @@ export class GfMarketsComponent implements OnInit {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
this.hasPermissionToReadMarketDataOfMarkets = hasPermission(
this.user.permissions,
permissions.readMarketDataOfMarkets
);
if (
this.hasPermissionToReadMarketDataOfMarkets &&
!this.fearAndGreedIndexData
) {
this.fetchMarketDataOfMarkets();
}
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
}); });
@ -76,27 +113,35 @@ export class GfMarketsComponent implements OnInit {
public ngOnInit() { public ngOnInit() {
this.dataService this.dataService
.fetchMarketDataOfMarkets({ includeHistoricalData: this.numberOfDays }) .fetchBenchmarks()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ fearAndGreedIndex }) => { .subscribe(({ benchmarks }) => {
this.fearAndGreedIndexData = fearAndGreedIndex; this.benchmarks.set(benchmarks);
});
}
this.initialize(); protected onChangeFearAndGreedIndexMode(
aFearAndGreedIndexMode: FearAndGreedIndexMode
) {
this.fearAndGreedIndexMode = aFearAndGreedIndexMode;
this.changeDetectorRef.markForCheck(); this.initializeFearAndGreedIndex();
}); }
private fetchMarketDataOfMarkets() {
this.dataService this.dataService
.fetchBenchmarks() .fetchMarketDataOfMarkets({ includeHistoricalData: this.numberOfDays })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ benchmarks }) => { .subscribe(({ fearAndGreedIndex }) => {
this.benchmarks = benchmarks; this.fearAndGreedIndexData = fearAndGreedIndex;
this.initializeFearAndGreedIndex();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }
public initialize() { private initializeFearAndGreedIndex() {
this.fearAndGreedIndex = this.fearAndGreedIndex =
this.fearAndGreedIndexData[this.fearAndGreedIndexMode]?.marketPrice; this.fearAndGreedIndexData[this.fearAndGreedIndexMode]?.marketPrice;
@ -109,12 +154,4 @@ export class GfMarketsComponent implements OnInit {
} }
]; ];
} }
public onChangeFearAndGreedIndexMode(
aFearAndGreedIndexMode: FearAndGreedIndexMode
) {
this.fearAndGreedIndexMode = aFearAndGreedIndexMode;
this.initialize();
}
} }

81
apps/client/src/app/components/markets/markets.html

@ -1,52 +1,61 @@
<div class="container"> <div class="container">
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Markets</h1> <h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Markets</h1>
<div class="mb-5 row"> @if (
<div class="col-xs-12 col-md-10 offset-md-1"> hasPermissionToAccessFearAndGreedIndex ||
@if (user?.settings?.isExperimentalFeatures) { hasPermissionToReadMarketDataOfMarkets
<div class="align-items-center d-flex flex-grow-1 justify-content-end"> ) {
<gf-toggle <div class="mb-4 row">
class="d-none d-lg-block" <div class="col-xs-12 col-md-10 offset-md-1">
[defaultValue]="fearAndGreedIndexMode" @if (hasPermissionToReadMarketDataOfMarkets) {
[isLoading]="false" @if (user?.settings?.isExperimentalFeatures) {
[options]="fearAndGreedIndexModeOptions" <div
(valueChange)="onChangeFearAndGreedIndexMode($event.value)" class="align-items-center d-flex flex-grow-1 justify-content-end"
>
<gf-toggle
class="d-none d-lg-block"
[defaultValue]="fearAndGreedIndexMode"
[isLoading]="false"
[options]="fearAndGreedIndexModeOptions"
(valueChange)="onChangeFearAndGreedIndexMode($event.value)"
/>
</div>
}
<div class="mb-2 text-center text-muted">
<small i18n>Last {{ numberOfDays }} Days</small>
</div>
<gf-line-chart
class="mb-3"
label="Fear & Greed Index"
[colorScheme]="user?.settings?.colorScheme"
[historicalDataItems]="historicalDataItems"
[isAnimated]="true"
[locale]="user?.settings?.locale || undefined"
[showXAxis]="true"
[showYAxis]="true"
[yMax]="100"
[yMaxLabel]="greedLabel"
[yMin]="0"
[yMinLabel]="fearLabel"
/> />
</div> }
} <gf-fear-and-greed-index
<div class="mb-2 text-center text-muted"> class="d-flex justify-content-center"
<small i18n>Last {{ numberOfDays }} Days</small> [fearAndGreedIndex]="fearAndGreedIndex"
/>
</div> </div>
<gf-line-chart
class="mb-3"
label="Fear & Greed Index"
[colorScheme]="user?.settings?.colorScheme"
[historicalDataItems]="historicalDataItems"
[isAnimated]="true"
[locale]="user?.settings?.locale || undefined"
[showXAxis]="true"
[showYAxis]="true"
[yMax]="100"
[yMaxLabel]="greedLabel"
[yMin]="0"
[yMinLabel]="fearLabel"
/>
<gf-fear-and-greed-index
class="d-flex justify-content-center"
[fearAndGreedIndex]="fearAndGreedIndex"
/>
</div> </div>
</div> }
<div class="mb-3 row"> <div class="mb-3 row">
<div class="col-xs-12 col-md-10 offset-md-1"> <div class="col-xs-12 col-md-10 offset-md-1">
<gf-benchmark <gf-benchmark
[benchmarks]="benchmarks" [benchmarks]="benchmarks()"
[deviceType]="deviceType" [deviceType]="deviceType()"
[locale]="user?.settings?.locale || undefined" [locale]="user?.settings?.locale || undefined"
[showSymbol]="false" [showSymbol]="false"
[user]="user" [user]="user"
/> />
@if (benchmarks?.length > 0) { @if (benchmarks()?.length > 0) {
<div <div
class="gf-text-wrap-balance line-height-1 mt-3 text-center text-muted" class="gf-text-wrap-balance line-height-1 mt-3 text-center text-muted"
> >

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

@ -1,7 +1,6 @@
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 { User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { import {
GfPageTabsComponent, GfPageTabsComponent,
@ -73,18 +72,8 @@ export class GfHomePageComponent implements OnInit {
}, },
{ {
iconName: 'newspaper-outline', iconName: 'newspaper-outline',
label: hasPermission( label: internalRoutes.home.subRoutes.markets.title,
this.user?.permissions, routerLink: internalRoutes.home.subRoutes.markets.routerLink
permissions.readMarketDataOfMarkets
)
? internalRoutes.home.subRoutes.marketsPremium.title
: internalRoutes.home.subRoutes.markets.title,
routerLink: hasPermission(
this.user?.permissions,
permissions.readMarketDataOfMarkets
)
? internalRoutes.home.subRoutes.marketsPremium.routerLink
: internalRoutes.home.subRoutes.markets.routerLink
} }
]; ];
} }

8
apps/client/src/app/pages/home/home-page.routes.ts

@ -1,5 +1,4 @@
import { GfHomeHoldingsComponent } from '@ghostfolio/client/components/home-holdings/home-holdings.component'; import { GfHomeHoldingsComponent } from '@ghostfolio/client/components/home-holdings/home-holdings.component';
import { GfHomeMarketComponent } from '@ghostfolio/client/components/home-market/home-market.component';
import { GfHomeOverviewComponent } from '@ghostfolio/client/components/home-overview/home-overview.component'; import { GfHomeOverviewComponent } from '@ghostfolio/client/components/home-overview/home-overview.component';
import { GfHomeSummaryComponent } from '@ghostfolio/client/components/home-summary/home-summary.component'; import { GfHomeSummaryComponent } from '@ghostfolio/client/components/home-summary/home-summary.component';
import { GfHomeWatchlistComponent } from '@ghostfolio/client/components/home-watchlist/home-watchlist.component'; import { GfHomeWatchlistComponent } from '@ghostfolio/client/components/home-watchlist/home-watchlist.component';
@ -31,13 +30,8 @@ export const routes: Routes = [
}, },
{ {
path: internalRoutes.home.subRoutes.markets.path, path: internalRoutes.home.subRoutes.markets.path,
component: GfHomeMarketComponent,
title: internalRoutes.home.subRoutes.markets.title
},
{
path: internalRoutes.home.subRoutes.marketsPremium.path,
component: GfMarketsComponent, component: GfMarketsComponent,
title: internalRoutes.home.subRoutes.marketsPremium.title title: internalRoutes.home.subRoutes.markets.title
}, },
{ {
path: internalRoutes.home.subRoutes.watchlist.path, path: internalRoutes.home.subRoutes.watchlist.path,

4
apps/client/src/app/pages/markets/markets-page.component.ts

@ -1,11 +1,11 @@
import { GfHomeMarketComponent } from '@ghostfolio/client/components/home-market/home-market.component'; import { GfMarketsComponent } from '@ghostfolio/client/components/markets/markets.component';
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page' }, host: { class: 'page' },
imports: [GfHomeMarketComponent], imports: [GfMarketsComponent],
selector: 'gf-markets-page', selector: 'gf-markets-page',
styleUrls: ['./markets-page.scss'], styleUrls: ['./markets-page.scss'],
templateUrl: './markets-page.html' templateUrl: './markets-page.html'

2
apps/client/src/app/pages/markets/markets-page.html

@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<gf-home-market /> <gf-markets />
</div> </div>
</div> </div>
</div> </div>

5
libs/common/src/lib/routes/routes.ts

@ -94,11 +94,6 @@ export const internalRoutes = {
routerLink: ['/home', 'markets'], routerLink: ['/home', 'markets'],
title: $localize`Markets` title: $localize`Markets`
}, },
marketsPremium: {
path: 'markets-premium',
routerLink: ['/home', 'markets-premium'],
title: $localize`Markets`
},
summary: { summary: {
path: 'summary', path: 'summary',
routerLink: ['/home', 'summary'], routerLink: ['/home', 'summary'],

Loading…
Cancel
Save