parent
43104f81d0
commit
6dea9093ba
@ -0,0 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { PublicPageComponent } from './public-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: ':id', component: PublicPageComponent, canActivate: [AuthGuard] }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class PublicPageRoutingModule {}
|
@ -0,0 +1,183 @@
|
||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import {
|
||||
PortfolioPosition,
|
||||
PortfolioPublicDetails
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { EMPTY, Subject } from 'rxjs';
|
||||
import { catchError, takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
host: { class: 'mb-5' },
|
||||
selector: 'gf-public-page',
|
||||
styleUrls: ['./public-page.scss'],
|
||||
templateUrl: './public-page.html'
|
||||
})
|
||||
export class PublicPageComponent implements OnInit {
|
||||
public continents: {
|
||||
[code: string]: { name: string; value: number };
|
||||
};
|
||||
public countries: {
|
||||
[code: string]: { name: string; value: number };
|
||||
};
|
||||
public deviceType: string;
|
||||
public portfolioPublicDetails: PortfolioPublicDetails;
|
||||
public positions: {
|
||||
[symbol: string]: Pick<PortfolioPosition, 'name' | 'value'>;
|
||||
};
|
||||
public sectors: {
|
||||
[name: string]: { name: string; value: number };
|
||||
};
|
||||
public symbols: {
|
||||
[name: string]: { name: string; symbol: string; value: number };
|
||||
};
|
||||
|
||||
private id: string;
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private router: Router
|
||||
) {
|
||||
this.activatedRoute.params.subscribe((params) => {
|
||||
this.id = params['id'];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.dataService
|
||||
.fetchPortfolioPublic(this.id)
|
||||
.pipe(
|
||||
takeUntil(this.unsubscribeSubject),
|
||||
catchError((error) => {
|
||||
if (error.status === StatusCodes.NOT_FOUND) {
|
||||
console.error(error);
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
|
||||
return EMPTY;
|
||||
})
|
||||
)
|
||||
.subscribe((portfolioPublicDetails) => {
|
||||
this.portfolioPublicDetails = portfolioPublicDetails;
|
||||
|
||||
this.initializeAnalysisData();
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
public initializeAnalysisData() {
|
||||
this.continents = {
|
||||
[UNKNOWN_KEY]: {
|
||||
name: UNKNOWN_KEY,
|
||||
value: 0
|
||||
}
|
||||
};
|
||||
this.countries = {
|
||||
[UNKNOWN_KEY]: {
|
||||
name: UNKNOWN_KEY,
|
||||
value: 0
|
||||
}
|
||||
};
|
||||
this.positions = {};
|
||||
this.sectors = {
|
||||
[UNKNOWN_KEY]: {
|
||||
name: UNKNOWN_KEY,
|
||||
value: 0
|
||||
}
|
||||
};
|
||||
this.symbols = {
|
||||
[UNKNOWN_KEY]: {
|
||||
name: UNKNOWN_KEY,
|
||||
symbol: UNKNOWN_KEY,
|
||||
value: 0
|
||||
}
|
||||
};
|
||||
|
||||
for (const [symbol, position] of Object.entries(
|
||||
this.portfolioPublicDetails.holdings
|
||||
)) {
|
||||
const value = position.allocationCurrent;
|
||||
|
||||
this.positions[symbol] = {
|
||||
value,
|
||||
name: position.name
|
||||
};
|
||||
|
||||
if (position.countries.length > 0) {
|
||||
for (const country of position.countries) {
|
||||
const { code, continent, name, weight } = country;
|
||||
|
||||
if (this.continents[continent]?.value) {
|
||||
this.continents[continent].value += weight * position.value;
|
||||
} else {
|
||||
this.continents[continent] = {
|
||||
name: continent,
|
||||
value: weight * this.portfolioPublicDetails.holdings[symbol].value
|
||||
};
|
||||
}
|
||||
|
||||
if (this.countries[code]?.value) {
|
||||
this.countries[code].value += weight * position.value;
|
||||
} else {
|
||||
this.countries[code] = {
|
||||
name,
|
||||
value: weight * this.portfolioPublicDetails.holdings[symbol].value
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.continents[UNKNOWN_KEY].value +=
|
||||
this.portfolioPublicDetails.holdings[symbol].value;
|
||||
|
||||
this.countries[UNKNOWN_KEY].value +=
|
||||
this.portfolioPublicDetails.holdings[symbol].value;
|
||||
}
|
||||
|
||||
if (position.sectors.length > 0) {
|
||||
for (const sector of position.sectors) {
|
||||
const { name, weight } = sector;
|
||||
|
||||
if (this.sectors[name]?.value) {
|
||||
this.sectors[name].value += weight * position.value;
|
||||
} else {
|
||||
this.sectors[name] = {
|
||||
name,
|
||||
value: weight * this.portfolioPublicDetails.holdings[symbol].value
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.sectors[UNKNOWN_KEY].value +=
|
||||
this.portfolioPublicDetails.holdings[symbol].value;
|
||||
}
|
||||
|
||||
this.symbols[symbol] = {
|
||||
symbol,
|
||||
name: position.name,
|
||||
value: position.value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>Portfolio</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="proportion-charts row">
|
||||
<div class="col-md-12 allocations-by-symbol">
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-content>
|
||||
<gf-portfolio-proportion-chart
|
||||
class="mx-auto"
|
||||
[isInPercent]="true"
|
||||
[keys]="['symbol']"
|
||||
[positions]="symbols"
|
||||
[showLabels]="deviceType !== 'mobile'"
|
||||
></gf-portfolio-proportion-chart>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-5">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<h2 class="h4 mb-1 text-center">
|
||||
Would you like to <strong>refine</strong> your
|
||||
<strong>personal investment strategy</strong>?
|
||||
</h2>
|
||||
<p class="lead mb-3 text-center" i18n>
|
||||
Ghostfolio empowers you to keep track of your wealth.
|
||||
</p>
|
||||
<div class="py-2 text-center">
|
||||
<a color="primary" i18n mat-flat-button [routerLink]="['/']">
|
||||
Get Started
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,23 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
||||
|
||||
import { PublicPageRoutingModule } from './public-page-routing.module';
|
||||
import { PublicPageComponent } from './public-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [PublicPageComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfPortfolioProportionChartModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
PublicPageRoutingModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class PublicPageModule {}
|
@ -0,0 +1,12 @@
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
|
||||
gf-portfolio-proportion-chart {
|
||||
max-width: 80vh;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
color: rgb(var(--light-primary-text));
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
export interface Access {
|
||||
granteeAlias: string;
|
||||
id: string;
|
||||
type: 'PUBLIC' | 'RESTRICTED_VIEW';
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
|
||||
export interface PortfolioPublicDetails {
|
||||
holdings: {
|
||||
[symbol: string]: Pick<
|
||||
PortfolioPosition,
|
||||
'allocationCurrent' | 'countries' | 'name' | 'sectors' | 'value'
|
||||
>;
|
||||
};
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Access" ALTER COLUMN "granteeUserId" DROP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Account" ALTER COLUMN "currency" DROP NOT NULL;
|
Loading…
Reference in new issue