Feature/add markets to public pages (#1062)

* Add Markets to public pages

* Update changelog
This commit is contained in:
Thomas Kaul 2022-07-05 21:45:27 +02:00 committed by GitHub
parent fd2408dd62
commit 2060fcaf0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 226 additions and 116 deletions

View File

@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
- Added _Markets_ to the public pages
### Changed ### Changed
- Upgraded `ngx-markdown` from version `13.0.0` to `14.0.1` - Upgraded `ngx-markdown` from version `13.0.0` to `14.0.1`

View File

@ -3,8 +3,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config'; import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces'; import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
import { Controller, Get, UseGuards, UseInterceptors } from '@nestjs/common'; import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { BenchmarkService } from './benchmark.service'; import { BenchmarkService } from './benchmark.service';
@ -16,7 +15,6 @@ export class BenchmarkController {
) {} ) {}
@Get() @Get()
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getBenchmark(): Promise<BenchmarkResponse> { public async getBenchmark(): Promise<BenchmarkResponse> {

View File

@ -63,6 +63,8 @@ export class InfoService {
} else { } else {
info.fearAndGreedDataSource = ghostfolioFearAndGreedIndexDataSource; info.fearAndGreedDataSource = ghostfolioFearAndGreedIndexDataSource;
} }
globalPermissions.push(permissions.enableFearAndGreedIndex);
} }
if (this.configurationService.get('ENABLE_FEATURE_IMPORT')) { if (this.configurationService.get('ENABLE_FEATURE_IMPORT')) {

View File

@ -46,7 +46,6 @@ export class SymbolController {
* Must be after /lookup * Must be after /lookup
*/ */
@Get(':dataSource/:symbol') @Get(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getSymbolData( public async getSymbolData(

View File

@ -158,10 +158,6 @@ export class UserService {
let currentPermissions = getPermissions(user.role); let currentPermissions = getPermissions(user.role);
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
currentPermissions.push(permissions.accessFearAndGreedIndex);
}
if (user.subscription?.type === 'Premium') { if (user.subscription?.type === 'Premium') {
currentPermissions.push(permissions.reportDataGlitch); currentPermissions.push(permissions.reportDataGlitch);
} }

View File

@ -90,6 +90,13 @@ const routes: Routes = [
loadChildren: () => loadChildren: () =>
import('./pages/home/home-page.module').then((m) => m.HomePageModule) import('./pages/home/home-page.module').then((m) => m.HomePageModule)
}, },
{
path: 'markets',
loadChildren: () =>
import('./pages/markets/markets-page.module').then(
(m) => m.MarketsPageModule
)
},
{ {
path: 'p', path: 'p',
loadChildren: () => loadChildren: () =>

View File

@ -269,6 +269,18 @@
[routerLink]="['/pricing']" [routerLink]="['/pricing']"
>Pricing</a >Pricing</a
> >
<a
*ngIf="hasPermissionToAccessFearAndGreedIndex"
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
[ngClass]="{
'font-weight-bold': currentRoute === 'markets',
'text-decoration-underline': currentRoute === 'markets'
}"
[routerLink]="['/markets']"
>Markets</a
>
<a <a
class="d-none d-sm-block mx-1 no-min-width px-1" class="d-none d-sm-block mx-1 no-min-width px-1"
href="https://github.com/ghostfolio/ghostfolio" href="https://github.com/ghostfolio/ghostfolio"

View File

@ -37,6 +37,7 @@ export class HeaderComponent implements OnChanges {
public hasPermissionForSocialLogin: boolean; public hasPermissionForSocialLogin: boolean;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
public hasPermissionToAccessAdminControl: boolean; public hasPermissionToAccessAdminControl: boolean;
public hasPermissionToAccessFearAndGreedIndex: boolean;
public impersonationId: string; public impersonationId: string;
public isMenuOpen: boolean; public isMenuOpen: boolean;
@ -73,6 +74,11 @@ export class HeaderComponent implements OnChanges {
this.user?.permissions, this.user?.permissions,
permissions.accessAdminControl permissions.accessAdminControl
); );
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.info?.globalPermissions,
permissions.enableFearAndGreedIndex
);
} }
public impersonateAccount(aId: string) { public impersonateAccount(aId: string) {

View File

@ -44,49 +44,49 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.user.permissions,
permissions.accessFearAndGreedIndex
);
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.dataService
.fetchSymbolItem({
dataSource: this.info.fearAndGreedDataSource,
includeHistoricalData: this.numberOfDays,
symbol: ghostfolioFearAndGreedIndexSymbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ historicalData, marketPrice }) => {
this.fearAndGreedIndex = marketPrice;
this.historicalData = [
...historicalData,
{
date: resetHours(new Date()).toISOString(),
value: marketPrice
}
];
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
}
this.dataService
.fetchBenchmarks()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ benchmarks }) => {
this.benchmarks = benchmarks;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
}); });
} }
public ngOnInit() {} public ngOnInit() {
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.info?.globalPermissions,
permissions.enableFearAndGreedIndex
);
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.dataService
.fetchSymbolItem({
dataSource: this.info.fearAndGreedDataSource,
includeHistoricalData: this.numberOfDays,
symbol: ghostfolioFearAndGreedIndexSymbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ historicalData, marketPrice }) => {
this.fearAndGreedIndex = marketPrice;
this.historicalData = [
...historicalData,
{
date: resetHours(new Date()).toISOString(),
value: marketPrice
}
];
this.changeDetectorRef.markForCheck();
});
}
this.dataService
.fetchBenchmarks()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ benchmarks }) => {
this.benchmarks = benchmarks;
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
}
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();

