mirror of https://github.com/ghostfolio/ghostfolio
Thomas Kaul
3 years ago
committed by
GitHub
22 changed files with 439 additions and 27 deletions
@ -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