Feature/zen mode (#110)
* Start with implementation * Refactor AuthGuard, persist displayMode in user settings * Refactor DisplayMode to ViewMode * Update changelog
This commit is contained in:
parent
702ee956a2
commit
78a4946e8b
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added _Zen Mode_: the distraction-free view
|
||||||
|
|
||||||
## 1.4.0 - 20.05.2021
|
## 1.4.0 - 20.05.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { Currency } from '@prisma/client';
|
import { Currency, ViewMode } from '@prisma/client';
|
||||||
import { IsString } from 'class-validator';
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateUserSettingsDto {
|
export class UpdateUserSettingsDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
currency: Currency;
|
baseCurrency: Currency;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
viewMode: ViewMode;
|
||||||
}
|
}
|
||||||
|
@ -93,8 +93,9 @@ export class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await this.userService.updateUserSettings({
|
return await this.userService.updateUserSettings({
|
||||||
currency: data.currency,
|
currency: data.baseCurrency,
|
||||||
userId: this.request.user.id
|
userId: this.request.user.id,
|
||||||
|
viewMode: data.viewMode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { resetHours } from '@ghostfolio/common/helper';
|
|||||||
import { User as IUser, UserWithSettings } from '@ghostfolio/common/interfaces';
|
import { User as IUser, UserWithSettings } from '@ghostfolio/common/interfaces';
|
||||||
import { getPermissions, permissions } from '@ghostfolio/common/permissions';
|
import { getPermissions, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency, Prisma, Provider, User } from '@prisma/client';
|
import { Currency, Prisma, Provider, User, ViewMode } from '@prisma/client';
|
||||||
import { add } from 'date-fns';
|
import { add } from 'date-fns';
|
||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -52,8 +52,9 @@ export class UserService {
|
|||||||
accounts: Account,
|
accounts: Account,
|
||||||
permissions: currentPermissions,
|
permissions: currentPermissions,
|
||||||
settings: {
|
settings: {
|
||||||
baseCurrency: Settings?.currency || UserService.DEFAULT_CURRENCY,
|
locale,
|
||||||
locale
|
baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY,
|
||||||
|
viewMode: Settings.viewMode ?? ViewMode.DEFAULT
|
||||||
},
|
},
|
||||||
subscription: {
|
subscription: {
|
||||||
expiresAt: resetHours(add(new Date(), { days: 7 })),
|
expiresAt: resetHours(add(new Date(), { days: 7 })),
|
||||||
@ -80,7 +81,8 @@ export class UserService {
|
|||||||
user.Settings = {
|
user.Settings = {
|
||||||
currency: UserService.DEFAULT_CURRENCY,
|
currency: UserService.DEFAULT_CURRENCY,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
userId: user?.id
|
userId: user?.id,
|
||||||
|
viewMode: ViewMode.DEFAULT
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,10 +189,12 @@ export class UserService {
|
|||||||
|
|
||||||
public async updateUserSettings({
|
public async updateUserSettings({
|
||||||
currency,
|
currency,
|
||||||
userId
|
userId,
|
||||||
|
viewMode
|
||||||
}: {
|
}: {
|
||||||
currency: Currency;
|
currency?: Currency;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
viewMode?: ViewMode;
|
||||||
}) {
|
}) {
|
||||||
await this.prisma.settings.upsert({
|
await this.prisma.settings.upsert({
|
||||||
create: {
|
create: {
|
||||||
@ -199,10 +203,12 @@ export class UserService {
|
|||||||
connect: {
|
connect: {
|
||||||
id: userId
|
id: userId
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
viewMode
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
currency
|
currency,
|
||||||
|
viewMode
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
userId: userId
|
userId: userId
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config';
|
import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config';
|
||||||
import { getUtc, getYesterday } from '@ghostfolio/common/helper';
|
import { getUtc, getYesterday } from '@ghostfolio/common/helper';
|
||||||
import { AccountType, Currency, DataSource, Role, Type } from '@prisma/client';
|
import {
|
||||||
|
AccountType,
|
||||||
|
Currency,
|
||||||
|
DataSource,
|
||||||
|
Role,
|
||||||
|
Type,
|
||||||
|
ViewMode
|
||||||
|
} from '@prisma/client';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
import { DataProviderService } from '../services/data-provider.service';
|
import { DataProviderService } from '../services/data-provider.service';
|
||||||
@ -120,7 +127,8 @@ describe('Portfolio', () => {
|
|||||||
Settings: {
|
Settings: {
|
||||||
currency: Currency.CHF,
|
currency: Currency.CHF,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
userId: USER_ID
|
userId: USER_ID,
|
||||||
|
viewMode: ViewMode.DEFAULT
|
||||||
},
|
},
|
||||||
thirdPartyId: null,
|
thirdPartyId: null,
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import { bool, cleanEnv, json, num, port, str } from 'envalid';
|
import { bool, cleanEnv, json, num, port, str } from 'envalid';
|
||||||
|
|
||||||
import { Environment } from './interfaces/environment.interface';
|
import { Environment } from './interfaces/environment.interface';
|
||||||
import { DataSource } from '.prisma/client';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ConfigurationService {
|
export class ConfigurationService {
|
||||||
|
@ -78,6 +78,11 @@ const routes: Routes = [
|
|||||||
(m) => m.TransactionsPageModule
|
(m) => m.TransactionsPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'zen',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/zen/zen-page.module').then((m) => m.ZenPageModule)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// wildcard, if requested url doesn't match any paths for routes defined
|
// wildcard, if requested url doesn't match any paths for routes defined
|
||||||
// earlier
|
// earlier
|
||||||
|
@ -8,11 +8,14 @@
|
|||||||
class="d-none d-sm-block"
|
class="d-none d-sm-block"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'home' ? 'primary' : null"
|
[color]="
|
||||||
|
currentRoute === 'home' || currentRoute === 'zen' ? 'primary' : null
|
||||||
|
"
|
||||||
[routerLink]="['/']"
|
[routerLink]="['/']"
|
||||||
>Overview</a
|
>Overview</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
*ngIf="user?.settings?.viewMode === 'DEFAULT'"
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
@ -21,6 +24,7 @@
|
|||||||
>Analysis</a
|
>Analysis</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
*ngIf="user?.settings?.viewMode === 'DEFAULT'"
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
|
@ -5,16 +5,19 @@ import {
|
|||||||
Router,
|
Router,
|
||||||
RouterStateSnapshot
|
RouterStateSnapshot
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
|
import { ViewMode } from '@prisma/client';
|
||||||
|
import { EMPTY } from 'rxjs';
|
||||||
|
import { catchError } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { DataService } from '../services/data.service';
|
||||||
import { SettingsStorageService } from '../services/settings-storage.service';
|
import { SettingsStorageService } from '../services/settings-storage.service';
|
||||||
import { TokenStorageService } from '../services/token-storage.service';
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AuthGuard implements CanActivate {
|
export class AuthGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
|
private dataService: DataService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private settingsStorageService: SettingsStorageService,
|
private settingsStorageService: SettingsStorageService
|
||||||
private tokenStorageService: TokenStorageService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||||
@ -25,23 +28,46 @@ export class AuthGuard implements CanActivate {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoggedIn = !!this.tokenStorageService.getToken();
|
return new Promise<boolean>((resolve) => {
|
||||||
|
this.dataService
|
||||||
|
.fetchUser()
|
||||||
|
.pipe(
|
||||||
|
catchError(() => {
|
||||||
|
if (state.url !== '/start') {
|
||||||
|
this.router.navigate(['/start']);
|
||||||
|
resolve(false);
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoggedIn) {
|
resolve(true);
|
||||||
if (state.url === '/start') {
|
return EMPTY;
|
||||||
this.router.navigate(['/home']);
|
})
|
||||||
return false;
|
)
|
||||||
}
|
.subscribe((user) => {
|
||||||
|
if (
|
||||||
|
state.url === '/home' &&
|
||||||
|
user.settings.viewMode === ViewMode.ZEN
|
||||||
|
) {
|
||||||
|
this.router.navigate(['/zen']);
|
||||||
|
resolve(false);
|
||||||
|
} else if (state.url === '/start') {
|
||||||
|
if (user.settings.viewMode === ViewMode.ZEN) {
|
||||||
|
this.router.navigate(['/zen']);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
resolve(false);
|
||||||
}
|
} else if (
|
||||||
|
state.url === '/zen' &&
|
||||||
|
user.settings.viewMode === ViewMode.DEFAULT
|
||||||
|
) {
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
// Not logged in
|
resolve(true);
|
||||||
if (state.url !== '/start') {
|
});
|
||||||
this.router.navigate(['/start']);
|
});
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,6 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
|||||||
}
|
}
|
||||||
} else if (error.status === StatusCodes.UNAUTHORIZED) {
|
} else if (error.status === StatusCodes.UNAUTHORIZED) {
|
||||||
this.tokenStorageService.signOut();
|
this.tokenStorageService.signOut();
|
||||||
this.router.navigate(['start']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return throwError('');
|
return throwError('');
|
||||||
|
@ -68,9 +68,14 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onChangeBaseCurrency({ value: currency }: { value: Currency }) {
|
public onChangeUserSettings(aKey: string, aValue: string) {
|
||||||
|
const settings = { ...this.user.settings, [aKey]: aValue };
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.putUserSettings({ currency })
|
.putUserSettings({
|
||||||
|
baseCurrency: settings?.baseCurrency,
|
||||||
|
viewMode: settings?.viewMode
|
||||||
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.dataService.fetchUser().subscribe((user) => {
|
this.dataService.fetchUser().subscribe((user) => {
|
||||||
|
@ -30,14 +30,14 @@
|
|||||||
<div class="d-flex mt-4 py-1">
|
<div class="d-flex mt-4 py-1">
|
||||||
<div class="pt-4 w-50" i18n>Settings</div>
|
<div class="pt-4 w-50" i18n>Settings</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
<form #addTransactionForm="ngForm">
|
<form #changeUserSettingsForm="ngForm">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="mb-3 w-100">
|
||||||
<mat-label i18n>Base Currency</mat-label>
|
<mat-label i18n>Base Currency</mat-label>
|
||||||
<mat-select
|
<mat-select
|
||||||
name="baseCurrency"
|
name="baseCurrency"
|
||||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||||
[value]="user.settings.baseCurrency"
|
[value]="user.settings.baseCurrency"
|
||||||
(selectionChange)="onChangeBaseCurrency($event)"
|
(selectionChange)="onChangeUserSettings('baseCurrency', $event.value)"
|
||||||
>
|
>
|
||||||
<mat-option
|
<mat-option
|
||||||
*ngFor="let currency of currencies"
|
*ngFor="let currency of currencies"
|
||||||
@ -46,6 +46,18 @@
|
|||||||
>
|
>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>View Mode</mat-label>
|
||||||
|
<mat-select
|
||||||
|
name="viewMode"
|
||||||
|
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||||
|
[value]="user.settings.viewMode"
|
||||||
|
(selectionChange)="onChangeUserSettings('viewMode', $event.value)"
|
||||||
|
>
|
||||||
|
<mat-option value="DEFAULT">Default</mat-option>
|
||||||
|
<mat-option value="ZEN">Zen</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -132,6 +132,11 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
private openDialog(): void {
|
private openDialog(): void {
|
||||||
const dialogRef = this.dialog.open(PerformanceChartDialog, {
|
const dialogRef = this.dialog.open(PerformanceChartDialog, {
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
@ -195,9 +200,4 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
|
||||||
this.unsubscribeSubject.next();
|
|
||||||
this.unsubscribeSubject.complete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||||
@ -27,7 +26,6 @@ import { HomePageComponent } from './home-page.component';
|
|||||||
GfPositionsModule,
|
GfPositionsModule,
|
||||||
GfToggleModule,
|
GfToggleModule,
|
||||||
HomePageRoutingModule,
|
HomePageRoutingModule,
|
||||||
MatButtonModule,
|
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
],
|
],
|
||||||
|
15
apps/client/src/app/pages/zen/zen-page-routing.module.ts
Normal file
15
apps/client/src/app/pages/zen/zen-page-routing.module.ts
Normal 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 { ZenPageComponent } from './zen-page.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: '', component: ZenPageComponent, canActivate: [AuthGuard] }
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class ZenPageRoutingModule {}
|
104
apps/client/src/app/pages/zen/zen-page.component.ts
Normal file
104
apps/client/src/app/pages/zen/zen-page.component.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
|
import { PortfolioPerformance, 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-zen-page',
|
||||||
|
templateUrl: './zen-page.html',
|
||||||
|
styleUrls: ['./zen-page.scss']
|
||||||
|
})
|
||||||
|
export class ZenPageComponent implements OnDestroy, OnInit {
|
||||||
|
public dateRange: DateRange = 'max';
|
||||||
|
public deviceType: string;
|
||||||
|
public hasImpersonationId: boolean;
|
||||||
|
public hasPermissionToReadForeignPortfolio: boolean;
|
||||||
|
public historicalDataItems: LineChartItem[];
|
||||||
|
public isLoadingPerformance = true;
|
||||||
|
public performance: PortfolioPerformance;
|
||||||
|
public user: User;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
private dataService: DataService,
|
||||||
|
private deviceService: DeviceDetectorService,
|
||||||
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
|
private tokenStorageService: TokenStorageService
|
||||||
|
) {
|
||||||
|
this.tokenStorageService
|
||||||
|
.onChangeHasToken()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.dataService.fetchUser().subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
this.hasPermissionToReadForeignPortfolio = hasPermission(
|
||||||
|
user.permissions,
|
||||||
|
permissions.readForeignPortfolio
|
||||||
|
);
|
||||||
|
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the controller
|
||||||
|
*/
|
||||||
|
public ngOnInit() {
|
||||||
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
|
this.impersonationStorageService
|
||||||
|
.onChangeHasImpersonation()
|
||||||
|
.subscribe((aId) => {
|
||||||
|
this.hasImpersonationId = !!aId;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private update() {
|
||||||
|
this.isLoadingPerformance = true;
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.fetchChart({ range: this.dateRange })
|
||||||
|
.subscribe((chartData) => {
|
||||||
|
this.historicalDataItems = chartData.map((chartDataItem) => {
|
||||||
|
return {
|
||||||
|
date: chartDataItem.date,
|
||||||
|
value: chartDataItem.value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.fetchPortfolioPerformance({ range: this.dateRange })
|
||||||
|
.subscribe((response) => {
|
||||||
|
this.performance = response;
|
||||||
|
this.isLoadingPerformance = false;
|
||||||
|
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
}
|
25
apps/client/src/app/pages/zen/zen-page.html
Normal file
25
apps/client/src/app/pages/zen/zen-page.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="chart-container col mr-3">
|
||||||
|
<gf-line-chart
|
||||||
|
symbol="Performance"
|
||||||
|
[historicalDataItems]="historicalDataItems"
|
||||||
|
[showLoader]="false"
|
||||||
|
[showXAxis]="false"
|
||||||
|
[showYAxis]="false"
|
||||||
|
></gf-line-chart>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overview-container row mb-5 mt-1">
|
||||||
|
<div class="col">
|
||||||
|
<gf-portfolio-performance-summary
|
||||||
|
class="pb-4"
|
||||||
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[isLoading]="isLoadingPerformance"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[performance]="performance"
|
||||||
|
[showDetails]="!hasImpersonationId || hasPermissionToReadForeignPortfolio"
|
||||||
|
></gf-portfolio-performance-summary>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
23
apps/client/src/app/pages/zen/zen-page.module.ts
Normal file
23
apps/client/src/app/pages/zen/zen-page.module.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||||
|
import { GfPortfolioPerformanceSummaryModule } from '@ghostfolio/client/components/portfolio-performance-summary/portfolio-performance-summary.module';
|
||||||
|
|
||||||
|
import { ZenPageRoutingModule } from './zen-page-routing.module';
|
||||||
|
import { ZenPageComponent } from './zen-page.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [ZenPageComponent],
|
||||||
|
exports: [],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfLineChartModule,
|
||||||
|
GfPortfolioPerformanceSummaryModule,
|
||||||
|
MatCardModule,
|
||||||
|
ZenPageRoutingModule
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class ZenPageModule {}
|
38
apps/client/src/app/pages/zen/zen-page.scss
Normal file
38
apps/client/src/app/pages/zen/zen-page.scss
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
:host {
|
||||||
|
color: rgb(var(--dark-primary-text));
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
margin-top: 3rem;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.is-dark-theme) {
|
||||||
|
color: rgb(var(--light-primary-text));
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { Currency } from '.prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
|
||||||
export const baseCurrency = Currency.CHF;
|
export const baseCurrency = Currency.CHF;
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Currency } from '@prisma/client';
|
import { Currency, ViewMode } from '@prisma/client';
|
||||||
|
|
||||||
export interface UserSettings {
|
export interface UserSettings {
|
||||||
baseCurrency: Currency;
|
baseCurrency: Currency;
|
||||||
locale: string;
|
locale: string;
|
||||||
|
viewMode: ViewMode;
|
||||||
}
|
}
|
||||||
|
@ -92,10 +92,11 @@ model Property {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Settings {
|
model Settings {
|
||||||
currency Currency
|
currency Currency?
|
||||||
updatedAt DateTime @updatedAt
|
viewMode ViewMode?
|
||||||
User User @relation(fields: [userId], references: [id])
|
updatedAt DateTime @updatedAt
|
||||||
userId String @id
|
User User @relation(fields: [userId], references: [id])
|
||||||
|
userId String @id
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
@ -133,6 +134,11 @@ enum DataSource {
|
|||||||
YAHOO
|
YAHOO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ViewMode {
|
||||||
|
DEFAULT
|
||||||
|
ZEN
|
||||||
|
}
|
||||||
|
|
||||||
enum Provider {
|
enum Provider {
|
||||||
ANONYMOUS
|
ANONYMOUS
|
||||||
GOOGLE
|
GOOGLE
|
||||||
|
Loading…
x
Reference in New Issue
Block a user