View File

@ -28,16 +28,17 @@
<div class="mb-3 row"> <div class="mb-3 row">
<div class="col-xs-12 col-md-8 offset-md-2"> <div class="col-xs-12 col-md-8 offset-md-2">
<gf-benchmark <gf-benchmark
*ngFor="let benchmark of benchmarks" [benchmarks]="benchmarks"
class="py-2"
[benchmark]="benchmark"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
></gf-benchmark> ></gf-benchmark>
<gf-benchmark <ngx-skeleton-loader
*ngIf="!benchmarks" *ngIf="isLoading"
class="py-2" animation="pulse"
[benchmark]="undefined" [theme]="{
></gf-benchmark> height: '1.5rem',
width: '100%'
}"
></ngx-skeleton-loader>
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,19 +3,20 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module'; import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module';
import { GfBenchmarkModule } from '@ghostfolio/ui/benchmark/benchmark.module'; import { GfBenchmarkModule } from '@ghostfolio/ui/benchmark/benchmark.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module'; import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { HomeMarketComponent } from './home-market.component'; import { HomeMarketComponent } from './home-market.component';
@NgModule({ @NgModule({
declarations: [HomeMarketComponent], declarations: [HomeMarketComponent],
exports: [], exports: [HomeMarketComponent],
imports: [ imports: [
CommonModule, CommonModule,
GfBenchmarkModule, GfBenchmarkModule,
GfFearAndGreedIndexModule, GfFearAndGreedIndexModule,
GfLineChartModule GfLineChartModule,
NgxSkeletonLoaderModule
], ],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfHomeMarketModule {} export class GfHomeMarketModule {}

View File

@ -22,6 +22,7 @@ export class AuthGuard implements CanActivate {
'/demo', '/demo',
'/en/blog', '/en/blog',
'/features', '/features',
'/markets',
'/p', '/p',
'/pricing', '/pricing',
'/register', '/register',

View File

@ -7,7 +7,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.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 { InfoItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -24,6 +24,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
public hasMessage: boolean; public hasMessage: boolean;
public hasPermissionToAccessFearAndGreedIndex: boolean; public hasPermissionToAccessFearAndGreedIndex: boolean;
public info: InfoItem;
public tabs: { iconName: string; path: string }[] = []; public tabs: { iconName: string; path: string }[] = [];
public user: User; public user: User;
@ -34,7 +35,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
private dataService: DataService, private dataService: DataService,
private userService: UserService private userService: UserService
) { ) {
const { systemMessage } = this.dataService.fetchInfo(); this.info = this.dataService.fetchInfo();
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
@ -51,11 +52,11 @@ export class HomePageComponent implements OnDestroy, OnInit {
hasPermission( hasPermission(
this.user?.permissions, this.user?.permissions,
permissions.createUserAccount permissions.createUserAccount
) || !!systemMessage; ) || !!this.info.systemMessage;
this.hasPermissionToAccessFearAndGreedIndex = hasPermission( this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.user.permissions, this.info?.globalPermissions,
permissions.accessFearAndGreedIndex permissions.enableFearAndGreedIndex
); );
if (this.hasPermissionToAccessFearAndGreedIndex) { if (this.hasPermissionToAccessFearAndGreedIndex) {

View File

@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { MarketsPageComponent } from './markets-page.component';
const routes: Routes = [
{ path: '', component: MarketsPageComponent, canActivate: [AuthGuard] }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class MarketsPageRoutingModule {}

View File

@ -0,0 +1,21 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
@Component({
host: { class: 'page' },
selector: 'gf-markets-page',
styleUrls: ['./markets-page.scss'],
templateUrl: './markets-page.html'
})
export class MarketsPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
public constructor() {}
public ngOnInit() {}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

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

View File

@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { GfHomeMarketModule } from '@ghostfolio/client/components/home-market/home-market.module';
import { MarketsPageRoutingModule } from './markets-page-routing.module';
import { MarketsPageComponent } from './markets-page.component';
@NgModule({
declarations: [MarketsPageComponent],
imports: [CommonModule, GfHomeMarketModule, MarketsPageRoutingModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class MarketsPageModule {}

View File

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

View File

@ -40,6 +40,10 @@
<loc>https://ghostfol.io/features</loc> <loc>https://ghostfol.io/features</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod> <lastmod>2022-07-01T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/markets</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/pricing</loc> <loc>https://ghostfol.io/pricing</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod> <lastmod>2022-07-01T00:00:00+00:00</lastmod>

View File

@ -4,7 +4,6 @@ import { UserWithSettings } from './interfaces';
export const permissions = { export const permissions = {
accessAdminControl: 'accessAdminControl', accessAdminControl: 'accessAdminControl',
accessFearAndGreedIndex: 'accessFearAndGreedIndex',
createAccess: 'createAccess', createAccess: 'createAccess',
createAccount: 'createAccount', createAccount: 'createAccount',
createOrder: 'createOrder', createOrder: 'createOrder',
@ -14,6 +13,7 @@ export const permissions = {
deleteAuthDevice: 'deleteAuthDevice', deleteAuthDevice: 'deleteAuthDevice',
deleteOrder: 'deleteOrder', deleteOrder: 'deleteOrder',
deleteUser: 'deleteUser', deleteUser: 'deleteUser',
enableFearAndGreedIndex: 'enableFearAndGreedIndex',
enableImport: 'enableImport', enableImport: 'enableImport',
enableBlog: 'enableBlog', enableBlog: 'enableBlog',
enableSocialLogin: 'enableSocialLogin', enableSocialLogin: 'enableSocialLogin',

View File

@ -1,49 +1,50 @@
<div class="align-items-center d-flex"> <table class="gf-table w-100">
<div *ngIf="benchmark?.name" class="flex-grow-1 text-truncate"> <thead>
{{ benchmark.name }} <tr class="mat-header-row">
</div> <th class="mat-header-cell px-1 py-2" i18n>Index</th>
<div *ngIf="!benchmark?.name" class="flex-grow-1"> <th class="mat-header-cell px-1 py-2 text-right">
<ngx-skeleton-loader <span class="d-none d-sm-block text-nowrap" i18n
animation="pulse" >Change from All Time High</span
[theme]="{ >
width: '67%' <span class="d-block d-sm-none text-nowrap" i18n>from ATH</span>
}" </th>
></ngx-skeleton-loader> <th class="mat-header-cell px-1 py-2 text-right" i18n></th>
</div> </tr>
<gf-value </thead>
class="mx-2" <tbody>
size="medium" <tr *ngFor="let benchmark of benchmarks" class="mat-row">
[isPercent]="true" <td class="mat-cell px-1 py-2">
[locale]="locale" <div class="d-flex align-items-center">
[ngClass]="{ {{ benchmark.name }}
'text-danger': </div>
benchmark?.performances?.allTimeHigh?.performancePercent < 0, </td>
'text-success': <td class="mat-cell px-1 py-2 text-right">
benchmark?.performances?.allTimeHigh?.performancePercent > 0 <gf-value
}" class="d-inline-block justify-content-end"
[value]=" size="medium"
benchmark?.performances?.allTimeHigh?.performancePercent ?? undefined [isPercent]="true"
" [locale]="locale"
></gf-value> [ngClass]="{
<div class="text-muted"> 'text-danger':
<small class="d-none d-sm-block text-nowrap" i18n>from All Time High</small benchmark?.performances?.allTimeHigh?.performancePercent < 0,
><small class="d-block d-sm-none text-nowrap" i18n>from ATH</small> 'text-success':
</div> benchmark?.performances?.allTimeHigh?.performancePercent > 0
<div class="ml-2"> }"
<div [value]="
*ngIf="benchmark?.marketCondition" benchmark?.performances?.allTimeHigh?.performancePercent ??
[title]="benchmark?.marketCondition" undefined
> "
{{ resolveMarketCondition(benchmark.marketCondition).emoji }} ></gf-value>
</div> </td>
<ngx-skeleton-loader <td class="mat-cell px-1 py-2">
*ngIf="!benchmark?.marketCondition" <div
animation="pulse" *ngIf="benchmark?.marketCondition"
appearance="circle" class="text-center"
[theme]="{ [title]="benchmark?.marketCondition"
height: '1rem', >
width: '1rem' {{ resolveMarketCondition(benchmark.marketCondition).emoji }}
}" </div>
></ngx-skeleton-loader> </td>
</div> </tr>
</div> </tbody>
</table>

View File

@ -1,4 +1,10 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges
} from '@angular/core';
import { locale } from '@ghostfolio/common/config';
import { resolveMarketCondition } from '@ghostfolio/common/helper'; import { resolveMarketCondition } from '@ghostfolio/common/helper';
import { Benchmark } from '@ghostfolio/common/interfaces'; import { Benchmark } from '@ghostfolio/common/interfaces';
@ -8,11 +14,17 @@ import { Benchmark } from '@ghostfolio/common/interfaces';
templateUrl: './benchmark.component.html', templateUrl: './benchmark.component.html',
styleUrls: ['./benchmark.component.scss'] styleUrls: ['./benchmark.component.scss']
}) })
export class BenchmarkComponent { export class BenchmarkComponent implements OnChanges {
@Input() benchmark: Benchmark; @Input() benchmarks: Benchmark[];
@Input() locale: string; @Input() locale: string;
public resolveMarketCondition = resolveMarketCondition; public resolveMarketCondition = resolveMarketCondition;
public constructor() {} public constructor() {}
public ngOnChanges() {
if (!this.locale) {
this.locale = locale;
}
}
} }

View File

@ -15,7 +15,11 @@ import {
getTooltipPositionerMapTop, getTooltipPositionerMapTop,
getVerticalHoverLinePlugin getVerticalHoverLinePlugin
} from '@ghostfolio/common/chart-helper'; } from '@ghostfolio/common/chart-helper';
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; import {
locale,
primaryColorRgb,
secondaryColorRgb
} from '@ghostfolio/common/config';
import { import {
getBackgroundColor, getBackgroundColor,
getDateFormatString, getDateFormatString,
@ -97,6 +101,10 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }
if (!this.locale) {
this.locale = locale;
}
} }
public ngOnDestroy() { public ngOnDestroy() {