Feature/migrate client to control flow (#3475)

* Migrate to control flow

* Update changelog
pull/3481/head^2
Thomas Kaul 7 months ago committed by GitHub
parent 88c420ca5e
commit b725e6e2ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Migrated the `@ghostfolio/client` components to control flow
- Improved the language localization for German (`de`) - Improved the language localization for German (`de`)
- Upgraded `angular` from version `17.3.10` to `18.0.2` - Upgraded `angular` from version `17.3.10` to `18.0.2`
- Upgraded `Nx` from version `19.0.5` to `19.2.2` - Upgraded `Nx` from version `19.0.5` to `19.2.2`

@ -1,33 +1,31 @@
<header> <header>
<div @if (canCreateAccount || user?.systemMessage) {
*ngIf="canCreateAccount || user?.systemMessage" <div class="info-message-container">
class="info-message-container" <div class="info-message-inner-container position-fixed w-100">
> <div class="align-items-center d-flex h-100 justify-content-center">
<div class="info-message-inner-container position-fixed w-100"> @if (canCreateAccount) {
<div class="align-items-center d-flex h-100 justify-content-center"> <a class="text-center" [routerLink]="routerLinkRegister">
<a <div
*ngIf="canCreateAccount" class="cursor-pointer d-inline-block info-message"
class="text-center" (click)="onCreateAccount()"
[routerLink]="routerLinkRegister" >
> <span i18n>You are using the Live Demo.</span>
<div <span class="a ml-2" i18n>Create Account</span>
class="cursor-pointer d-inline-block info-message" </div></a
(click)="onCreateAccount()" >
> }
<span i18n>You are using the Live Demo.</span> @if (!canCreateAccount && user?.systemMessage) {
<span class="a ml-2" i18n>Create Account</span> <div
</div></a class="cursor-pointer d-inline-block info-message text-truncate"
> (click)="onClickSystemMessage()"
<div >
*ngIf="!canCreateAccount && user?.systemMessage" {{ user.systemMessage.message }}
class="cursor-pointer d-inline-block info-message text-truncate" </div>
(click)="onClickSystemMessage()" }
>
{{ user.systemMessage.message }}
</div> </div>
</div> </div>
</div> </div>
</div> }
<gf-header <gf-header
class="position-fixed w-100" class="position-fixed w-100"
@ -45,144 +43,159 @@
<router-outlet></router-outlet> <router-outlet></router-outlet>
</main> </main>
<footer *ngIf="showFooter" class="d-flex justify-content-center py-4 w-100"> @if (showFooter) {
<div class="container"> <footer class="d-flex justify-content-center py-4 w-100">
<div class="mb-3 row"> <div class="container">
<div class="col-sm"> <div class="mb-3 row">
<a [routerLink]="['/']"><gf-logo /></a> <div class="col-sm">
</div> <a [routerLink]="['/']"><gf-logo /></a>
<div class="col-sm"> </div>
<div class="h6 mt-2" i18n>Personal Finance</div> <div class="col-sm">
<ul class="list-unstyled"> <div class="h6 mt-2" i18n>Personal Finance</div>
<li *ngIf="hasPermissionToAccessFearAndGreedIndex"> <ul class="list-unstyled">
<a i18n [routerLink]="routerLinkMarkets">Markets</a> @if (hasPermissionToAccessFearAndGreedIndex) {
</li> <li>
<li><a i18n [routerLink]="routerLinkResources">Resources</a></li> <a i18n [routerLink]="routerLinkMarkets">Markets</a>
</ul> </li>
</div> }
<div class="col-sm"> <li><a i18n [routerLink]="routerLinkResources">Resources</a></li>
<div class="h6 mt-2">Ghostfolio</div> </ul>
<ul class="list-unstyled"> </div>
<li><a i18n [routerLink]="routerLinkAbout">About</a></li> <div class="col-sm">
<li *ngIf="hasPermissionForSubscription"> <div class="h6 mt-2">Ghostfolio</div>
<a i18n [routerLink]="['/blog']">Blog</a> <ul class="list-unstyled">
</li> <li><a i18n [routerLink]="routerLinkAbout">About</a></li>
<li> @if (hasPermissionForSubscription) {
<a i18n [routerLink]="routerLinkAboutChangelog">Changelog</a> <li>
</li> <a i18n [routerLink]="['/blog']">Blog</a>
<li><a i18n [routerLink]="routerLinkFeatures">Features</a></li> </li>
<li *ngIf="hasPermissionForSubscription"> }
<a i18n [routerLink]="routerLinkFaq" <li>
>Frequently Asked Questions (FAQ)</a <a i18n [routerLink]="routerLinkAboutChangelog">Changelog</a>
> </li>
</li> <li><a i18n [routerLink]="routerLinkFeatures">Features</a></li>
<li> @if (hasPermissionForSubscription) {
<a i18n [routerLink]="routerLinkAboutLicense">License</a> <li>
</li> <a i18n [routerLink]="routerLinkFaq"
<li *ngIf="hasPermissionForStatistics"> >Frequently Asked Questions (FAQ)</a
<a [routerLink]="['/open']">Open Startup</a> >
</li> </li>
<li *ngIf="hasPermissionForSubscription"> }
<a i18n [routerLink]="routerLinkPricing">Pricing</a> <li>
</li> <a i18n [routerLink]="routerLinkAboutLicense">License</a>
<li *ngIf="hasPermissionForSubscription"> </li>
<a i18n [routerLink]="routerLinkAboutPrivacyPolicy" @if (hasPermissionForStatistics) {
>Privacy Policy</a <li>
> <a [routerLink]="['/open']">Open Startup</a>
</li> </li>
<li *ngIf="hasPermissionForSubscription"> }
<a @if (hasPermissionForSubscription) {
class="align-items-baseline d-flex" <li>
href="https://status.ghostfol.io" <a i18n [routerLink]="routerLinkPricing">Pricing</a>
target="_blank" </li>
title="Ghostfolio Status" }
>Status<ion-icon class="ml-1" name="open-outline" @if (hasPermissionForSubscription) {
/></a> <li>
</li> <a i18n [routerLink]="routerLinkAboutPrivacyPolicy"
</ul> >Privacy Policy</a
</div> >
<div class="col-sm"> </li>
<div class="h6 mt-2" i18n>Community</div> }
<ul class="list-unstyled"> @if (hasPermissionForSubscription) {
<li> <li>
<a <a
class="align-items-baseline d-flex" class="align-items-baseline d-flex"
href="https://github.com/ghostfolio/ghostfolio" href="https://status.ghostfol.io"
target="_blank" target="_blank"
title="Find Ghostfolio on GitHub" title="Ghostfolio Status"
>GitHub<ion-icon class="ml-1" name="open-outline" >Status<ion-icon class="ml-1" name="open-outline"
/></a> /></a>
</li> </li>
<li> }
<a </ul>
class="align-items-baseline d-flex" </div>
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg" <div class="col-sm">
target="_blank" <div class="h6 mt-2" i18n>Community</div>
title="Join the Ghostfolio Slack community" <ul class="list-unstyled">
>Slack<ion-icon class="ml-1" name="open-outline" <li>
/></a> <a
</li> class="align-items-baseline d-flex"
<li> href="https://github.com/ghostfolio/ghostfolio"
<a target="_blank"
class="align-items-baseline d-flex" title="Find Ghostfolio on GitHub"
href="https://twitter.com/ghostfolio_" >GitHub<ion-icon class="ml-1" name="open-outline"
target="_blank" /></a>
title="Follow Ghostfolio on X (formerly Twitter)" </li>
>X (formerly Twitter)<ion-icon class="ml-1" name="open-outline" <li>
/></a> <a
</li> class="align-items-baseline d-flex"
<li>&nbsp;</li> href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
<li> target="_blank"
<a href="../de" title="Ghostfolio in Deutsch">Deutsch</a> title="Join the Ghostfolio Slack community"
</li> >Slack<ion-icon class="ml-1" name="open-outline"
<li> /></a>
<a href="../en" title="Ghostfolio in English">English</a> </li>
</li> <li>
<li> <a
<a href="../es" title="Ghostfolio in Español">Español</a> class="align-items-baseline d-flex"
</li> href="https://twitter.com/ghostfolio_"
<li> target="_blank"
<a href="../fr" title="Ghostfolio en Français">Français</a> title="Follow Ghostfolio on X (formerly Twitter)"
</li> >X (formerly Twitter)<ion-icon class="ml-1" name="open-outline"
<li> /></a>
<a href="../it" title="Ghostfolio in Italiano">Italiano</a> </li>
</li> <li>&nbsp;</li>
<li>
<a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a>
</li>
<!--
<li> <li>
<a href="../pl" title="Ghostfolio in Polski">Polski</a> <a href="../de" title="Ghostfolio in Deutsch">Deutsch</a>
</li> </li>
-->
<li>
<a href="../pt" title="Ghostfolio in Português">Português</a>
</li>
<li>
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
</li>
<!--
<li> <li>
<a href="../zh" title="Ghostfolio in Chinese">Chinese</a> <a href="../en" title="Ghostfolio in English">English</a>
</li> </li>
--> <li>
</ul> <a href="../es" title="Ghostfolio in Español">Español</a>
</li>
<li>
<a href="../fr" title="Ghostfolio en Français">Français</a>
</li>
<li>
<a href="../it" title="Ghostfolio in Italiano">Italiano</a>
</li>
<li>
<a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a>
</li>
<!--
<li>
<a href="../pl" title="Ghostfolio in Polski">Polski</a>
</li>
-->
<li>
<a href="../pt" title="Ghostfolio in Português">Português</a>
</li>
<li>
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
</li>
<!--
<li>
<a href="../zh" title="Ghostfolio in Chinese">Chinese</a>
</li>
-->
</ul>
</div>
</div> </div>
</div> <div class="row text-center">
<div class="col">
<div class="row text-center"> © 2021 - {{ currentYear }}
<div class="col"> <a href="https://ghostfol.io">Ghostfolio</a>
© 2021 - {{ currentYear }} <a href="https://ghostfol.io">Ghostfolio</a> </div>
</div> </div>
</div> <div class="row text-center text-muted">
<div class="col">
<div class="row text-center text-muted"> <small i18n
<div class="col"> >The risk of loss in trading can be substantial. It is not advisable
<small i18n to invest money you may need in the short term.</small
>The risk of loss in trading can be substantial. It is not advisable >
to invest money you may need in the short term.</small </div>
>
</div> </div>
</div> </div>
</div> </footer>
</footer> }

@ -32,17 +32,16 @@
<ng-container matColumnDef="details"> <ng-container matColumnDef="details">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Details</th> <th *matHeaderCellDef class="px-1" i18n mat-header-cell>Details</th>
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell> <td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
<div @if (element.type === 'PUBLIC') {
*ngIf="element.type === 'PUBLIC'" <div class="align-items-center d-flex">
class="align-items-center d-flex" <ion-icon class="mr-1" name="link-outline" />
> <a
<ion-icon class="mr-1" name="link-outline" /> href="{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}"
<a target="_blank"
href="{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}" >{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}</a
target="_blank" >
>{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}</a </div>
> }
</div>
</td> </td>
</ng-container> </ng-container>

@ -1,14 +1,16 @@
<div *ngIf="showActions" class="d-flex justify-content-end"> @if (showActions) {
<button <div class="d-flex justify-content-end">
class="align-items-center d-flex" <button
mat-stroked-button class="align-items-center d-flex"
[disabled]="dataSource?.data.length < 2" mat-stroked-button
(click)="onTransferBalance()" [disabled]="dataSource?.data.length < 2"
> (click)="onTransferBalance()"
<ion-icon class="mr-2" name="arrow-redo-outline" /> >
<ng-container i18n>Transfer Cash Balance</ng-container>... <ion-icon class="mr-2" name="arrow-redo-outline" />
</button> <ng-container i18n>Transfer Cash Balance</ng-container>...
</div> </button>
</div>
}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="gf-table w-100" mat-table matSort [dataSource]="dataSource"> <table class="gf-table w-100" mat-table matSort [dataSource]="dataSource">
@ -24,7 +26,9 @@
mat-cell mat-cell
> >
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<ion-icon *ngIf="element.isExcluded" name="eye-off-outline" /> @if (element.isExcluded) {
<ion-icon name="eye-off-outline" />
}
</div> </div>
</td> </td>
<td <td
@ -39,12 +43,13 @@
<ng-container i18n>Name</ng-container> <ng-container i18n>Name</ng-container>
</th> </th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<gf-asset-profile-icon @if (element.Platform?.url) {
*ngIf="element.Platform?.url" <gf-asset-profile-icon
class="d-inline d-sm-none mr-1" class="d-inline d-sm-none mr-1"
[tooltip]="element.Platform?.name" [tooltip]="element.Platform?.name"
[url]="element.Platform?.url" [url]="element.Platform?.url"
/> />
}
<span>{{ element.name }}</span> <span>{{ element.name }}</span>
</td> </td>
<td *matFooterCellDef class="px-1" i18n mat-footer-cell>Total</td> <td *matFooterCellDef class="px-1" i18n mat-footer-cell>Total</td>
@ -86,12 +91,13 @@
mat-cell mat-cell
> >
<div class="d-flex"> <div class="d-flex">
<gf-asset-profile-icon @if (element.Platform?.url) {
*ngIf="element.Platform?.url" <gf-asset-profile-icon
class="mr-1" class="mr-1"
[tooltip]="element.Platform?.name" [tooltip]="element.Platform?.name"
[url]="element.Platform?.url" [url]="element.Platform?.url"
/> />
}
<span>{{ element.Platform?.name }}</span> <span>{{ element.Platform?.name }}</span>
</div> </div>
</td> </td>
@ -236,15 +242,16 @@
class="d-none d-lg-table-cell px-1" class="d-none d-lg-table-cell px-1"
mat-cell mat-cell
> >
<button @if (element.comment) {
*ngIf="element.comment" <button
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
mat-button mat-button
title="Note" title="Note"
(click)="onOpenComment(element.comment); $event.stopPropagation()" (click)="onOpenComment(element.comment); $event.stopPropagation()"
> >
<ion-icon name="document-text-outline" /> <ion-icon name="document-text-outline" />
</button> </button>
}
</td> </td>
<td <td
*matFooterCellDef *matFooterCellDef
@ -303,12 +310,13 @@
</table> </table>
</div> </div>
<ngx-skeleton-loader @if (isLoading) {
*ngIf="isLoading" <ngx-skeleton-loader
animation="pulse" animation="pulse"
class="px-4 py-3" class="px-4 py-3"
[theme]="{ [theme]="{
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'
}" }"
/> />
}

@ -5,11 +5,14 @@
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-select formControlName="status"> <mat-select formControlName="status">
<mat-option /> <mat-option />
<mat-option @for (
*ngFor="let statusFilterOption of statusFilterOptions" statusFilterOption of statusFilterOptions;
[value]="statusFilterOption" track statusFilterOption
>{{ statusFilterOption }}</mat-option ) {
> <mat-option [value]="statusFilterOption">{{
statusFilterOption
}}</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</form> </form>
@ -28,15 +31,11 @@
<ng-container i18n>Type</ng-container> <ng-container i18n>Type</ng-container>
</th> </th>
<td *matCellDef="let element" class="px-1 py-2" mat-cell> <td *matCellDef="let element" class="px-1 py-2" mat-cell>
<ng-container *ngIf="element.name === 'GATHER_ASSET_PROFILE'" i18n> @if (element.name === 'GATHER_ASSET_PROFILE') {
Asset Profile <ng-container i18n>Asset Profile</ng-container>
</ng-container> } @else if (element.name === 'GATHER_HISTORICAL_MARKET_DATA') {
<ng-container <ng-container i18n>Historical Market Data</ng-container>
*ngIf="element.name === 'GATHER_HISTORICAL_MARKET_DATA'" }
i18n
>
Historical Market Data
</ng-container>
</td> </td>
</ng-container> </ng-container>
@ -109,37 +108,29 @@
<ng-container i18n>Status</ng-container> <ng-container i18n>Status</ng-container>
</th> </th>
<td *matCellDef="let element" class="px-1 py-2" mat-cell> <td *matCellDef="let element" class="px-1 py-2" mat-cell>
<ion-icon @if (element.state === 'active') {
*ngIf="element.state === 'active'" <ion-icon class="h6 mb-0" name="play-outline" />
class="h6 mb-0" } @else if (element.state === 'completed') {
name="play-outline" <ion-icon
/> class="h6 mb-0 text-success"
<ion-icon name="checkmark-circle-outline"
*ngIf="element.state === 'completed'" />
class="h6 mb-0 text-success" } @else if (element.state === 'delayed') {
name="checkmark-circle-outline" <ion-icon
/> class="h6 mb-0"
<ion-icon name="time-outline"
*ngIf="element.state === 'delayed'" [ngClass]="{ 'text-danger': element.stacktrace?.length > 0 }"
class="h6 mb-0" />
name="time-outline" } @else if (element.state === 'failed') {
[ngClass]="{ 'text-danger': element.stacktrace?.length > 0 }" <ion-icon
/> class="h6 mb-0 text-danger"
<ion-icon name="alert-circle-outline"
*ngIf="element.state === 'failed'" />
class="h6 mb-0 text-danger" } @else if (element.state === 'paused') {
name="alert-circle-outline" <ion-icon class="h6 mb-0" name="pause-outline" />
/> } @else if (element.state === 'waiting') {
<ion-icon <ion-icon class="h6 mb-0" name="cafe-outline" />
*ngIf="element.state === 'paused'" }
class="h6 mb-0"
name="pause-outline"
/>
<ion-icon
*ngIf="element.state === 'waiting'"
class="h6 mb-0"
name="cafe-outline"
/>
</td> </td>
</ng-container> </ng-container>

@ -9,35 +9,38 @@
[showYAxis]="true" [showYAxis]="true"
[symbol]="symbol" [symbol]="symbol"
/> />
<div *ngFor="let itemByMonth of marketDataByMonth | keyvalue" class="d-flex"> @for (itemByMonth of marketDataByMonth | keyvalue; track itemByMonth) {
<div class="date px-1 text-nowrap">{{ itemByMonth.key }}</div> <div class="d-flex">
<div class="align-items-center d-flex flex-grow-1 px-1"> <div class="date px-1 text-nowrap">{{ itemByMonth.key }}</div>
<div <div class="align-items-center d-flex flex-grow-1 px-1">
*ngFor="let dayItem of days; let i = index" @for (dayItem of days; track dayItem; let i = $index) {
class="day" <div
[ngClass]="{ class="day"
'cursor-pointer valid': isDateOfInterest( [ngClass]="{
itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1) 'cursor-pointer valid': isDateOfInterest(
), itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1)
available: ),
marketDataByMonth[itemByMonth.key][ available:
i + 1 < 10 ? '0' + (i + 1) : i + 1 marketDataByMonth[itemByMonth.key][
]?.marketPrice, i + 1 < 10 ? '0' + (i + 1) : i + 1
today: isToday( ]?.marketPrice,
itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1) today: isToday(
) itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1)
}" )
[title]=" }"
(itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1) [title]="
| date: defaultDateFormat) ?? '' (itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1)
" | date: defaultDateFormat) ?? ''
(click)=" "
onOpenMarketDataDetail({ (click)="
day: i + 1 < 10 ? '0' + (i + 1) : i + 1, onOpenMarketDataDetail({
yearMonth: itemByMonth.key day: i + 1 < 10 ? '0' + (i + 1) : i + 1,
}) yearMonth: itemByMonth.key
" })
></div> "
></div>
}
</div>
</div> </div>
</div> }
</div> </div>

@ -39,9 +39,13 @@
</th> </th>
<td *matCellDef="let element" class="line-height-1 px-1" mat-cell> <td *matCellDef="let element" class="line-height-1 px-1" mat-cell>
<div class="text-truncate">{{ element.name }}</div> <div class="text-truncate">{{ element.name }}</div>
<div *ngIf="!isUUID(element.symbol)"> @if (!isUUID(element.symbol)) {
<small class="text-muted">{{ element.symbol | gfSymbol }}</small> <div>
</div> <small class="text-muted">{{
element.symbol | gfSymbol
}}</small>
</div>
}
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td> <td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container> </ng-container>
@ -121,11 +125,9 @@
<ng-container matColumnDef="comment"> <ng-container matColumnDef="comment">
<th *matHeaderCellDef class="px-1" mat-header-cell></th> <th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<ion-icon @if (element.comment) {
*ngIf="element.comment" <ion-icon class="d-block" name="document-text-outline" />
class="d-block" }
name="document-text-outline"
/>
</td> </td>
</ng-container> </ng-container>
@ -222,15 +224,16 @@
(page)="onChangePage($event)" (page)="onChangePage($event)"
/> />
<ngx-skeleton-loader @if (isLoading && totalItems === 0) {
*ngIf="isLoading && totalItems === 0" <ngx-skeleton-loader
animation="pulse" animation="pulse"
class="px-4 py-3" class="px-4 py-3"
[theme]="{ [theme]="{
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'
}" }"
/> />
}
</div> </div>
</div> </div>

@ -243,11 +243,11 @@
<mat-label i18n>Asset Class</mat-label> <mat-label i18n>Asset Class</mat-label>
<mat-select formControlName="assetClass"> <mat-select formControlName="assetClass">
<mat-option [value]="null" /> <mat-option [value]="null" />
<mat-option @for (assetClass of assetClasses; track assetClass) {
*ngFor="let assetClass of assetClasses" <mat-option [value]="assetClass.id">{{
[value]="assetClass.id" assetClass.label
>{{ assetClass.label }}</mat-option }}</mat-option>
> }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -256,11 +256,11 @@
<mat-label i18n>Asset Sub Class</mat-label> <mat-label i18n>Asset Sub Class</mat-label>
<mat-select formControlName="assetSubClass"> <mat-select formControlName="assetSubClass">
<mat-option [value]="null" /> <mat-option [value]="null" />
<mat-option @for (assetSubClass of assetSubClasses; track assetSubClass) {
*ngFor="let assetSubClass of assetSubClasses" <mat-option [value]="assetSubClass.id">{{
[value]="assetSubClass.id" assetSubClass.label
>{{ assetSubClass.label }}</mat-option }}</mat-option>
> }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>

@ -20,21 +20,24 @@
</mat-radio-group> </mat-radio-group>
</div> </div>
<div *ngIf="mode === 'auto'"> @if (mode === 'auto') {
<mat-form-field appearance="outline" class="w-100"> <div>
<mat-label i18n>Name, symbol or ISIN</mat-label> <mat-form-field appearance="outline" class="w-100">
<gf-symbol-autocomplete <mat-label i18n>Name, symbol or ISIN</mat-label>
formControlName="searchSymbol" <gf-symbol-autocomplete
[includeIndices]="true" formControlName="searchSymbol"
/> [includeIndices]="true"
</mat-form-field> />
</div> </mat-form-field>
<div *ngIf="mode === 'manual'"> </div>
<mat-form-field appearance="outline" class="w-100"> } @else if (mode === 'manual') {
<mat-label i18n>Symbol</mat-label> <div>
<input formControlName="addSymbol" matInput /> <mat-form-field appearance="outline" class="w-100">
</mat-form-field> <mat-label i18n>Symbol</mat-label>
</div> <input formControlName="addSymbol" matInput />
</mat-form-field>
</div>
}
</div> </div>
<div class="d-flex justify-content-end" mat-dialog-actions> <div class="d-flex justify-content-end" mat-dialog-actions>
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button> <button i18n mat-button type="button" (click)="onCancel()">Cancel</button>

@ -27,72 +27,77 @@
[precision]="0" [precision]="0"
[value]="transactionCount" [value]="transactionCount"
/> />
<div *ngIf="transactionCount && userCount"> @if (transactionCount && userCount) {
{{ transactionCount / userCount | number: '1.2-2' }} <div>
<span i18n>per User</span> {{ transactionCount / userCount | number: '1.2-2' }}
</div> <span i18n>per User</span>
</div>
}
</div> </div>
</div> </div>
<div class="align-items-start d-flex my-3"> <div class="align-items-start d-flex my-3">
<div class="w-50" i18n>Exchange Rates</div> <div class="w-50" i18n>Exchange Rates</div>
<div class="w-50"> <div class="w-50">
<table> <table>
<tr *ngFor="let exchangeRate of exchangeRates"> @for (exchangeRate of exchangeRates; track exchangeRate) {
<td> <tr>
<gf-value [locale]="user?.settings?.locale" [value]="1" /> <td>
</td> <gf-value [locale]="user?.settings?.locale" [value]="1" />
<td class="pl-1">{{ exchangeRate.label1 }}</td> </td>
<td class="px-1">=</td> <td class="pl-1">{{ exchangeRate.label1 }}</td>
<td align="right"> <td class="px-1">=</td>
<gf-value <td align="right">
class="d-inline-block" <gf-value
[locale]="user?.settings?.locale" class="d-inline-block"
[precision]="4" [locale]="user?.settings?.locale"
[value]="exchangeRate.value" [precision]="4"
/> [value]="exchangeRate.value"
</td> />
<td class="pl-1">{{ exchangeRate.label2 }}</td> </td>
<td> <td class="pl-1">{{ exchangeRate.label2 }}</td>
<button <td>
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="exchangeRateActionsMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-horizontal" />
</button>
<mat-menu
#exchangeRateActionsMenu="matMenu"
class="h-100 mx-1 no-min-width px-2"
xPosition="before"
>
<a
mat-menu-item
[queryParams]="{
assetProfileDialog: true,
dataSource: exchangeRate.dataSource,
symbol: exchangeRate.symbol
}"
[routerLink]="['/admin', 'market-data']"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
<span i18n>Edit</span>
</span>
</a>
<button <button
*ngIf="customCurrencies.includes(exchangeRate.label2)" class="mx-1 no-min-width px-2"
mat-menu-item mat-button
(click)="onDeleteCurrency(exchangeRate.label2)" [matMenuTriggerFor]="exchangeRateActionsMenu"
(click)="$event.stopPropagation()"
> >
<span class="align-items-center d-flex"> <ion-icon name="ellipsis-horizontal" />
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Delete</span>
</span>
</button> </button>
</mat-menu> <mat-menu
</td> #exchangeRateActionsMenu="matMenu"
</tr> class="h-100 mx-1 no-min-width px-2"
xPosition="before"
>
<a
mat-menu-item
[queryParams]="{
assetProfileDialog: true,
dataSource: exchangeRate.dataSource,
symbol: exchangeRate.symbol
}"
[routerLink]="['/admin', 'market-data']"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
<span i18n>Edit</span>
</span>
</a>
@if (customCurrencies.includes(exchangeRate.label2)) {
<button
mat-menu-item
(click)="onDeleteCurrency(exchangeRate.label2)"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Delete</span>
</span>
</button>
}
</mat-menu>
</td>
</tr>
}
</table> </table>
<div class="mt-2"> <div class="mt-2">
<button <button
@ -119,17 +124,19 @@
/> />
</div> </div>
</div> </div>
<div *ngIf="hasPermissionToToggleReadOnlyMode" class="d-flex my-3"> @if (hasPermissionToToggleReadOnlyMode) {
<div class="w-50" i18n>Read-only Mode</div> <div class="d-flex my-3">
<div class="w-50"> <div class="w-50" i18n>Read-only Mode</div>
<mat-slide-toggle <div class="w-50">
color="primary" <mat-slide-toggle
hideIcon="true" color="primary"
[checked]="info?.isReadOnlyMode" hideIcon="true"
(change)="onReadOnlyModeChange($event)" [checked]="info?.isReadOnlyMode"
/> (change)="onReadOnlyModeChange($event)"
/>
</div>
</div> </div>
</div> }
<div class="d-flex my-3"> <div class="d-flex my-3">
<div class="w-50" i18n>Data Gathering</div> <div class="w-50" i18n>Data Gathering</div>
<div class="w-50"> <div class="w-50">
@ -141,99 +148,105 @@
/> />
</div> </div>
</div> </div>
<div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3"> @if (hasPermissionForSystemMessage) {
<div class="w-50" i18n>System Message</div> <div class="d-flex my-3">
<div class="w-50"> <div class="w-50" i18n>System Message</div>
<div *ngIf="systemMessage" class="align-items-center d-flex"> <div class="w-50">
<div class="text-truncate">{{ systemMessage | json }}</div> @if (systemMessage) {
<button <div class="align-items-center d-flex">
class="h-100 mx-1 no-min-width px-2" <div class="text-truncate">{{ systemMessage | json }}</div>
mat-button
(click)="onDeleteSystemMessage()"
>
<ion-icon name="trash-outline" />
</button>
</div>
<button
*ngIf="!info?.systemMessage"
class="mt-2"
color="accent"
mat-flat-button
(click)="onSetSystemMessage()"
>
<ion-icon class="mr-1" name="information-circle-outline" />
<span i18n>Set Message</span>
</button>
</div>
</div>
<div
*ngIf="hasPermissionForSubscription"
class="d-flex my-3 subscription"
>
<div class="w-50" i18n>Coupons</div>
<div class="w-50">
<table>
<tr *ngFor="let coupon of coupons">
<td class="text-monospace">{{ coupon.code }}</td>
<td class="pl-2 text-right">{{ coupon.duration }}</td>
<td>
<button <button
class="mx-1 no-min-width px-2" class="h-100 mx-1 no-min-width px-2"
mat-button mat-button
[matMenuTriggerFor]="couponActionsMenu" (click)="onDeleteSystemMessage()"
(click)="$event.stopPropagation()"
> >
<ion-icon name="ellipsis-horizontal" /> <ion-icon name="trash-outline" />
</button> </button>
<mat-menu </div>
#couponActionsMenu="matMenu" }
class="h-100 mx-1 no-min-width px-2" @if (!info?.systemMessage) {
xPosition="before"
>
<button
mat-menu-item
(click)="onDeleteCoupon(coupon.code)"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Delete</span>
</span>
</button>
</mat-menu>
</td>
</tr>
</table>
<div class="mt-2">
<form #couponForm="ngForm" class="align-items-center d-flex">
<mat-form-field
appearance="outline"
class="mr-2 without-hint"
>
<mat-select
name="duration"
[value]="couponDuration"
(selectionChange)="onChangeCouponDuration($event.value)"
>
<mat-option value="7 days">7 Days</mat-option>
<mat-option value="14 days">14 Days</mat-option>
<mat-option value="30 days">30 Days</mat-option>
<mat-option value="90 days">90 Days</mat-option>
<mat-option value="180 days">180 Days</mat-option>
<mat-option value="1 year">1 Year</mat-option>
</mat-select>
</mat-form-field>
<button <button
class="mt-1" class="mt-2"
color="primary" color="accent"
mat-flat-button mat-flat-button
(click)="onAddCoupon()" (click)="onSetSystemMessage()"
> >
<span i18n>Add</span> <ion-icon class="mr-1" name="information-circle-outline" />
<span i18n>Set Message</span>
</button> </button>
</form> }
</div> </div>
</div> </div>
</div> }
@if (hasPermissionForSubscription) {
<div class="d-flex my-3 subscription">
<div class="w-50" i18n>Coupons</div>
<div class="w-50">
<table>
@for (coupon of coupons; track coupon) {
<tr>
<td class="text-monospace">{{ coupon.code }}</td>
<td class="pl-2 text-right">{{ coupon.duration }}</td>
<td>
<button
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="couponActionsMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-horizontal" />
</button>
<mat-menu
#couponActionsMenu="matMenu"
class="h-100 mx-1 no-min-width px-2"
xPosition="before"
>
<button
mat-menu-item
(click)="onDeleteCoupon(coupon.code)"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Delete</span>
</span>
</button>
</mat-menu>
</td>
</tr>
}
</table>
<div class="mt-2">
<form #couponForm="ngForm" class="align-items-center d-flex">
<mat-form-field
appearance="outline"
class="mr-2 without-hint"
>
<mat-select
name="duration"
[value]="couponDuration"
(selectionChange)="onChangeCouponDuration($event.value)"
>
<mat-option value="7 days">7 Days</mat-option>
<mat-option value="14 days">14 Days</mat-option>
<mat-option value="30 days">30 Days</mat-option>
<mat-option value="90 days">90 Days</mat-option>
<mat-option value="180 days">180 Days</mat-option>
<mat-option value="1 year">1 Year</mat-option>
</mat-select>
</mat-form-field>
<button
class="mt-1"
color="primary"
mat-flat-button
(click)="onAddCoupon()"
>
<span i18n>Add</span>
</button>
</form>
</div>
</div>
</div>
}
<div class="d-flex my-3"> <div class="d-flex my-3">
<div class="w-50" i18n>Housekeeping</div> <div class="w-50" i18n>Housekeeping</div>
<div class="w-50"> <div class="w-50">

@ -30,12 +30,13 @@
<ng-container i18n>Name</ng-container> <ng-container i18n>Name</ng-container>
</th> </th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<gf-asset-profile-icon @if (element.url) {
*ngIf="element.url" <gf-asset-profile-icon
class="d-inline mr-1" class="d-inline mr-1"
[tooltip]="element.name" [tooltip]="element.name"
[url]="element.url" [url]="element.url"
/> />
}
<span>{{ element.name }}</span> <span>{{ element.name }}</span>
</td></ng-container </td></ng-container
> >

@ -4,8 +4,11 @@
(keyup.enter)="platformForm.valid && onSubmit()" (keyup.enter)="platformForm.valid && onSubmit()"
(ngSubmit)="onSubmit()" (ngSubmit)="onSubmit()"
> >
<h1 *ngIf="data.platform.id" i18n mat-dialog-title>Update platform</h1> @if (data.platform.id) {
<h1 *ngIf="!data.platform.id" i18n mat-dialog-title>Add platform</h1> <h1 i18n mat-dialog-title>Update platform</h1>
} @else {
<h1 i18n mat-dialog-title>Add platform</h1>
}
<div class="flex-grow-1 py-3" mat-dialog-content> <div class="flex-grow-1 py-3" mat-dialog-content>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">

@ -4,8 +4,11 @@
(keyup.enter)="tagForm.valid && onSubmit()" (keyup.enter)="tagForm.valid && onSubmit()"
(ngSubmit)="onSubmit()" (ngSubmit)="onSubmit()"
> >
<h1 *ngIf="data.tag.id" i18n mat-dialog-title>Update tag</h1> @if (data.tag.id) {
<h1 *ngIf="!data.tag.id" i18n mat-dialog-title>Add tag</h1> <h1 i18n mat-dialog-title>Update tag</h1>
} @else {
<h1 i18n mat-dialog-title>Add tag</h1>
}
<div class="flex-grow-1 py-3" mat-dialog-content> <div class="flex-grow-1 py-3" mat-dialog-content>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">

@ -49,43 +49,44 @@
}" }"
>{{ (element.id | slice: 0 : 5) + '...' }}</span >{{ (element.id | slice: 0 : 5) + '...' }}</span
> >
<gf-premium-indicator @if (element?.subscription?.type === 'Premium') {
*ngIf="element?.subscription?.type === 'Premium'" <gf-premium-indicator
class="ml-1" class="ml-1"
[enableLink]="false" [enableLink]="false"
[title]=" [title]="
'Expires ' + 'Expires ' +
formatDistanceToNow(element.subscription.expiresAt) + formatDistanceToNow(element.subscription.expiresAt) +
' (' + ' (' +
(element.subscription.expiresAt | date: defaultDateFormat) + (element.subscription.expiresAt
')' | date: defaultDateFormat) +
" ')'
/> "
/>
}
</div> </div>
</td> </td>
</ng-container> </ng-container>
<ng-container @if (hasPermissionForSubscription) {
*ngIf="hasPermissionForSubscription" <ng-container matColumnDef="country">
matColumnDef="country" <th
> *matHeaderCellDef
<th class="mat-mdc-header-cell px-1 py-2"
*matHeaderCellDef mat-header-cell
class="mat-mdc-header-cell px-1 py-2" >
mat-header-cell <ng-container i18n>Country</ng-container>
> </th>
<ng-container i18n>Country</ng-container> <td
</th> *matCellDef="let element"
<td class="mat-mdc-cell px-1 py-2"
*matCellDef="let element" mat-cell
class="mat-mdc-cell px-1 py-2" >
mat-cell <span class="h5" [title]="element.country">{{
> getEmojiFlag(element.country)
<span class="h5" [title]="element.country">{{ }}</span>
getEmojiFlag(element.country) </td>
}}</span> </ng-container>
</td> }
</ng-container>
<ng-container matColumnDef="registration"> <ng-container matColumnDef="registration">
<th <th
@ -146,51 +147,49 @@
</td> </td>
</ng-container> </ng-container>
<ng-container @if (hasPermissionForSubscription) {
*ngIf="hasPermissionForSubscription" <ng-container matColumnDef="engagementPerDay">
matColumnDef="engagementPerDay" <th
> *matHeaderCellDef
<th class="mat-mdc-header-cell px-1 py-2 text-right"
*matHeaderCellDef mat-header-cell
class="mat-mdc-header-cell px-1 py-2 text-right" >
mat-header-cell <ng-container i18n>Engagement per Day</ng-container>
> </th>
<ng-container i18n>Engagement per Day</ng-container> <td
</th> *matCellDef="let element"
<td class="mat-mdc-cell px-1 py-2 text-right"
*matCellDef="let element" mat-cell
class="mat-mdc-cell px-1 py-2 text-right" >
mat-cell <gf-value
> class="d-inline-block justify-content-end"
<gf-value [locale]="user?.settings?.locale"
class="d-inline-block justify-content-end" [precision]="0"
[locale]="user?.settings?.locale" [value]="element.engagement"
[precision]="0" />
[value]="element.engagement" </td>
/> </ng-container>
</td> }
</ng-container>
<ng-container @if (hasPermissionForSubscription) {
*ngIf="hasPermissionForSubscription" <ng-container matColumnDef="lastRequest">
matColumnDef="lastRequest" <th
> *matHeaderCellDef
<th class="mat-mdc-header-cell px-1 py-2"
*matHeaderCellDef i18n
class="mat-mdc-header-cell px-1 py-2" mat-header-cell
i18n >
mat-header-cell Last Request
> </th>
Last Request <td
</th> *matCellDef="let element"
<td class="mat-mdc-cell px-1 py-2"
*matCellDef="let element" mat-cell
class="mat-mdc-cell px-1 py-2" >
mat-cell {{ formatDistanceToNow(element.lastActivity) }}
> </td>
{{ formatDistanceToNow(element.lastActivity) }} </ng-container>
</td> }
</ng-container>
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
<th <th
@ -212,16 +211,14 @@
<ion-icon name="ellipsis-horizontal" /> <ion-icon name="ellipsis-horizontal" />
</button> </button>
<mat-menu #userMenu="matMenu" xPosition="before"> <mat-menu #userMenu="matMenu" xPosition="before">
<button @if (hasPermissionToImpersonateAllUsers) {
*ngIf="hasPermissionToImpersonateAllUsers" <button mat-menu-item (click)="onImpersonateUser(element.id)">
mat-menu-item <span class="align-items-center d-flex">
(click)="onImpersonateUser(element.id)" <ion-icon class="mr-2" name="contract-outline" />
> <span i18n>Impersonate User</span>
<span class="align-items-center d-flex"> </span>
<ion-icon class="mr-2" name="contract-outline" /> </button>
<span i18n>Impersonate User</span> }
</span>
</button>
<button <button
mat-menu-item mat-menu-item
[disabled]="element.id === user?.id" [disabled]="element.id === user?.id"

@ -4,10 +4,9 @@
class="align-items-center d-flex flex-grow-1 h5 mb-0 py-2 text-truncate" class="align-items-center d-flex flex-grow-1 h5 mb-0 py-2 text-truncate"
> >
<span i18n>Performance</span> <span i18n>Performance</span>
<gf-premium-indicator @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <gf-premium-indicator class="ml-1" />
class="ml-1" }
/>
</div> </div>
</div> </div>
<div class="col-md-6 col-xs-12 d-flex justify-content-end"> <div class="col-md-6 col-xs-12 d-flex justify-content-end">
@ -24,33 +23,33 @@
(selectionChange)="onChangeBenchmark($event.value)" (selectionChange)="onChangeBenchmark($event.value)"
> >
<mat-option [value]="null" /> <mat-option [value]="null" />
<mat-option @for (symbolProfile of benchmarks; track symbolProfile) {
*ngFor="let symbolProfile of benchmarks" <mat-option [value]="symbolProfile.id">{{
[value]="symbolProfile.id" symbolProfile.name
>{{ symbolProfile.name }}</mat-option }}</mat-option>
> }
<mat-option @if (hasPermissionToAccessAdminControl) {
*ngIf="hasPermissionToAccessAdminControl" <mat-option [routerLink]="['/admin', 'market-data']">
[routerLink]="['/admin', 'market-data']" <div class="align-items-center d-flex">
> <ion-icon class="mr-2 text-muted" name="arrow-forward-outline" />
<div class="align-items-center d-flex"> <span i18n>Manage Benchmarks</span>
<ion-icon class="mr-2 text-muted" name="arrow-forward-outline" /> </div>
<span i18n>Manage Benchmarks</span> </mat-option>
</div> }
</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div class="chart-container"> <div class="chart-container">
<ngx-skeleton-loader @if (isLoading) {
*ngIf="isLoading" <ngx-skeleton-loader
animation="pulse" animation="pulse"
[theme]="{ [theme]="{
height: '100%', height: '100%',
width: '100%' width: '100%'
}" }"
/> />
}
<canvas <canvas
#chartCanvas #chartCanvas
class="h-100" class="h-100"

@ -1,7 +1,5 @@
<button @if (deviceType === 'mobile') {
*ngIf="deviceType === 'mobile'" <button mat-button (click)="onClickCloseButton()">
mat-button <ion-icon name="close" size="large" />
(click)="onClickCloseButton()" </button>
> }
<ion-icon name="close" size="large" />
</button>

@ -3,11 +3,8 @@
[ngClass]="{ 'text-center': position === 'center' }" [ngClass]="{ 'text-center': position === 'center' }"
>{{ title }}</span >{{ title }}</span
> >
<button @if (deviceType !== 'mobile') {
*ngIf="deviceType !== 'mobile'" <button class="no-min-width px-0" mat-button (click)="onClickCloseButton()">
class="no-min-width px-0" <ion-icon name="close" size="large" />
mat-button </button>
(click)="onClickCloseButton()" }
>
<ion-icon name="close" size="large" />
</button>

@ -12,12 +12,13 @@
<small class="d-block" i18n>Current Market Mood</small> <small class="d-block" i18n>Current Market Mood</small>
</div> </div>
</div> </div>
<ngx-skeleton-loader @if (!fearAndGreedIndex) {
*ngIf="!fearAndGreedIndex" <ngx-skeleton-loader
animation="pulse" animation="pulse"
class="position-absolute w-100" class="position-absolute w-100"
[theme]="{ [theme]="{
height: '100%' height: '100%'
}" }"
/> />
}
</div> </div>

@ -1,5 +1,5 @@
<mat-toolbar class="px-0"> <mat-toolbar class="px-0">
<ng-container *ngIf="user"> @if (user) {
<div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }"> <div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }">
<a <a
class="align-items-center justify-content-start rounded-0" class="align-items-center justify-content-start rounded-0"
@ -54,19 +54,21 @@
>Accounts</a >Accounts</a
> >
</li> </li>
<li *ngIf="hasPermissionToAccessAdminControl" class="list-inline-item"> @if (hasPermissionToAccessAdminControl) {
<a <li class="list-inline-item">
class="d-none d-sm-block" <a
i18n class="d-none d-sm-block"
mat-flat-button i18n
[ngClass]="{ mat-flat-button
'font-weight-bold': currentRoute === 'admin', [ngClass]="{
'text-decoration-underline': currentRoute === 'admin' 'font-weight-bold': currentRoute === 'admin',
}" 'text-decoration-underline': currentRoute === 'admin'
[routerLink]="['/admin']" }"
>Admin Control</a [routerLink]="['/admin']"
> >Admin Control</a
</li> >
</li>
}
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
class="d-none d-sm-block" class="d-none d-sm-block"
@ -80,24 +82,23 @@
>Resources</a >Resources</a
> >
</li> </li>
<li @if (
*ngIf=" hasPermissionForSubscription && user?.subscription?.type === 'Basic'
hasPermissionForSubscription && user?.subscription?.type === 'Basic' ) {
" <li class="list-inline-item">
class="list-inline-item" <a
> class="d-none d-sm-block"
<a i18n
class="d-none d-sm-block" mat-flat-button
i18n [ngClass]="{
mat-flat-button 'font-weight-bold': currentRoute === routePricing,
[ngClass]="{ 'text-decoration-underline': currentRoute === routePricing
'font-weight-bold': currentRoute === routePricing, }"
'text-decoration-underline': currentRoute === routePricing [routerLink]="routerLinkPricing"
}" >Pricing</a
[routerLink]="routerLinkPricing" >
>Pricing</a </li>
> }
</li>
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
class="d-none d-sm-block" class="d-none d-sm-block"
@ -111,42 +112,44 @@
>About</a >About</a
> >
</li> </li>
<li *ngIf="hasPermissionToAccessAssistant" class="list-inline-item"> @if (hasPermissionToAccessAssistant) {
<button <li class="list-inline-item">
#assistantTrigger="matMenuTrigger" <button
class="h-100 no-min-width px-2" #assistantTrigger="matMenuTrigger"
mat-button class="h-100 no-min-width px-2"
matBadge="✓" mat-button
matBadgeSize="small" matBadge="✓"
[mat-menu-trigger-for]="assistantMenu" matBadgeSize="small"
[matBadgeHidden]=" [mat-menu-trigger-for]="assistantMenu"
!hasFilters || !user?.settings?.isExperimentalFeatures [matBadgeHidden]="
" !hasFilters || !user?.settings?.isExperimentalFeatures
[matMenuTriggerRestoreFocus]="false"
(menuOpened)="onOpenAssistant()"
>
<ion-icon class="rotate-90" name="options-outline" />
</button>
<mat-menu
#assistantMenu="matMenu"
class="assistant"
xPosition="before"
[overlapTrigger]="true"
(closed)="assistantElement?.setIsOpen(false)"
>
<gf-assistant
#assistant
[deviceType]="deviceType"
[hasPermissionToAccessAdminControl]="
hasPermissionToAccessAdminControl
" "
[user]="user" [matMenuTriggerRestoreFocus]="false"
(closed)="closeAssistant()" (menuOpened)="onOpenAssistant()"
(dateRangeChanged)="onDateRangeChange($event)" >
(filtersChanged)="onFiltersChanged($event)" <ion-icon class="rotate-90" name="options-outline" />
/> </button>
</mat-menu> <mat-menu
</li> #assistantMenu="matMenu"
class="assistant"
xPosition="before"
[overlapTrigger]="true"
(closed)="assistantElement?.setIsOpen(false)"
>
<gf-assistant
#assistant
[deviceType]="deviceType"
[hasPermissionToAccessAdminControl]="
hasPermissionToAccessAdminControl
"
[user]="user"
(closed)="closeAssistant()"
(dateRangeChanged)="onDateRangeChange($event)"
(filtersChanged)="onFiltersChanged($event)"
/>
</mat-menu>
</li>
}
<li class="list-inline-item"> <li class="list-inline-item">
<button <button
class="no-min-width px-1" class="no-min-width px-1"
@ -167,40 +170,31 @@
/> />
</button> </button>
<mat-menu #accountMenu="matMenu" xPosition="before"> <mat-menu #accountMenu="matMenu" xPosition="before">
<ng-container @if (
*ngIf=" hasPermissionForSubscription && user?.subscription?.type === 'Basic'
hasPermissionForSubscription && ) {
user?.subscription?.type === 'Basic'
"
>
<a class="d-flex" mat-menu-item [routerLink]="routerLinkPricing" <a class="d-flex" mat-menu-item [routerLink]="routerLinkPricing"
><span class="align-items-center d-flex" ><span class="align-items-center d-flex"
><span ><span>
><ng-container @if (user.subscription.offer === 'default') {
*ngIf="user.subscription.offer === 'default'" <ng-container i18n>Upgrade Plan</ng-container>
i18n } @else if (
>Upgrade Plan</ng-container user.subscription.offer === 'renewal' ||
> user.subscription.offer === 'renewal-early-bird'
<ng-container ) {
*ngIf=" <ng-container i18n>Renew Plan</ng-container>
user.subscription.offer === 'renewal' || }
user.subscription.offer === 'renewal-early-bird' </span>
"
i18n
>Renew Plan</ng-container
></span
>
<gf-premium-indicator <gf-premium-indicator
class="d-inline-block ml-1" class="d-inline-block ml-1"
[enableLink]="false" /></span [enableLink]="false" /></span
></a> ></a>
<hr class="m-0" /> <hr class="m-0" />
</ng-container> }
<ng-container *ngIf="user?.access?.length > 0"> @if (user?.access?.length > 0) {
<button mat-menu-item (click)="impersonateAccount(null)"> <button mat-menu-item (click)="impersonateAccount(null)">
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon <ion-icon
*ngIf="user?.access?.length > 0"
class="mr-2" class="mr-2"
[name]=" [name]="
impersonationId impersonationId
@ -211,27 +205,28 @@
<span i18n>Me</span> <span i18n>Me</span>
</span> </span>
</button> </button>
<button @for (accessItem of user?.access; track accessItem) {
*ngFor="let accessItem of user?.access" <button mat-menu-item (click)="impersonateAccount(accessItem.id)">
mat-menu-item <span class="align-items-center d-flex">
(click)="impersonateAccount(accessItem.id)" <ion-icon
> class="mr-2"
<span class="align-items-center d-flex"> name="square-outline"
<ion-icon [name]="
class="mr-2" accessItem.id === impersonationId
name="square-outline" ? 'radio-button-on-outline'
[name]=" : 'radio-button-off-outline'
accessItem.id === impersonationId "
? 'radio-button-on-outline' />
: 'radio-button-off-outline' @if (accessItem.alias) {
" <span>{{ accessItem.alias }}</span>
/> } @else {
<span *ngIf="accessItem.alias">{{ accessItem.alias }}</span> <span i18n>User</span>
<span *ngIf="!accessItem.alias" i18n>User</span> }
</span> </span>
</button> </button>
}
<hr class="m-0" /> <hr class="m-0" />
</ng-container> }
<a <a
class="d-flex d-sm-none" class="d-flex d-sm-none"
i18n i18n
@ -268,15 +263,16 @@
[routerLink]="['/account']" [routerLink]="['/account']"
>My Ghostfolio</a >My Ghostfolio</a
> >
<a @if (hasPermissionToAccessAdminControl) {
*ngIf="hasPermissionToAccessAdminControl" <a
class="d-flex d-sm-none" class="d-flex d-sm-none"
i18n i18n
mat-menu-item mat-menu-item
[ngClass]="{ 'font-weight-bold': currentRoute === 'admin' }" [ngClass]="{ 'font-weight-bold': currentRoute === 'admin' }"
[routerLink]="['/admin']" [routerLink]="['/admin']"
>Admin Control</a >Admin Control</a
> >
}
<hr class="m-0" /> <hr class="m-0" />
<a <a
class="d-flex d-sm-none" class="d-flex d-sm-none"
@ -288,18 +284,18 @@
[routerLink]="routerLinkResources" [routerLink]="routerLinkResources"
>Resources</a >Resources</a
> >
<a @if (
*ngIf=" hasPermissionForSubscription && user?.subscription?.type === 'Basic'
hasPermissionForSubscription && ) {
user?.subscription?.type === 'Basic' <a
" class="d-flex d-sm-none"
class="d-flex d-sm-none" i18n
i18n mat-menu-item
mat-menu-item [ngClass]="{ 'font-weight-bold': currentRoute === routePricing }"
[ngClass]="{ 'font-weight-bold': currentRoute === routePricing }" [routerLink]="routerLinkPricing"
[routerLink]="routerLinkPricing" >Pricing</a
>Pricing</a >
> }
<a <a
class="d-flex d-sm-none" class="d-flex d-sm-none"
i18n i18n
@ -313,8 +309,8 @@
</mat-menu> </mat-menu>
</li> </li>
</ul> </ul>
</ng-container> }
<ng-container *ngIf="user === null"> @if (user === null) {
<div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }"> <div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }">
<a <a
class="align-items-center justify-content-start rounded-0" class="align-items-center justify-content-start rounded-0"
@ -357,35 +353,36 @@
>About</a >About</a
> >
</li> </li>
<li *ngIf="hasPermissionForSubscription" class="list-inline-item"> @if (hasPermissionForSubscription) {
<a <li class="list-inline-item">
class="d-sm-block" <a
i18n class="d-sm-block"
mat-flat-button i18n
[ngClass]="{ mat-flat-button
'font-weight-bold': currentRoute === routePricing, [ngClass]="{
'text-decoration-underline': currentRoute === routePricing 'font-weight-bold': currentRoute === routePricing,
}" 'text-decoration-underline': currentRoute === routePricing
[routerLink]="routerLinkPricing" }"
>Pricing</a [routerLink]="routerLinkPricing"
> >Pricing</a
</li> >
<li </li>
*ngIf="hasPermissionToAccessFearAndGreedIndex" }
class="list-inline-item" @if (hasPermissionToAccessFearAndGreedIndex) {
> <li class="list-inline-item">
<a <a
class="d-none d-sm-block" class="d-none d-sm-block"
i18n i18n
mat-flat-button mat-flat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === routeMarkets, 'font-weight-bold': currentRoute === routeMarkets,
'text-decoration-underline': currentRoute === routeMarkets 'text-decoration-underline': currentRoute === routeMarkets
}" }"
[routerLink]="routerLinkMarkets" [routerLink]="routerLinkMarkets"
>Markets</a >Markets</a
> >
</li> </li>
}
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
class="d-none d-sm-block no-min-width p-1" class="d-none d-sm-block no-min-width p-1"
@ -399,18 +396,17 @@
<ng-container i18n>Sign in</ng-container> <ng-container i18n>Sign in</ng-container>
</button> </button>
</li> </li>
<li @if (currentRoute !== 'register' && hasPermissionToCreateUser) {
*ngIf="currentRoute !== 'register' && hasPermissionToCreateUser" <li class="list-inline-item ml-1">
class="list-inline-item ml-1" <a
> class="d-none d-sm-block"
<a color="primary"
class="d-none d-sm-block" mat-flat-button
color="primary" [routerLink]="routerLinkRegister"
mat-flat-button ><ng-container i18n>Get started</ng-container>
[routerLink]="routerLinkRegister" </a>
><ng-container i18n>Get started</ng-container> </li>
</a> }
</li>
</ul> </ul>
</ng-container> }
</mat-toolbar> </mat-toolbar>

@ -376,9 +376,9 @@
<div class="col"> <div class="col">
<div class="h5" i18n>Tags</div> <div class="h5" i18n>Tags</div>
<mat-chip-listbox> <mat-chip-listbox>
<mat-chip-option *ngFor="let tag of tags" disabled>{{ @for (tag of tags; track tag) {
tag.name <mat-chip-option disabled>{{ tag.name }}</mat-chip-option>
}}</mat-chip-option> }
</mat-chip-listbox> </mat-chip-listbox>
</div> </div>
</div> </div>

@ -1,30 +1,32 @@
<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 *ngIf="hasPermissionToAccessFearAndGreedIndex" class="mb-5 row"> @if (hasPermissionToAccessFearAndGreedIndex) {
<div class="col-xs-12 col-md-8 offset-md-2"> <div class="mb-5 row">
<div class="mb-2 text-center text-muted"> <div class="col-xs-12 col-md-8 offset-md-2">
<small i18n>Last {{ numberOfDays }} Days</small> <div class="mb-2 text-center text-muted">
<small i18n>Last {{ numberOfDays }} Days</small>
</div>
<gf-line-chart
class="mb-3"
symbol="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>
<gf-line-chart
class="mb-3"
symbol="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-8 offset-md-2"> <div class="col-xs-12 col-md-8 offset-md-2">
@ -33,15 +35,16 @@
[locale]="user?.settings?.locale || undefined" [locale]="user?.settings?.locale || undefined"
[user]="user" [user]="user"
/> />
<ngx-skeleton-loader @if (isLoading) {
*ngIf="isLoading" <ngx-skeleton-loader
animation="pulse" animation="pulse"
class="px-2 py-3" class="px-2 py-3"
[theme]="{ [theme]="{
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'
}" }"
/> />
}
</div> </div>
</div> </div>
</div> </div>

@ -39,22 +39,19 @@
</li> </li>
</ol> </ol>
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<a @if (user?.accounts?.length === 1) {
*ngIf="user?.accounts?.length === 1" <a color="primary" mat-flat-button [routerLink]="['/accounts']">
color="primary" <ng-container i18n>Setup accounts</ng-container>
mat-flat-button </a>
[routerLink]="['/accounts']" } @else if (user?.accounts?.length > 1) {
> <a
<ng-container i18n>Setup accounts</ng-container> color="primary"
</a> mat-flat-button
<a [routerLink]="['/portfolio', 'activities']"
*ngIf="user?.accounts?.length > 1" >
color="primary" <ng-container i18n>Add activity</ng-container>
mat-flat-button </a>
[routerLink]="['/portfolio', 'activities']" }
>
<ng-container i18n>Add activity</ng-container>
</a>
</div> </div>
</div> </div>
</div> </div>

@ -1,11 +1,12 @@
<ngx-skeleton-loader @if (isLoading) {
*ngIf="isLoading" <ngx-skeleton-loader
animation="pulse" animation="pulse"
[theme]="{ [theme]="{
height: '100%', height: '100%',
width: '100%' width: '100%'
}" }"
/> />
}
<canvas <canvas
#chartCanvas #chartCanvas
class="h-100" class="h-100"

@ -27,7 +27,7 @@
</button> </button>
</mat-form-field> </mat-form-field>
</form> </form>
<ng-container *ngIf="data.hasPermissionToUseSocialLogin"> @if (data.hasPermissionToUseSocialLogin) {
<div class="my-3 text-center text-muted" i18n>or</div> <div class="my-3 text-center text-muted" i18n>or</div>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<button <button
@ -52,7 +52,7 @@
/><span i18n>Sign in with Google</span></a /><span i18n>Sign in with Google</span></a
> >
</div> </div>
</ng-container> }
</div> </div>
</div> </div>
<div mat-dialog-actions> <div mat-dialog-actions>

@ -10,16 +10,18 @@
/> />
} }
</div> </div>
<div *ngIf="isLoading" class="align-items-center d-flex"> @if (isLoading) {
<ngx-skeleton-loader <div class="align-items-center d-flex">
animation="pulse" <ngx-skeleton-loader
class="mb-2" animation="pulse"
[theme]="{ class="mb-2"
height: '4rem', [theme]="{
width: '15rem' height: '4rem',
}" width: '15rem'
/> }"
</div> />
</div>
}
<div <div
class="display-4 font-weight-bold m-0 text-center value-container" class="display-4 font-weight-bold m-0 text-center value-container"
[hidden]="isLoading" [hidden]="isLoading"
@ -34,28 +36,32 @@
{{ unit }} {{ unit }}
</div> </div>
</div> </div>
<div *ngIf="showDetails" class="row"> @if (showDetails) {
<div class="d-flex col justify-content-end"> <div class="row">
<gf-value <div class="d-flex col justify-content-end">
[colorizeSign]="true" <gf-value
[isCurrency]="true" [colorizeSign]="true"
[locale]="locale" [isCurrency]="true"
[value]=" [locale]="locale"
isLoading ? undefined : performance?.netPerformanceWithCurrencyEffect [value]="
" isLoading
/> ? undefined
</div> : performance?.netPerformanceWithCurrencyEffect
<div class="col"> "
<gf-value />
[colorizeSign]="true" </div>
[isPercent]="true" <div class="col">
[locale]="locale" <gf-value
[value]=" [colorizeSign]="true"
isLoading [isPercent]="true"
? undefined [locale]="locale"
: performance?.netPerformancePercentageWithCurrencyEffect [value]="
" isLoading
/> ? undefined
: performance?.netPerformancePercentageWithCurrencyEffect
"
/>
</div>
</div> </div>
</div> }
</div> </div>

@ -1,43 +1,51 @@
<div class="py-3"> <div class="py-3">
<div class="align-items-center flex-nowrap no-gutters row"> <div class="align-items-center flex-nowrap no-gutters row">
<div *ngIf="isLoading"> @if (isLoading) {
<ngx-skeleton-loader <div>
animation="pulse" <ngx-skeleton-loader
class="mr-2" animation="pulse"
[theme]="{ class="mr-2"
height: '2rem', [theme]="{
width: '2rem' height: '2rem',
}" width: '2rem'
/> }"
</div> />
<div </div>
*ngIf="!isLoading" } @else {
class="align-items-center d-flex icon-container mr-2 px-2" <div
[ngClass]="{ okay: rule?.value === true, warn: rule?.value === false }" class="align-items-center d-flex icon-container mr-2 px-2"
> [ngClass]="{ okay: rule?.value === true, warn: rule?.value === false }"
<ion-icon *ngIf="rule?.value === true" name="checkmark-circle-outline" /> >
<ion-icon *ngIf="rule?.value === false" name="warning-outline" /> @if (rule?.value === true) {
</div> <ion-icon name="checkmark-circle-outline" />
<div *ngIf="isLoading" class="flex-grow-1"> } @else {
<ngx-skeleton-loader <ion-icon name="warning-outline" />
animation="pulse" }
class="mt-1 mb-1" </div>
[theme]="{ }
height: '1rem', @if (isLoading) {
width: '10rem' <div class="flex-grow-1">
}" <ngx-skeleton-loader
/> animation="pulse"
<ngx-skeleton-loader class="mt-1 mb-1"
animation="pulse" [theme]="{
[theme]="{ height: '1rem',
height: '1rem', width: '10rem'
width: '15rem' }"
}" />
/> <ngx-skeleton-loader
</div> animation="pulse"
<div *ngIf="!isLoading" class="flex-grow-1"> [theme]="{
<div class="h6 my-1">{{ rule?.name }}</div> height: '1rem',
<div class="evaluation">{{ rule?.evaluation }}</div> width: '15rem'
</div> }"
/>
</div>
} @else {
<div class="flex-grow-1">
<div class="h6 my-1">{{ rule?.name }}</div>
<div class="evaluation">{{ rule?.evaluation }}</div>
</div>
}
</div> </div>
</div> </div>

@ -1,20 +1,22 @@
<div class="container p-0"> <div class="container p-0">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">
<mat-card @if (hasPermissionToCreateOrder && rules === null) {
*ngIf="hasPermissionToCreateOrder && rules === null" <mat-card appearance="outlined" class="my-2 text-center">
appearance="outlined" <mat-card-content>
class="my-2 text-center" <gf-no-transactions-info-indicator [hasBorder]="false" />
> </mat-card-content>
<mat-card-content> </mat-card>
<gf-no-transactions-info-indicator [hasBorder]="false" /> }
</mat-card-content>
</mat-card>
<gf-rule *ngIf="rules?.length === 0" [isLoading]="true" /> @if (rules?.length === 0) {
<ng-container *ngIf="rules !== null && rules !== undefined"> <gf-rule [isLoading]="true" />
<gf-rule *ngFor="let rule of rules" [rule]="rule" /> }
</ng-container> @if (rules !== null && rules !== undefined) {
@for (rule of rules; track rule) {
<gf-rule [rule]="rule" />
}
}
</div> </div>
</div> </div>
</div> </div>

@ -3,12 +3,13 @@
[formControl]="optionFormControl" [formControl]="optionFormControl"
(change)="onValueChange()" (change)="onValueChange()"
> >
<mat-radio-button @for (option of options; track option) {
*ngFor="let option of options" <mat-radio-button
class="d-inline-flex" class="d-inline-flex"
[disabled]="isLoading" [disabled]="isLoading"
[ngClass]="{ 'cursor-pointer': !isLoading }" [ngClass]="{ 'cursor-pointer': !isLoading }"
[value]="option.value" [value]="option.value"
>{{ option.label }}</mat-radio-button >{{ option.label }}</mat-radio-button
> >
}
</mat-radio-group> </mat-radio-group>

@ -3,25 +3,26 @@
class="align-items-center d-none d-sm-flex h3 justify-content-center mb-3 text-center" class="align-items-center d-none d-sm-flex h3 justify-content-center mb-3 text-center"
> >
<span i18n>Granted Access</span> <span i18n>Granted Access</span>
<gf-premium-indicator @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <gf-premium-indicator class="ml-1" />
class="ml-1" }
/>
</h1> </h1>
<gf-access-table <gf-access-table
[accesses]="accesses" [accesses]="accesses"
[showActions]="hasPermissionToDeleteAccess" [showActions]="hasPermissionToDeleteAccess"
(accessDeleted)="onDeleteAccess($event)" (accessDeleted)="onDeleteAccess($event)"
/> />
<div *ngIf="hasPermissionToCreateAccess" class="fab-container"> @if (hasPermissionToCreateAccess) {
<a <div class="fab-container">
class="align-items-center d-flex justify-content-center" <a
color="primary" class="align-items-center d-flex justify-content-center"
mat-fab color="primary"
[queryParams]="{ createDialog: true }" mat-fab
[routerLink]="[]" [queryParams]="{ createDialog: true }"
> [routerLink]="[]"
<ion-icon name="add-outline" size="large" /> >
</a> <ion-icon name="add-outline" size="large" />
</div> </a>
</div>
}
</div> </div>

@ -6,64 +6,57 @@
[expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat" [expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat"
[name]="user?.subscription?.type" [name]="user?.subscription?.type"
/> />
<div @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <div class="d-flex flex-column mt-5">
class="d-flex flex-column mt-5" @if (
>
<ng-container
*ngIf="
hasPermissionForSubscription && hasPermissionToUpdateUserSettings hasPermissionForSubscription && hasPermissionToUpdateUserSettings
" ) {
> <button color="primary" mat-flat-button (click)="onCheckout()">
<button color="primary" mat-flat-button (click)="onCheckout()"> @if (user.subscription.offer === 'default') {
<ng-container *ngIf="user.subscription.offer === 'default'" i18n <ng-container i18n>Upgrade Plan</ng-container>
>Upgrade Plan</ng-container } @else if (
>
<ng-container
*ngIf="
user.subscription.offer === 'renewal' || user.subscription.offer === 'renewal' ||
user.subscription.offer === 'renewal-early-bird' user.subscription.offer === 'renewal-early-bird'
" ) {
i18n <ng-container i18n>Renew Plan</ng-container>
>Renew Plan</ng-container }
> </button>
</button> @if (price) {
<div *ngIf="price" class="mt-1 text-center"> <div class="mt-1 text-center">
<ng-container *ngIf="coupon" @if (coupon) {
><del class="text-muted" <del class="text-muted"
>{{ baseCurrency }}&nbsp;{{ price }}</del >{{ baseCurrency }}&nbsp;{{ price }}</del
>&nbsp;{{ baseCurrency }}&nbsp;{{ >&nbsp;{{ baseCurrency }}&nbsp;{{ price - coupon }}
price - coupon } @else {
}}</ng-container {{ baseCurrency }}&nbsp;{{ price }}
> }
<ng-container *ngIf="!coupon" &nbsp;<span i18n>per year</span>
>{{ baseCurrency }}&nbsp;{{ price }}</ng-container </div>
>&nbsp;<span i18n>per year</span> }
}
<div class="align-items-center d-flex justify-content-center mt-4">
@if (!user?.subscription?.expiresAt) {
<a class="mx-1" mat-stroked-button [href]="trySubscriptionMail"
><span i18n>Try Premium</span>
<gf-premium-indicator
class="d-inline-block ml-1"
[enableLink]="false"
/>
</a>
}
@if (hasPermissionToUpdateUserSettings) {
<a
class="mx-1"
i18n
mat-stroked-button
[routerLink]=""
(click)="onRedeemCoupon()"
>Redeem Coupon</a
>
}
</div> </div>
</ng-container>
<div class="align-items-center d-flex justify-content-center mt-4">
<a
*ngIf="!user?.subscription?.expiresAt"
class="mx-1"
mat-stroked-button
[href]="trySubscriptionMail"
><span i18n>Try Premium</span>
<gf-premium-indicator
class="d-inline-block ml-1"
[enableLink]="false"
/>
</a>
<a
*ngIf="hasPermissionToUpdateUserSettings"
class="mx-1"
i18n
mat-stroked-button
[routerLink]=""
(click)="onRedeemCoupon()"
>Redeem Coupon</a
>
</div> </div>
</div> }
</div> </div>
</div> </div>
</div> </div>

@ -36,11 +36,9 @@
onChangeUserSetting('baseCurrency', $event.value) onChangeUserSetting('baseCurrency', $event.value)
" "
> >
<mat-option @for (currency of currencies; track currency) {
*ngFor="let currency of currencies" <mat-option [value]="currency">{{ currency }}</mat-option>
[value]="currency" }
>{{ currency }}</mat-option
>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -48,20 +46,18 @@
<div class="align-items-center d-flex mb-2"> <div class="align-items-center d-flex mb-2">
<div class="pr-1 w-50"> <div class="pr-1 w-50">
<div i18n>Language</div> <div i18n>Language</div>
<div @if (isCommunityLanguage()) {
*ngIf="isCommunityLanguage()" <div class="hint-text text-muted" i18n>
class="hint-text text-muted" If a translation is missing, kindly support us in extending it
i18n <a
> href="https://github.com/ghostfolio/ghostfolio/blob/main/apps/client/src/locales/messages.{{
If a translation is missing, kindly support us in extending it language
<a }}.xlf"
href="https://github.com/ghostfolio/ghostfolio/blob/main/apps/client/src/locales/messages.{{ target="_blank"
language >here</a
}}.xlf" >.
target="_blank" </div>
>here</a }
>.
</div>
</div> </div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field appearance="outline" class="w-100 without-hint">
@ -134,9 +130,9 @@
" "
> >
<mat-option [value]="null" /> <mat-option [value]="null" />
<mat-option *ngFor="let locale of locales" [value]="locale">{{ @for (locale of locales; track locale) {
locale <mat-option [value]="locale">{{ locale }}</mat-option>
}}</mat-option> }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -198,26 +194,25 @@
/> />
</div> </div>
</div> </div>
<div @if (hasPermissionToUpdateUserSettings) {
*ngIf="hasPermissionToUpdateUserSettings" <div class="align-items-center d-flex mt-4 py-1">
class="align-items-center d-flex mt-4 py-1" <div class="pr-1 w-50">
> <div i18n>Experimental Features</div>
<div class="pr-1 w-50"> <div class="hint-text text-muted" i18n>
<div i18n>Experimental Features</div> Sneak peek at upcoming functionality
<div class="hint-text text-muted" i18n> </div>
Sneak peek at upcoming functionality </div>
<div class="pl-1 w-50">
<mat-slide-toggle
color="primary"
hideIcon="true"
[checked]="user.settings.isExperimentalFeatures"
[disabled]="!hasPermissionToUpdateUserSettings"
(change)="onExperimentalFeaturesChange($event)"
/>
</div> </div>
</div> </div>
<div class="pl-1 w-50"> }
<mat-slide-toggle
color="primary"
hideIcon="true"
[checked]="user.settings.isExperimentalFeatures"
[disabled]="!hasPermissionToUpdateUserSettings"
(change)="onExperimentalFeaturesChange($event)"
/>
</div>
</div>
<div class="align-items-center d-flex mt-4 py-1"> <div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50"> <div class="pr-1 w-50">
Ghostfolio <ng-container i18n>User ID</ng-container> Ghostfolio <ng-container i18n>User ID</ng-container>

@ -1,10 +1,11 @@
<ngx-skeleton-loader @if (isLoading) {
*ngIf="isLoading" <ngx-skeleton-loader
animation="pulse" animation="pulse"
class="h-100" class="h-100"
[theme]="{ [theme]="{
width: '100%' width: '100%'
}" }"
/> />
}
<div class="align-items-center d-flex h-100 w-100" id="svgMap"></div> <div class="align-items-center d-flex h-100 w-100" id="svgMap"></div>

@ -8,22 +8,23 @@
[disablePagination]="true" [disablePagination]="true"
[tabPanel]="tabPanel" [tabPanel]="tabPanel"
> >
<ng-container *ngFor="let tab of tabs"> @for (tab of tabs; track tab) {
<a @if (tab.showCondition !== false) {
#rla="routerLinkActive" <a
*ngIf="tab.showCondition !== false" #rla="routerLinkActive"
class="no-min-width px-3" class="no-min-width px-3"
mat-tab-link mat-tab-link
routerLinkActive routerLinkActive
[active]="rla.isActive" [active]="rla.isActive"
[routerLink]="tab.path" [routerLink]="tab.path"
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
> >
<ion-icon <ion-icon
[name]="tab.iconName" [name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'" [size]="deviceType === 'mobile' ? 'large' : 'small'"
/> />
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div> <div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a> </a>
</ng-container> }
}
</nav> </nav>

@ -21,11 +21,12 @@
title="GNU Affero General Public License" title="GNU Affero General Public License"
>AGPL-3.0 license</a >AGPL-3.0 license</a
> >
<ng-container *ngIf="hasPermissionForStatistics"> @if (hasPermissionForStatistics) {
and we share aggregated and we share aggregated
<a title="Open Startup" [routerLink]="['/open']">key metrics</a> <a title="Open Startup" [routerLink]="['/open']">key metrics</a>
of the platforms performance</ng-container of the platforms performance
>. The project has been initiated by }
. The project has been initiated by
<a href="https://dotsilver.ch" title="Website of Thomas Kaul" <a href="https://dotsilver.ch" title="Website of Thomas Kaul"
>Thomas Kaul</a >Thomas Kaul</a
> >
@ -35,12 +36,12 @@
title="Contributors to Ghostfolio" title="Contributors to Ghostfolio"
>contributors</a >contributors</a
>. >.
<ng-container *ngIf="hasPermissionForSubscription" @if (hasPermissionForSubscription) {
>Check the system status at Check the system status at
<a href="https://status.ghostfol.io" title="Ghostfolio Status" <a href="https://status.ghostfol.io" title="Ghostfolio Status"
>status.ghostfol.io</a >status.ghostfol.io</a
>.</ng-container >.
> }
</p> </p>
<p> <p>
If you encounter a bug or would like to suggest an improvement or a If you encounter a bug or would like to suggest an improvement or a
@ -57,12 +58,13 @@
href="https://twitter.com/ghostfolio_" href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)" title="Post to Ghostfolio on X (formerly Twitter)"
>&#64;ghostfolio_</a >&#64;ghostfolio_</a
><ng-container *ngIf="user?.subscription?.type === 'Premium'" >
>, send an e-mail to @if (user?.subscription?.type === 'Premium') {
, send an e-mail to
<a href="mailto:hi@ghostfol.io" title="Send an e-mail" <a href="mailto:hi@ghostfol.io" title="Send an e-mail"
>hi&#64;ghostfol.io</a >hi&#64;ghostfol.io</a
></ng-container >
> }
or start a discussion at or start a discussion at
<a <a
href="https://github.com/ghostfolio/ghostfolio" href="https://github.com/ghostfolio/ghostfolio"
@ -79,15 +81,16 @@
> >
<ion-icon name="logo-x" /> <ion-icon name="logo-x" />
</a> </a>
<a @if (user?.subscription?.type === 'Premium') {
*ngIf="user?.subscription?.type === 'Premium'" <a
class="mx-2" class="mx-2"
href="mailto:hi@ghostfol.io" href="mailto:hi@ghostfol.io"
mat-icon-button mat-icon-button
title="Send an e-mail" title="Send an e-mail"
> >
<ion-icon name="mail" /> <ion-icon name="mail" />
</a> </a>
}
<a <a
class="mx-2" class="mx-2"
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg" href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
@ -105,29 +108,26 @@
<ion-icon name="logo-github" /> <ion-icon name="logo-github" />
</a> </a>
</p> </p>
<div @if (hasPermissionForSubscription) {
*ngIf="hasPermissionForSubscription" <div class="d-flex justify-content-center">
class="d-flex justify-content-center" <div
> class="independent-and-bootstrapped-logo mb-2"
<div title="Ghostfolio is an independent & bootstrapped business"
class="independent-and-bootstrapped-logo mb-2" ></div>
title="Ghostfolio is an independent & bootstrapped business" </div>
></div> } @else {
</div> <div class="d-flex justify-content-center">
<div <a
*ngIf="!hasPermissionForSubscription" href="https://www.buymeacoffee.com/ghostfolio"
class="d-flex justify-content-center" target="_blank"
> title="Support Ghostfolio"
<a ><img
href="https://www.buymeacoffee.com/ghostfolio" class="mb-2"
target="_blank" src="../assets/images/button-buy-me-a-coffee.png"
title="Support Ghostfolio" width="180"
><img /></a>
class="mb-2" </div>
src="../assets/images/button-buy-me-a-coffee.png" }
width="180"
/></a>
</div>
</div> </div>
</div> </div>
</div> </div>

@ -22,22 +22,21 @@
</div> </div>
</div> </div>
<div @if (
*ngIf=" !hasImpersonationId &&
!hasImpersonationId && hasPermissionToCreateAccount &&
hasPermissionToCreateAccount && !user.settings.isRestrictedView
!user.settings.isRestrictedView ) {
" <div class="fab-container">
class="fab-container" <a
> class="align-items-center d-flex justify-content-center"
<a color="primary"
class="align-items-center d-flex justify-content-center" mat-fab
color="primary" [queryParams]="{ createDialog: true }"
mat-fab [routerLink]="[]"
[queryParams]="{ createDialog: true }" >
[routerLink]="[]" <ion-icon name="add-outline" size="large" />
> </a>
<ion-icon name="add-outline" size="large" /> </div>
</a> }
</div>
</div> </div>

@ -10,16 +10,20 @@
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>From</mat-label> <mat-label i18n>From</mat-label>
<mat-select formControlName="fromAccount"> <mat-select formControlName="fromAccount">
<mat-option *ngFor="let account of accounts" [value]="account.id"> @for (account of accounts; track account) {
<div class="d-flex"> <mat-option [value]="account.id">
<gf-asset-profile-icon <div class="d-flex">
*ngIf="account.Platform?.url" @if (account.Platform?.url) {
class="mr-1" <gf-asset-profile-icon
[tooltip]="account.Platform?.name" class="mr-1"
[url]="account.Platform?.url" [tooltip]="account.Platform?.name"
/><span>{{ account.name }}</span> [url]="account.Platform?.url"
</div> />
</mat-option> }
<span>{{ account.name }}</span>
</div>
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -27,16 +31,20 @@
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>To</mat-label> <mat-label i18n>To</mat-label>
<mat-select formControlName="toAccount"> <mat-select formControlName="toAccount">
<mat-option *ngFor="let account of accounts" [value]="account.id"> @for (account of accounts; track account) {
<div class="d-flex"> <mat-option [value]="account.id">
<gf-asset-profile-icon <div class="d-flex">
*ngIf="account.Platform?.url" @if (account.Platform?.url) {
class="mr-1" <gf-asset-profile-icon
[tooltip]="account.Platform?.name" class="mr-1"
[url]="account.Platform?.url" [tooltip]="account.Platform?.name"
/><span>{{ account.name }}</span> [url]="account.Platform?.url"
</div> />
</mat-option> }
<span>{{ account.name }}</span>
</div>
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>

@ -8,22 +8,23 @@
[disablePagination]="true" [disablePagination]="true"
[tabPanel]="tabPanel" [tabPanel]="tabPanel"
> >
<ng-container *ngFor="let tab of tabs"> @for (tab of tabs; track tab) {
<a @if (tab.showCondition !== false) {
#rla="routerLinkActive" <a
*ngIf="tab.showCondition !== false" #rla="routerLinkActive"
class="no-min-width px-3" class="no-min-width px-3"
mat-tab-link mat-tab-link
routerLinkActive routerLinkActive
[active]="rla.isActive" [active]="rla.isActive"
[routerLink]="tab.path" [routerLink]="tab.path"
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
> >
<ion-icon <ion-icon
[name]="tab.iconName" [name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'" [size]="deviceType === 'mobile' ? 'large' : 'small'"
/> />
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div> <div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a> </a>
</ng-container> }
}
</nav> </nav>

@ -2,14 +2,12 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { InfoItem } from '@ghostfolio/common/interfaces'; import { InfoItem } from '@ghostfolio/common/interfaces';
import { CommonModule } from '@angular/common';
import { Component, OnDestroy } from '@angular/core'; import { Component, OnDestroy } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
imports: [CommonModule],
selector: 'gf-demo-page', selector: 'gf-demo-page',
standalone: true, standalone: true,
templateUrl: './demo-page.html' templateUrl: './demo-page.html'

@ -8,22 +8,23 @@
[disablePagination]="true" [disablePagination]="true"
[tabPanel]="tabPanel" [tabPanel]="tabPanel"
> >
<ng-container *ngFor="let tab of tabs"> @for (tab of tabs; track tab) {
<a @if (tab.showCondition !== false) {
#rla="routerLinkActive" <a
*ngIf="tab.showCondition !== false" #rla="routerLinkActive"
class="no-min-width px-3" class="no-min-width px-3"
mat-tab-link mat-tab-link
routerLinkActive routerLinkActive
[active]="rla.isActive" [active]="rla.isActive"
[routerLink]="tab.path" [routerLink]="tab.path"
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
> >
<ion-icon <ion-icon
[name]="tab.iconName" [name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'" [size]="deviceType === 'mobile' ? 'large' : 'small'"
/> />
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div> <div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a> </a>
</ng-container> }
}
</nav> </nav>

@ -125,12 +125,13 @@
href="https://twitter.com/ghostfolio_" href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)" title="Post to Ghostfolio on X (formerly Twitter)"
>&#64;ghostfolio_</a >&#64;ghostfolio_</a
><ng-container *ngIf="user?.subscription?.type === 'Premium'" >
>, @if (user?.subscription?.type === 'Premium') {
,
<a href="mailto:hi@ghostfol.io" title="Send an e-mail" <a href="mailto:hi@ghostfol.io" title="Send an e-mail"
>hi&#64;ghostfol.io</a >hi&#64;ghostfol.io</a
></ng-container >
> }
or or
<a <a
href="https://github.com/ghostfolio/ghostfolio" href="https://github.com/ghostfolio/ghostfolio"
@ -154,12 +155,13 @@
href="https://twitter.com/ghostfolio_" href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)" title="Post to Ghostfolio on X (formerly Twitter)"
>&#64;ghostfolio_</a >&#64;ghostfolio_</a
><ng-container *ngIf="user?.subscription?.type === 'Premium'" >
>, send an e-mail to @if (user?.subscription?.type === 'Premium') {
, send an e-mail to
<a href="mailto:hi@ghostfol.io" title="Send an e-mail" <a href="mailto:hi@ghostfol.io" title="Send an e-mail"
>hi&#64;ghostfol.io</a >hi&#64;ghostfol.io</a
></ng-container >
> }
or start a discussion at or start a discussion at
<a <a
href="https://github.com/ghostfolio/ghostfolio" href="https://github.com/ghostfolio/ghostfolio"

@ -98,23 +98,21 @@
start a new subscription.</mat-card-content start a new subscription.</mat-card-content
> >
</mat-card> </mat-card>
<mat-card @if (user?.subscription?.type === 'Premium') {
*ngIf="user?.subscription?.type === 'Premium'" <mat-card appearance="outlined" class="mb-3">
appearance="outlined" <mat-card-header>
class="mb-3" <mat-card-title
> >I cannot find my broker in the list of platforms. What can I
<mat-card-header> do?</mat-card-title
<mat-card-title >
>I cannot find my broker in the list of platforms. What can I </mat-card-header>
do?</mat-card-title <mat-card-content>
> Please send an e-mail with the web address of your broker to
</mat-card-header> <a href="mailto:hi@ghostfol.io">hi&#64;ghostfol.io</a> and we are
<mat-card-content> happy to add it.
Please send an e-mail with the web address of your broker to </mat-card-content>
<a href="mailto:hi@ghostfol.io">hi&#64;ghostfol.io</a> and we are </mat-card>
happy to add it. }
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-header> <mat-card-header>
<mat-card-title>Which devices are supported?</mat-card-title> <mat-card-title>Which devices are supported?</mat-card-title>
@ -156,12 +154,13 @@
href="https://twitter.com/ghostfolio_" href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)" title="Post to Ghostfolio on X (formerly Twitter)"
>&#64;ghostfolio_</a >&#64;ghostfolio_</a
><ng-container *ngIf="user?.subscription?.type === 'Premium'" >
>, send an e-mail to @if (user?.subscription?.type === 'Premium') {
, send an e-mail to
<a href="mailto:hi@ghostfol.io" title="Send an e-mail" <a href="mailto:hi@ghostfol.io" title="Send an e-mail"
>hi&#64;ghostfol.io</a >hi&#64;ghostfol.io</a
></ng-container >
> }
or start a discussion at or start a discussion at
<a <a
href="https://github.com/ghostfolio/ghostfolio" href="https://github.com/ghostfolio/ghostfolio"

@ -4,7 +4,6 @@ import { InfoItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
@ -14,7 +13,6 @@ import { Subject, takeUntil } from 'rxjs';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
imports: [ imports: [
CommonModule,
GfPremiumIndicatorComponent, GfPremiumIndicatorComponent,
MatButtonModule, MatButtonModule,
MatCardModule, MatCardModule,

@ -8,22 +8,23 @@
[disablePagination]="true" [disablePagination]="true"
[tabPanel]="tabPanel" [tabPanel]="tabPanel"
> >
<ng-container *ngFor="let tab of tabs"> @for (tab of tabs; track tab) {
<a @if (tab.showCondition !== false) {
#rla="routerLinkActive" <a
*ngIf="tab.showCondition !== false" #rla="routerLinkActive"
class="no-min-width px-3" class="no-min-width px-3"
mat-tab-link mat-tab-link
routerLinkActive routerLinkActive
[active]="rla.isActive" [active]="rla.isActive"
[routerLink]="tab.path" [routerLink]="tab.path"
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
> >
<ion-icon <ion-icon
[name]="tab.iconName" [name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'" [size]="deviceType === 'mobile' ? 'large' : 'small'"
/> />
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div> <div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a> </a>
</ng-container> }
}
</nav> </nav>

@ -1,10 +1,8 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
imports: [CommonModule],
selector: 'gf-i18n-page', selector: 'gf-i18n-page',
standalone: true, standalone: true,
styleUrls: ['./i18n-page.scss'], styleUrls: ['./i18n-page.scss'],

@ -32,7 +32,7 @@
<div class="container"> <div class="container">
<div class="button-container mb-5 row"> <div class="button-container mb-5 row">
<div class="align-items-center col d-flex justify-content-center"> <div class="align-items-center col d-flex justify-content-center">
<ng-container *ngIf="hasPermissionToCreateUser"> @if (hasPermissionToCreateUser) {
<a <a
color="primary" color="primary"
i18n i18n
@ -41,72 +41,74 @@
> >
Get Started Get Started
</a> </a>
</ng-container> }
<ng-container *ngIf="hasPermissionForDemo"> @if (hasPermissionForDemo) {
<div *ngIf="hasPermissionToCreateUser" class="mx-3 text-muted" i18n> @if (hasPermissionToCreateUser) {
or <div class="mx-3 text-muted" i18n>or</div>
</div> }
<a i18n mat-stroked-button [routerLink]="['/demo']">Live Demo</a> <a i18n mat-stroked-button [routerLink]="['/demo']">Live Demo</a>
</ng-container> }
</div> </div>
</div> </div>
<div *ngIf="hasPermissionForStatistics" class="row mb-5"> @if (hasPermissionForStatistics) {
<div <div class="row mb-5">
class="col-md-4 d-flex my-1" <div
[ngClass]="{ 'justify-content-center': deviceType !== 'mobile' }" class="col-md-4 d-flex my-1"
> [ngClass]="{ 'justify-content-center': deviceType !== 'mobile' }"
<a
class="d-block"
title="Ghostfolio in Numbers: Monthly Active Users (MAU)"
[routerLink]="['/open']"
> >
<gf-value <a
i18n class="d-block"
icon="people-outline" title="Ghostfolio in Numbers: Monthly Active Users (MAU)"
size="large" [routerLink]="['/open']"
[value]="statistics?.activeUsers30d ?? '-'"
>Monthly Active Users</gf-value
> >
</a> <gf-value
</div> i18n
<div icon="people-outline"
class="col-md-4 d-flex my-1" size="large"
[ngClass]="{ 'justify-content-center': deviceType !== 'mobile' }" [value]="statistics?.activeUsers30d ?? '-'"
> >Monthly Active Users</gf-value
<a >
class="d-block" </a>
title="Ghostfolio in Numbers: Stars on GitHub" </div>
[routerLink]="['/open']" <div
class="col-md-4 d-flex my-1"
[ngClass]="{ 'justify-content-center': deviceType !== 'mobile' }"
> >
<gf-value <a
i18n class="d-block"
icon="star-outline" title="Ghostfolio in Numbers: Stars on GitHub"
size="large" [routerLink]="['/open']"
[value]="statistics?.gitHubStargazers ?? '-'"
>Stars on GitHub</gf-value
> >
</a> <gf-value
</div> i18n
<div icon="star-outline"
class="col-md-4 d-flex my-1" size="large"
[ngClass]="{ 'justify-content-center': deviceType !== 'mobile' }" [value]="statistics?.gitHubStargazers ?? '-'"
> >Stars on GitHub</gf-value
<a >
class="d-block" </a>
title="Ghostfolio in Numbers: Pulls on Docker Hub" </div>
[routerLink]="['/open']" <div
class="col-md-4 d-flex my-1"
[ngClass]="{ 'justify-content-center': deviceType !== 'mobile' }"
> >
<gf-value <a
i18n class="d-block"
icon="cloud-download-outline" title="Ghostfolio in Numbers: Pulls on Docker Hub"
size="large" [routerLink]="['/open']"
[value]="statistics?.dockerHubPulls ?? '-'"
>Pulls on Docker Hub</gf-value
> >
</a> <gf-value
i18n
icon="cloud-download-outline"
size="large"
[value]="statistics?.dockerHubPulls ?? '-'"
>Pulls on Docker Hub</gf-value
>
</a>
</div>
</div> </div>
</div> }
<div class="row mb-5"> <div class="row mb-5">
<div class="col-12 text-center text-muted"> <div class="col-12 text-center text-muted">
@ -320,112 +322,130 @@
</div> </div>
<div class="col-md-8 offset-md-2"> <div class="col-md-8 offset-md-2">
<gf-carousel [aria-label]="'Testimonials'"> <gf-carousel [aria-label]="'Testimonials'">
<div *ngFor="let testimonial of testimonials" gf-carousel-item> @for (testimonial of testimonials; track testimonial) {
<div class="d-flex px-4"> <div gf-carousel-item>
<gf-logo class="mr-3 mt-2 pt-1" size="medium" [showLabel]="false" /> <div class="d-flex px-4">
<div> <gf-logo
<div>{{ testimonial.quote }}</div> class="mr-3 mt-2 pt-1"
<div class="mt-2 text-muted"> size="medium"
[showLabel]="false"
<a />
*ngIf="testimonial.url" <div>
target="_blank" <div>{{ testimonial.quote }}</div>
[href]="testimonial.url" <div class="mt-2 text-muted">
>{{ testimonial.author }}</a
> @if (testimonial.url) {
<span *ngIf="!testimonial.url">{{ testimonial.author }}</span <a target="_blank" [href]="testimonial.url">{{
>, testimonial.author
{{ testimonial.country }} }}</a>
} @else {
<span>{{ testimonial.author }}</span>
}
,
{{ testimonial.country }}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> }
</gf-carousel> </gf-carousel>
</div> </div>
</div> </div>
<div *ngIf="hasPermissionForSubscription" class="row my-5"> @if (hasPermissionForSubscription) {
<div class="col-12"> <div class="row my-5">
<h2 class="h4 text-center" i18n> <div class="col-12">
Members from around the globe are using <h2 class="h4 text-center" i18n>
<a href="pricing"><strong>Ghostfolio Premium</strong></a> Members from around the globe are using
</h2> <a href="pricing"><strong>Ghostfolio Premium</strong></a>
</div> </h2>
<div class="col-md-8 customer-map-container offset-md-2"> </div>
<gf-world-map-chart format="👻" [countries]="countriesOfSubscribersMap" /> <div class="col-md-8 customer-map-container offset-md-2">
<gf-world-map-chart
format="👻"
[countries]="countriesOfSubscribersMap"
/>
</div>
</div> </div>
</div> }
<div *ngIf="hasPermissionForSubscription" class="row my-3"> @if (hasPermissionForSubscription) {
<div class="col-12"> <div class="row my-3">
<h2 class="h4 mb-1 text-center" i18n> <div class="col-12">
How does <strong>Ghostfolio</strong> work? <h2 class="h4 mb-1 text-center" i18n>
</h2> How does <strong>Ghostfolio</strong> work?
<p class="lead mb-3 text-center" i18n>Get started in only 3 steps</p> </h2>
</div> <p class="lead mb-3 text-center" i18n>Get started in only 3 steps</p>
<div class="col-md-4 my-2"> </div>
<mat-card appearance="outlined" class="h-100"> <div class="col-md-4 my-2">
<mat-card-content class="d-flex flex-row"> <mat-card appearance="outlined" class="h-100">
<div class="flex-grow-1"> <mat-card-content class="d-flex flex-row">
<div class="font-weight-bold" i18n>Sign up anonymously*</div> <div class="flex-grow-1">
<div class="text-muted" i18n> <div class="font-weight-bold" i18n>Sign up anonymously*</div>
<small>* no e-mail address nor credit card required</small> <div class="text-muted" i18n>
<small>* no e-mail address nor credit card required</small>
</div>
</div> </div>
</div> <div class="pl-2 text-muted text-right">1</div>
<div class="pl-2 text-muted text-right">1</div> </mat-card-content>
</mat-card-content> </mat-card>
</mat-card> </div>
</div> <div class="col-md-4 my-2">
<div class="col-md-4 my-2"> <mat-card appearance="outlined" class="h-100">
<mat-card appearance="outlined" class="h-100"> <mat-card-content class="d-flex flex-row">
<mat-card-content class="d-flex flex-row"> <div class="flex-grow-1">
<div class="flex-grow-1"> <div class="font-weight-bold" i18n>
<div class="font-weight-bold" i18n> Add any of your historical transactions
Add any of your historical transactions </div>
</div> </div>
</div> <div class="pl-2 text-muted text-right">2</div>
<div class="pl-2 text-muted text-right">2</div> </mat-card-content>
</mat-card-content> </mat-card>
</mat-card> </div>
</div> <div class="col-md-4 my-2">
<div class="col-md-4 my-2"> <mat-card appearance="outlined" class="h-100">
<mat-card appearance="outlined" class="h-100"> <mat-card-content class="d-flex flex-row">
<mat-card-content class="d-flex flex-row"> <div class="flex-grow-1">
<div class="flex-grow-1"> <div class="font-weight-bold" i18n>
<div class="font-weight-bold" i18n> Get valuable insights of your portfolio composition
Get valuable insights of your portfolio composition </div>
</div> </div>
</div> <div class="pl-2 text-muted text-right">3</div>
<div class="pl-2 text-muted text-right">3</div> </mat-card-content>
</mat-card-content> </mat-card>
</mat-card> </div>
</div> </div>
</div> }
<div *ngIf="hasPermissionToCreateUser" class="row my-5"> @if (hasPermissionToCreateUser) {
<div class="col"> <div class="row my-5">
<h2 class="h4 mb-1 text-center" i18n>Are <strong>you</strong> ready?</h2> <div class="col">
<p class="lead mb-3 text-center" i18n> <h2 class="h4 mb-1 text-center" i18n>
Join now<ng-container *ngIf="hasPermissionForDemo"> Are <strong>you</strong> ready?
or check out the example account</ng-container </h2>
> <p class="lead mb-3 text-center" i18n>
</p> Join now
<div class="align-items-center d-flex justify-content-center py-2"> @if (hasPermissionForDemo) {
<a or check out the example account
color="primary" }
i18n </p>
mat-flat-button <div class="align-items-center d-flex justify-content-center py-2">
[routerLink]="routerLinkRegister" <a
> color="primary"
Get Started i18n
</a> mat-flat-button
<ng-container *ngIf="hasPermissionForDemo"> [routerLink]="routerLinkRegister"
<div class="mx-3 text-muted" i18n>or</div> >
<a i18n mat-stroked-button [routerLink]="['/demo']">Live Demo</a> Get Started
</ng-container> </a>
@if (hasPermissionForDemo) {
<div class="mx-3 text-muted" i18n>or</div>
<a i18n mat-stroked-button [routerLink]="['/demo']">Live Demo</a>
}
</div>
</div> </div>
</div> </div>
</div> }
</div> </div>
<div class="container"> <div class="container">

@ -4,8 +4,11 @@
(keyup.enter)="activityForm.valid && onSubmit()" (keyup.enter)="activityForm.valid && onSubmit()"
(ngSubmit)="onSubmit()" (ngSubmit)="onSubmit()"
> >
<h1 *ngIf="data.activity.id" i18n mat-dialog-title>Update activity</h1> @if (data.activity.id) {
<h1 *ngIf="!data.activity.id" i18n mat-dialog-title>Add activity</h1> <h1 i18n mat-dialog-title>Update activity</h1>
} @else {
<h1 i18n mat-dialog-title>Add activity</h1>
}
<div class="flex-grow-1 py-3" mat-dialog-content> <div class="flex-grow-1 py-3" mat-dialog-content>
<div class="mb-3"> <div class="mb-3">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
@ -81,25 +84,25 @@
> >
<mat-label i18n>Account</mat-label> <mat-label i18n>Account</mat-label>
<mat-select formControlName="accountId"> <mat-select formControlName="accountId">
<mat-option @if (
*ngIf=" !activityForm.get('accountId').hasValidator(Validators.required)
!activityForm.get('accountId').hasValidator(Validators.required) ) {
" <mat-option [value]="null" />
[value]="null" }
/> @for (account of data.accounts; track account) {
<mat-option <mat-option [value]="account.id">
*ngFor="let account of data.accounts" <div class="d-flex">
[value]="account.id" @if (account.Platform?.url) {
> <gf-asset-profile-icon
<div class="d-flex"> class="mr-1"
<gf-asset-profile-icon [tooltip]="account.Platform?.name"
*ngIf="account.Platform?.url" [url]="account.Platform?.url"
class="mr-1" />
[tooltip]="account.Platform?.name" }
[url]="account.Platform?.url" <span>{{ account.name }}</span>
/><span>{{ account.name }}</span> </div>
</div> </mat-option>
</mat-option> }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -139,9 +142,9 @@
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Currency</mat-label> <mat-label i18n>Currency</mat-label>
<mat-select formControlName="currency"> <mat-select formControlName="currency">
<mat-option *ngFor="let currency of currencies" [value]="currency">{{ @for (currency of currencies; track currency) {
currency <mat-option [value]="currency">{{ currency }}</mat-option>
}}</mat-option> }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -186,18 +189,24 @@
> >
<div class="align-items-start d-flex"> <div class="align-items-start d-flex">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label <mat-label>
><ng-container [ngSwitch]="activityForm.get('type')?.value"> @switch (activityForm.get('type')?.value) {
<ng-container *ngSwitchCase="'DIVIDEND'" i18n @case ('DIVIDEND') {
>Dividend</ng-container <ng-container i18n>Dividend</ng-container>
> }
<ng-container *ngSwitchCase="'INTEREST'" i18n>Value</ng-container> @case ('INTEREST') {
<ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container> <ng-container i18n>Value</ng-container>
<ng-container *ngSwitchCase="'LIABILITY'" i18n }
>Value</ng-container @case ('ITEM') {
> <ng-container i18n>Value</ng-container>
<ng-container *ngSwitchDefault i18n>Unit Price</ng-container> }
</ng-container> @case ('LIABILITY') {
<ng-container i18n>Value</ng-container>
}
@default {
<ng-container i18n>Unit Price</ng-container>
}
}
</mat-label> </mat-label>
<input <input
formControlName="unitPriceInCustomCurrency" formControlName="unitPriceInCustomCurrency"
@ -210,56 +219,67 @@
[ngClass]="{ 'd-none': !activityForm.get('currency')?.value }" [ngClass]="{ 'd-none': !activityForm.get('currency')?.value }"
> >
<mat-select formControlName="currencyOfUnitPrice"> <mat-select formControlName="currencyOfUnitPrice">
<mat-option @for (currency of currencies; track currency) {
*ngFor="let currency of currencies" <mat-option [value]="currency">
[value]="currency" {{ currency }}
> </mat-option>
{{ currency }} }
</mat-option>
</mat-select> </mat-select>
</div> </div>
<mat-error @if (
*ngIf=" activityForm.get('unitPriceInCustomCurrency').hasError('invalid')
activityForm.get('unitPriceInCustomCurrency').hasError('invalid') ) {
" <mat-error
><ng-container i18n ><ng-container i18n
>Oops! Could not get the historical exchange rate >Oops! Could not get the historical exchange rate
from</ng-container from</ng-container
>
{{
activityForm.get('date')?.value | date: defaultDateFormat
}}</mat-error
> >
{{ }
activityForm.get('date')?.value | date: defaultDateFormat
}}</mat-error
>
</mat-form-field> </mat-form-field>
<button @if (
*ngIf=" currentMarketPrice &&
currentMarketPrice && (data.activity.type === 'BUY' || data.activity.type === 'SELL') &&
(data.activity.type === 'BUY' || data.activity.type === 'SELL') && isToday(activityForm.get('date')?.value)
isToday(activityForm.get('date')?.value) ) {
" <button
class="ml-2 mt-1 no-min-width" class="ml-2 mt-1 no-min-width"
mat-button mat-button
title="Apply current market price" title="Apply current market price"
type="button" type="button"
(click)="applyCurrentMarketPrice()" (click)="applyCurrentMarketPrice()"
> >
<ion-icon class="text-muted" name="refresh-outline" /> <ion-icon class="text-muted" name="refresh-outline" />
</button> </button>
}
</div> </div>
</div> </div>
<div class="d-none"> <div class="d-none">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label <mat-label>
><ng-container [ngSwitch]="activityForm.get('type')?.value"> @switch (activityForm.get('type')?.value) {
<ng-container *ngSwitchCase="'DIVIDEND'" i18n @case ('DIVIDEND') {
>Dividend</ng-container <ng-container i18n>Dividend</ng-container>
> }
<ng-container *ngSwitchCase="'FEE'" i18n>Value</ng-container> @case ('FEE') {
<ng-container *ngSwitchCase="'INTEREST'" i18n>Value</ng-container> <ng-container i18n>Value</ng-container>
<ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container> }
<ng-container *ngSwitchCase="'LIABILITY'" i18n>Value</ng-container> @case ('INTEREST') {
<ng-container *ngSwitchDefault i18n>Unit Price</ng-container> <ng-container i18n>Value</ng-container>
</ng-container> }
@case ('ITEM') {
<ng-container i18n>Value</ng-container>
}
@case ('LIABILITY') {
<ng-container i18n>Value</ng-container>
}
@default {
<ng-container i18n>Unit Price</ng-container>
}
}
</mat-label> </mat-label>
<input formControlName="unitPrice" matInput type="number" /> <input formControlName="unitPrice" matInput type="number" />
<span class="ml-2" matTextSuffix>{{ <span class="ml-2" matTextSuffix>{{
@ -286,15 +306,17 @@
> >
{{ activityForm.get('currencyOfUnitPrice').value }} {{ activityForm.get('currencyOfUnitPrice').value }}
</div> </div>
<mat-error @if (activityForm.get('feeInCustomCurrency').hasError('invalid')) {
*ngIf="activityForm.get('feeInCustomCurrency').hasError('invalid')" <mat-error
><ng-container i18n ><ng-container i18n
>Oops! Could not get the historical exchange rate from</ng-container >Oops! Could not get the historical exchange rate
from</ng-container
>
{{
activityForm.get('date')?.value | date: defaultDateFormat
}}</mat-error
> >
{{ }
activityForm.get('date')?.value | date: defaultDateFormat
}}</mat-error
>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="d-none"> <div class="d-none">
@ -326,11 +348,11 @@
<mat-label i18n>Asset Class</mat-label> <mat-label i18n>Asset Class</mat-label>
<mat-select formControlName="assetClass"> <mat-select formControlName="assetClass">
<mat-option [value]="null" /> <mat-option [value]="null" />
<mat-option @for (assetClass of assetClasses; track assetClass) {
*ngFor="let assetClass of assetClasses" <mat-option [value]="assetClass.id">{{
[value]="assetClass.id" assetClass.label
>{{ assetClass.label }}</mat-option }}</mat-option>
> }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -342,11 +364,11 @@
<mat-label i18n>Asset Sub Class</mat-label> <mat-label i18n>Asset Sub Class</mat-label>
<mat-select formControlName="assetSubClass"> <mat-select formControlName="assetSubClass">
<mat-option [value]="null" /> <mat-option [value]="null" />
<mat-option @for (assetSubClass of assetSubClasses; track assetSubClass) {
*ngFor="let assetSubClass of assetSubClasses" <mat-option [value]="assetSubClass.id">{{
[value]="assetSubClass.id" assetSubClass.label
>{{ assetSubClass.label }}</mat-option }}</mat-option>
> }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -354,15 +376,16 @@
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Tags</mat-label> <mat-label i18n>Tags</mat-label>
<mat-chip-grid #tagsChipList> <mat-chip-grid #tagsChipList>
<mat-chip-row @for (tag of activityForm.get('tags')?.value; track tag) {
*ngFor="let tag of activityForm.get('tags')?.value" <mat-chip-row
matChipRemove matChipRemove
[removable]="true" [removable]="true"
(removed)="onRemoveTag(tag)" (removed)="onRemoveTag(tag)"
> >
{{ tag.name }} {{ tag.name }}
<ion-icon class="ml-2" matPrefix name="close-outline" /> <ion-icon class="ml-2" matPrefix name="close-outline" />
</mat-chip-row> </mat-chip-row>
}
<input <input
#tagInput #tagInput
name="close-outline" name="close-outline"
@ -375,12 +398,11 @@
#autocompleteTags="matAutocomplete" #autocompleteTags="matAutocomplete"
(optionSelected)="onAddTag($event)" (optionSelected)="onAddTag($event)"
> >
<mat-option @for (tag of filteredTagsObservable | async; track tag) {
*ngFor="let tag of filteredTagsObservable | async" <mat-option [value]="tag.id">
[value]="tag.id" {{ tag.name }}
> </mat-option>
{{ tag.name }} }
</mat-option>
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
</div> </div>

@ -16,12 +16,11 @@
> >
<mat-step [completed]="importStep === 0" [selected]="importStep === 0"> <mat-step [completed]="importStep === 0" [selected]="importStep === 0">
<ng-template matStepLabel> <ng-template matStepLabel>
<ng-container *ngIf="mode === 'DIVIDEND'" i18n @if (mode === 'DIVIDEND') {
>Select Holding</ng-container <ng-container i18n>Select Holding</ng-container>
> } @else {
<ng-container *ngIf="mode !== 'DIVIDEND'" i18n <ng-container i18n>Select File</ng-container>
>Select File</ng-container }
>
</ng-template> </ng-template>
<div class="pt-3"> <div class="pt-3">
@if (mode === 'DIVIDEND') { @if (mode === 'DIVIDEND') {
@ -35,30 +34,29 @@
<mat-select-trigger>{{ <mat-select-trigger>{{
uniqueAssetForm.get('uniqueAsset')?.value?.name uniqueAssetForm.get('uniqueAsset')?.value?.name
}}</mat-select-trigger> }}</mat-select-trigger>
<mat-option @for (holding of holdings; track holding) {
*ngFor="let holding of holdings" <mat-option
class="line-height-1" class="line-height-1"
[value]="{ [value]="{
dataSource: holding.dataSource, dataSource: holding.dataSource,
name: holding.name, name: holding.name,
symbol: holding.symbol symbol: holding.symbol
}" }"
>
<span
><b>{{ holding.name }}</b></span
>
<br />
<small class="text-muted"
>{{ holding.symbol | gfSymbol }} ·
{{ holding.currency }}</small
> >
</mat-option> <span
><b>{{ holding.name }}</b></span
>
<br />
<small class="text-muted"
>{{ holding.symbol | gfSymbol }} ·
{{ holding.currency }}</small
>
</mat-option>
}
</mat-select> </mat-select>
<mat-spinner @if (isLoading) {
*ngIf="isLoading" <mat-spinner class="position-absolute" [diameter]="20" />
class="position-absolute" }
[diameter]="20"
/>
</mat-form-field> </mat-form-field>
<div class="d-flex flex-column justify-content-center"> <div class="d-flex flex-column justify-content-center">
<button <button
@ -111,36 +109,36 @@
<mat-step [completed]="importStep === 1" [selected]="importStep === 1"> <mat-step [completed]="importStep === 1" [selected]="importStep === 1">
<ng-template matStepLabel> <ng-template matStepLabel>
<ng-container *ngIf="mode === 'DIVIDEND'" i18n @if (mode === 'DIVIDEND') {
>Select Dividends</ng-container <ng-container i18n>Select Dividends</ng-container>
> } @else {
<ng-container *ngIf="mode !== 'DIVIDEND'" i18n <ng-container i18n>Select Activities</ng-container>
>Select Activities</ng-container }
>
</ng-template> </ng-template>
<div class="pt-3"> <div class="pt-3">
@if (errorMessages?.length === 0) { @if (errorMessages?.length === 0) {
<gf-activities-table @if (importStep === 1) {
*ngIf="importStep === 1" <gf-activities-table
[baseCurrency]="data?.user?.settings?.baseCurrency" [baseCurrency]="data?.user?.settings?.baseCurrency"
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="data?.deviceType" [deviceType]="data?.deviceType"
[hasPermissionToCreateActivity]="false" [hasPermissionToCreateActivity]="false"
[hasPermissionToDeleteActivity]="false" [hasPermissionToDeleteActivity]="false"
[hasPermissionToExportActivities]="false" [hasPermissionToExportActivities]="false"
[hasPermissionToFilter]="false" [hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[locale]="data?.user?.settings?.locale" [locale]="data?.user?.settings?.locale"
[pageSize]="maxSafeInteger" [pageSize]="maxSafeInteger"
[showActions]="false" [showActions]="false"
[showCheckbox]="true" [showCheckbox]="true"
[showSymbolColumn]="false" [showSymbolColumn]="false"
[sortColumn]="sortColumn" [sortColumn]="sortColumn"
[sortDirection]="sortDirection" [sortDirection]="sortDirection"
[sortDisabled]="true" [sortDisabled]="true"
[totalItems]="totalItems" [totalItems]="totalItems"
(selectedActivities)="updateSelection($event)" (selectedActivities)="updateSelection($event)"
/> />
}
<div class="d-flex justify-content-end mt-3"> <div class="d-flex justify-content-end mt-3">
<button mat-button (click)="onReset(stepper)"> <button mat-button (click)="onReset(stepper)">
<ng-container i18n>Back</ng-container> <ng-container i18n>Back</ng-container>
@ -157,25 +155,23 @@
</div> </div>
} @else { } @else {
<mat-accordion displayMode="flat"> <mat-accordion displayMode="flat">
<mat-expansion-panel @for (message of errorMessages; track message; let i = $index) {
*ngFor="let message of errorMessages; let i = index" <mat-expansion-panel [disabled]="!details[i]">
[disabled]="!details[i]" <mat-expansion-panel-header class="pl-1">
> <mat-panel-title>
<mat-expansion-panel-header class="pl-1"> <div class="d-flex">
<mat-panel-title> <div class="align-items-center d-flex mr-2">
<div class="d-flex"> <ion-icon name="warning-outline" />
<div class="align-items-center d-flex mr-2"> </div>
<ion-icon name="warning-outline" /> <div>{{ message }}</div>
</div> </div>
<div>{{ message }}</div> </mat-panel-title>
</div> </mat-expansion-panel-header>
</mat-panel-title> @if (details[i]) {
</mat-expansion-panel-header> <pre class="m-0"><code>{{ details[i] | json }}</code></pre>
<pre }
*ngIf="details[i]" </mat-expansion-panel>
class="m-0" }
><code>{{ details[i] | json }}</code></pre>
</mat-expansion-panel>
</mat-accordion> </mat-accordion>
<div class="d-flex justify-content-end mt-3"> <div class="d-flex justify-content-end mt-3">
<button mat-button (click)="onReset(stepper)"> <button mat-button (click)="onReset(stepper)">

@ -27,10 +27,9 @@
class="align-items-center d-flex flex-grow-1 mr-2 text-truncate" class="align-items-center d-flex flex-grow-1 mr-2 text-truncate"
> >
<span i18n>Absolute Asset Performance</span> <span i18n>Absolute Asset Performance</span>
<gf-premium-indicator @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <gf-premium-indicator class="ml-1" />
class="ml-1" }
/>
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
@ -71,10 +70,9 @@
class="align-items-center d-flex flex-grow-1 mr-2 text-truncate" class="align-items-center d-flex flex-grow-1 mr-2 text-truncate"
> >
<span i18n>Absolute Currency Performance</span> <span i18n>Absolute Currency Performance</span>
<gf-premium-indicator @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <gf-premium-indicator class="ml-1" />
class="ml-1" }
/>
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
@ -170,39 +168,42 @@
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<ol class="mb-0 ml-1 pl-3"> <ol class="mb-0 ml-1 pl-3">
<li *ngFor="let holding of top3" class="py-1"> @for (holding of top3; track holding) {
<a <li class="py-1">
class="d-flex" <a
[queryParams]="{ class="d-flex"
dataSource: holding.dataSource, [queryParams]="{
holdingDetailDialog: true, dataSource: holding.dataSource,
symbol: holding.symbol holdingDetailDialog: true,
}" symbol: holding.symbol
[routerLink]="[]" }"
> [routerLink]="[]"
<div class="flex-grow-1 mr-2">{{ holding.name }}</div> >
<div class="d-flex justify-content-end"> <div class="flex-grow-1 mr-2">{{ holding.name }}</div>
<gf-value <div class="d-flex justify-content-end">
class="justify-content-end" <gf-value
position="end" class="justify-content-end"
[colorizeSign]="true" position="end"
[isPercent]="true" [colorizeSign]="true"
[locale]="user?.settings?.locale" [isPercent]="true"
[value]="holding.netPerformancePercentWithCurrencyEffect" [locale]="user?.settings?.locale"
/> [value]="holding.netPerformancePercentWithCurrencyEffect"
</div> />
</a> </div>
</li> </a>
</li>
}
</ol> </ol>
<div> <div>
<ngx-skeleton-loader @if (!top3) {
*ngIf="!top3" <ngx-skeleton-loader
animation="pulse" animation="pulse"
[theme]="{ [theme]="{
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'
}" }"
/> />
}
</div> </div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
@ -216,39 +217,42 @@
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<ol class="mb-0 ml-1 pl-3"> <ol class="mb-0 ml-1 pl-3">
<li *ngFor="let holding of bottom3" class="py-1"> @for (holding of bottom3; track holding) {
<a <li class="py-1">
class="d-flex" <a
[queryParams]="{ class="d-flex"
dataSource: holding.dataSource, [queryParams]="{
holdingDetailDialog: true, dataSource: holding.dataSource,
symbol: holding.symbol holdingDetailDialog: true,
}" symbol: holding.symbol
[routerLink]="[]" }"
> [routerLink]="[]"
<div class="flex-grow-1 mr-2">{{ holding.name }}</div> >
<div class="d-flex justify-content-end"> <div class="flex-grow-1 mr-2">{{ holding.name }}</div>
<gf-value <div class="d-flex justify-content-end">
class="justify-content-end" <gf-value
position="end" class="justify-content-end"
[colorizeSign]="true" position="end"
[isPercent]="true" [colorizeSign]="true"
[locale]="user?.settings?.locale" [isPercent]="true"
[value]="holding.netPerformancePercentWithCurrencyEffect" [locale]="user?.settings?.locale"
/> [value]="holding.netPerformancePercentWithCurrencyEffect"
</div> />
</a> </div>
</li> </a>
</li>
}
</ol> </ol>
<div> <div>
<ngx-skeleton-loader @if (!bottom3) {
*ngIf="!bottom3" <ngx-skeleton-loader
animation="pulse" animation="pulse"
[theme]="{ [theme]="{
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'
}" }"
/> />
}
</div> </div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
@ -262,10 +266,9 @@
class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate" class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate"
> >
<span i18n>Portfolio Evolution</span> <span i18n>Portfolio Evolution</span>
<gf-premium-indicator @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <gf-premium-indicator class="ml-1" />
class="ml-1" }
/>
</div> </div>
</div> </div>
<div class="chart-container"> <div class="chart-container">
@ -292,10 +295,9 @@
class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate" class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate"
> >
<span i18n>Investment Timeline</span> <span i18n>Investment Timeline</span>
<gf-premium-indicator @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <gf-premium-indicator class="ml-1" />
class="ml-1" }
/>
</div> </div>
<gf-toggle <gf-toggle
class="d-none d-lg-block" class="d-none d-lg-block"
@ -305,26 +307,28 @@
(change)="onChangeGroupBy($event.value)" (change)="onChangeGroupBy($event.value)"
/> />
</div> </div>
<div *ngIf="streaks" class="row"> @if (streaks) {
<div class="col-md-6 col-xs-12 my-2"> <div class="row">
<gf-value <div class="col-md-6 col-xs-12 my-2">
i18n <gf-value
size="large" i18n
[unit]="unitCurrentStreak" size="large"
[value]="streaks?.currentStreak" [unit]="unitCurrentStreak"
>Current Streak</gf-value [value]="streaks?.currentStreak"
> >Current Streak</gf-value
</div> >
<div class="col-md-6 col-xs-12 my-2"> </div>
<gf-value <div class="col-md-6 col-xs-12 my-2">
i18n <gf-value
size="large" i18n
[unit]="unitLongestStreak" size="large"
[value]="streaks?.longestStreak" [unit]="unitLongestStreak"
>Longest Streak</gf-value [value]="streaks?.longestStreak"
> >Longest Streak</gf-value
>
</div>
</div> </div>
</div> }
<div class="chart-container"> <div class="chart-container">
<gf-investment-chart <gf-investment-chart
class="h-100" class="h-100"
@ -350,10 +354,9 @@
class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate" class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate"
> >
<span i18n>Dividend Timeline</span> <span i18n>Dividend Timeline</span>
<gf-premium-indicator @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <gf-premium-indicator class="ml-1" />
class="ml-1" }
/>
</div> </div>
<gf-toggle <gf-toggle
class="d-none d-lg-block" class="d-none d-lg-block"

@ -4,11 +4,10 @@
<h2 class="d-none d-sm-block h3 mb-3 text-center" i18n>FIRE</h2> <h2 class="d-none d-sm-block h3 mb-3 text-center" i18n>FIRE</h2>
<div> <div>
<h4 class="align-items-center d-flex mb-3"> <h4 class="align-items-center d-flex mb-3">
<span i18n>Calculator</span <span i18n>Calculator</span>
><gf-premium-indicator @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <gf-premium-indicator class="ml-1" />
class="ml-1" }
/>
</h4> </h4>
<gf-fire-calculator <gf-fire-calculator
[annualInterestRate]="user?.settings?.annualInterestRate" [annualInterestRate]="user?.settings?.annualInterestRate"
@ -38,67 +37,68 @@
</div> </div>
<div> <div>
<h4 class="align-items-center d-flex"> <h4 class="align-items-center d-flex">
<span i18n>4% Rule</span <span i18n>4% Rule</span>
><gf-premium-indicator @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <gf-premium-indicator class="ml-1" />
class="ml-1" }
/>
</h4> </h4>
<div *ngIf="isLoading"> @if (isLoading) {
<ngx-skeleton-loader <div>
animation="pulse" <ngx-skeleton-loader
class="my-1" animation="pulse"
[theme]="{ class="my-1"
height: '1rem', [theme]="{
width: '100%' height: '1rem',
}" width: '100%'
/> }"
<ngx-skeleton-loader
animation="pulse"
[theme]="{
height: '1rem',
width: '10rem'
}"
/>
</div>
<div
*ngIf="!isLoading"
i18n
[ngClass]="{ 'text-muted': user?.subscription?.type === 'Basic' }"
>
If you retire today, you would be able to withdraw
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerYear?.toNumber()"
/>
per year</span
>
or
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerMonth?.toNumber()"
/> />
per month</span <ngx-skeleton-loader
>, based on your total assets of animation="pulse"
<span class="font-weight-bold" [theme]="{
><gf-value height: '1rem',
class="d-inline-block" width: '10rem'
[isCurrency]="true" }"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="fireWealth?.toNumber()"
/> />
</span> </div>
and a withdrawal rate of 4%. } @else {
</div> <div
i18n
[ngClass]="{ 'text-muted': user?.subscription?.type === 'Basic' }"
>
If you retire today, you would be able to withdraw
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerYear?.toNumber()"
/>
per year</span
>
or
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerMonth?.toNumber()"
/>
per month</span
>, based on your total assets of
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="fireWealth?.toNumber()"
/>
</span>
and a withdrawal rate of 4%.
</div>
}
</div> </div>
</div> </div>
@ -119,11 +119,10 @@
</p> </p>
<div class="mb-4"> <div class="mb-4">
<h4 class="align-items-center d-flex m-0"> <h4 class="align-items-center d-flex m-0">
<span i18n>Emergency Fund</span <span i18n>Emergency Fund</span>
><gf-premium-indicator @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <gf-premium-indicator class="ml-1" />
class="ml-1" }
/>
</h4> </h4>
<gf-rules <gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder" [hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
@ -132,11 +131,10 @@
</div> </div>
<div class="mb-4"> <div class="mb-4">
<h4 class="align-items-center d-flex m-0"> <h4 class="align-items-center d-flex m-0">
<span i18n>Currency Cluster Risks</span <span i18n>Currency Cluster Risks</span>
><gf-premium-indicator @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <gf-premium-indicator class="ml-1" />
class="ml-1" }
/>
</h4> </h4>
<gf-rules <gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder" [hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
@ -145,11 +143,10 @@
</div> </div>
<div class="mb-4"> <div class="mb-4">
<h4 class="align-items-center d-flex m-0"> <h4 class="align-items-center d-flex m-0">
<span i18n>Account Cluster Risks</span <span i18n>Account Cluster Risks</span>
><gf-premium-indicator @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <gf-premium-indicator class="ml-1" />
class="ml-1" }
/>
</h4> </h4>
<gf-rules <gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder" [hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
@ -158,11 +155,10 @@
</div> </div>
<div> <div>
<h4 class="align-items-center d-flex m-0"> <h4 class="align-items-center d-flex m-0">
<span i18n>Fees</span <span i18n>Fees</span>
><gf-premium-indicator @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <gf-premium-indicator class="ml-1" />
class="ml-1" }
/>
</h4> </h4>
<gf-rules <gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder" [hasPermissionToCreateOrder]="hasPermissionToCreateOrder"

@ -8,22 +8,23 @@
[disablePagination]="true" [disablePagination]="true"
[tabPanel]="tabPanel" [tabPanel]="tabPanel"
> >
<ng-container *ngFor="let tab of tabs"> @for (tab of tabs; track tab) {
<a @if (tab.showCondition !== false) {
#rla="routerLinkActive" <a
*ngIf="tab.showCondition !== false" #rla="routerLinkActive"
class="no-min-width px-3" class="no-min-width px-3"
mat-tab-link mat-tab-link
routerLinkActive routerLinkActive
[active]="rla.isActive" [active]="rla.isActive"
[routerLink]="tab.path" [routerLink]="tab.path"
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
> >
<ion-icon <ion-icon
[name]="tab.iconName" [name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'" [size]="deviceType === 'mobile' ? 'large' : 'small'"
/> />
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div> <div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a> </a>
</ng-container> }
}
</nav> </nav>

@ -9,18 +9,20 @@
for most people. Revenue is used to cover the costs of the hosting for most people. Revenue is used to cover the costs of the hosting
infrastructure and to fund ongoing development. infrastructure and to fund ongoing development.
</p> </p>
<p *ngIf="user?.subscription?.type === 'Basic'"> @if (user?.subscription?.type === 'Basic') {
If you plan to open an account at <i>DEGIRO</i>, <i>finpension</i>, <p>
<i>frankly</i>, <i>Interactive Brokers</i>, <i>Swissquote</i>, If you plan to open an account at <i>DEGIRO</i>, <i>finpension</i>,
<i>VIAC</i>, or <i>Zak</i>, please <i>frankly</i>, <i>Interactive Brokers</i>, <i>Swissquote</i>,
<a href="mailto:hi@ghostfol.io?Subject=Referral link for..." <i>VIAC</i>, or <i>Zak</i>, please
>contact us</a <a href="mailto:hi@ghostfol.io?Subject=Referral link for..."
> >contact us</a
to use our referral link and get a Ghostfolio Premium membership for >
one year. Looking for a student discount? Request it to use our referral link and get a Ghostfolio Premium membership for
<a href="mailto:hi@ghostfol.io?Subject=Student Discount">here</a> with one year. Looking for a student discount? Request it
your university e-mail address. <a href="mailto:hi@ghostfol.io?Subject=Student Discount">here</a>
</p> with your university e-mail address.
</p>
}
<p i18n> <p i18n>
If you prefer to run Ghostfolio on your own infrastructure, please If you prefer to run Ghostfolio on your own infrastructure, please
find the source code and further instructions on find the source code and further instructions on
@ -91,15 +93,14 @@
</div> </div>
<p i18n>Self-hosted, update manually.</p> <p i18n>Self-hosted, update manually.</p>
<p class="h5 text-right" i18n>Free</p> <p class="h5 text-right" i18n>Free</p>
<div @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <div class="d-none d-lg-block hidden mt-3 text-center">
class="d-none d-lg-block hidden mt-3 text-center" <a color="primary" mat-flat-button>&nbsp;</a>
> <p class="m-0 text-muted">
<a color="primary" mat-flat-button>&nbsp;</a> <small>&nbsp;</small>
<p class="m-0 text-muted"> </p>
<small>&nbsp;</small> </div>
</p> }
</div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -113,9 +114,11 @@
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="align-items-center d-flex mb-2"> <div class="align-items-center d-flex mb-2">
<h4 class="flex-grow-1 m-0">Basic</h4> <h4 class="flex-grow-1 m-0">Basic</h4>
<div *ngIf="user?.subscription?.type === 'Basic'"> @if (user?.subscription?.type === 'Basic') {
<ion-icon class="mr-1" name="checkmark-outline" /> <div>
</div> <ion-icon class="mr-1" name="checkmark-outline" />
</div>
}
</div> </div>
<p i18n> <p i18n>
For new investors who are just getting started with trading. For new investors who are just getting started with trading.
@ -148,15 +151,14 @@
</div> </div>
<p i18n>Fully managed Ghostfolio cloud offering.</p> <p i18n>Fully managed Ghostfolio cloud offering.</p>
<p class="h5 text-right" i18n>Free</p> <p class="h5 text-right" i18n>Free</p>
<div @if (user?.subscription?.type === 'Basic') {
*ngIf="user?.subscription?.type === 'Basic'" <div class="d-none d-lg-block hidden mt-3 text-center">
class="d-none d-lg-block hidden mt-3 text-center" <a color="primary" mat-flat-button>&nbsp;</a>
> <p class="m-0 text-muted">
<a color="primary" mat-flat-button>&nbsp;</a> <small>&nbsp;</small>
<p class="m-0 text-muted"> </p>
<small>&nbsp;</small> </div>
</p> }
</div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -173,9 +175,11 @@
<span>Premium</span> <span>Premium</span>
<gf-premium-indicator class="ml-1" [enableLink]="false" /> <gf-premium-indicator class="ml-1" [enableLink]="false" />
</h4> </h4>
<div *ngIf="user?.subscription?.type === 'Premium'"> @if (user?.subscription?.type === 'Premium') {
<ion-icon class="mr-1" name="checkmark-outline" /> <div>
</div> <ion-icon class="mr-1" name="checkmark-outline" />
</div>
}
</div> </div>
<p i18n> <p i18n>
For ambitious investors who need the full picture of their For ambitious investors who need the full picture of their
@ -240,58 +244,61 @@
<p i18n>Fully managed Ghostfolio cloud offering.</p> <p i18n>Fully managed Ghostfolio cloud offering.</p>
<p class="h5 text-right" [hidden]="!price"> <p class="h5 text-right" [hidden]="!price">
<span class="font-weight-normal"> <span class="font-weight-normal">
<ng-container *ngIf="coupon" @if (coupon) {
><del class="text-muted" <del class="text-muted"
>{{ baseCurrency }}&nbsp;{{ price }}</del >{{ baseCurrency }}&nbsp;{{ price }}</del
>&nbsp;{{ baseCurrency }}&nbsp;<strong>{{ >&nbsp;{{ baseCurrency }}&nbsp;<strong>{{
price - coupon price - coupon
}}</strong> }}</strong>
</ng-container> } @else {
<ng-container *ngIf="!coupon" {{ baseCurrency }}&nbsp;<strong>{{ price }}</strong>
>{{ baseCurrency }}&nbsp;<strong>{{ }
price &nbsp;<span i18n>per year</span></span
}}</strong></ng-container
>&nbsp;<span i18n>per year</span></span
> >
</p> </p>
<div @if (
*ngIf=" hasPermissionToUpdateUserSettings &&
hasPermissionToUpdateUserSettings && user?.subscription?.type === 'Basic'
user?.subscription?.type === 'Basic' ) {
" <div class="mt-3 text-center">
class="mt-3 text-center" <button
> color="primary"
<button color="primary" mat-flat-button (click)="onCheckout()"> mat-flat-button
<ng-container (click)="onCheckout()"
*ngIf="user.subscription.offer === 'default'"
i18n
>Upgrade Plan</ng-container
> >
<ng-container @if (user.subscription.offer === 'default') {
*ngIf=" <ng-container i18n>Upgrade Plan</ng-container>
} @else if (
user.subscription.offer === 'renewal' || user.subscription.offer === 'renewal' ||
user.subscription.offer === 'renewal-early-bird' user.subscription.offer === 'renewal-early-bird'
" ) {
i18n <ng-container i18n>Renew Plan</ng-container>
>Renew Plan</ng-container }
> </button>
</button> <p class="m-0 text-muted">
<p class="m-0 text-muted"> <small i18n>One-time payment, no auto-renewal.</small>
<small i18n>One-time payment, no auto-renewal.</small> </p>
</p> </div>
</div> }
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div *ngIf="!user" class="row"> @if (!user) {
<div class="col mt-3 text-center"> <div class="row">
<a color="primary" i18n mat-flat-button [routerLink]="routerLinkRegister"> <div class="col mt-3 text-center">
Get Started <a
</a> color="primary"
<p class="m-0 text-muted"><small i18n>Its free.</small></p> i18n
mat-flat-button
[routerLink]="routerLinkRegister"
>
Get Started
</a>
<p class="m-0 text-muted"><small i18n>Its free.</small></p>
</div>
</div> </div>
</div> }
</div> </div>

@ -24,110 +24,121 @@
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
<div *ngIf="portfolioPublicDetails?.hasDetails" class="col-md-4"> @if (portfolioPublicDetails?.hasDetails) {
<mat-card appearance="outlined" class="mb-3"> <div class="col-md-4">
<mat-card-header class="overflow-hidden w-100"> <mat-card appearance="outlined" class="mb-3">
<mat-card-title class="text-truncate" i18n>Currencies</mat-card-title> <mat-card-header class="overflow-hidden w-100">
</mat-card-header> <mat-card-title class="text-truncate" i18n
<mat-card-content> >Currencies</mat-card-title
<gf-portfolio-proportion-chart >
[isInPercent]="true" </mat-card-header>
[keys]="['currency']" <mat-card-content>
[maxItems]="10" <gf-portfolio-proportion-chart
[positions]="positions"
/>
</mat-card-content>
</mat-card>
</div>
<div *ngIf="portfolioPublicDetails?.hasDetails" class="col-md-4">
<mat-card appearance="outlined" class="mb-3">
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="text-truncate" i18n>Sectors</mat-card-title>
</mat-card-header>
<mat-card-content>
<gf-portfolio-proportion-chart
[isInPercent]="true"
[keys]="['name']"
[maxItems]="10"
[positions]="sectors"
/>
</mat-card-content>
</mat-card>
</div>
<div *ngIf="portfolioPublicDetails?.hasDetails" class="col-md-4">
<mat-card appearance="outlined" class="mb-3">
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="text-truncate" i18n>Continents</mat-card-title>
</mat-card-header>
<mat-card-content>
<gf-portfolio-proportion-chart
[isInPercent]="true"
[keys]="['name']"
[positions]="continents"
/>
</mat-card-content>
</mat-card>
</div>
</div>
<div *ngIf="portfolioPublicDetails?.hasDetails" class="row world-map-chart">
<div class="col-lg">
<mat-card appearance="outlined" class="mb-3">
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="text-truncate" i18n>Regions</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="world-map-chart-container">
<gf-world-map-chart
format="{0}%"
[countries]="countries"
[isInPercent]="true" [isInPercent]="true"
[keys]="['currency']"
[maxItems]="10"
[positions]="positions"
/> />
</div> </mat-card-content>
<div class="row"> </mat-card>
<div class="col-xs-12 col-md my-2"> </div>
<gf-value }
i18n @if (portfolioPublicDetails?.hasDetails) {
size="large" <div class="col-md-4">
[isPercent]="true" <mat-card appearance="outlined" class="mb-3">
[value]="markets?.developedMarkets?.value" <mat-card-header class="overflow-hidden w-100">
>Developed Markets</gf-value <mat-card-title class="text-truncate" i18n>Sectors</mat-card-title>
> </mat-card-header>
</div> <mat-card-content>
<div class="col-xs-12 col-md my-2"> <gf-portfolio-proportion-chart
<gf-value [isInPercent]="true"
i18n [keys]="['name']"
size="large" [maxItems]="10"
[isPercent]="true" [positions]="sectors"
[value]="markets?.emergingMarkets?.value" />
>Emerging Markets</gf-value </mat-card-content>
> </mat-card>
</div> </div>
<div class="col-xs-12 col-md my-2"> }
<gf-value @if (portfolioPublicDetails?.hasDetails) {
i18n <div class="col-md-4">
size="large" <mat-card appearance="outlined" class="mb-3">
[isPercent]="true" <mat-card-header class="overflow-hidden w-100">
[value]="markets?.otherMarkets?.value" <mat-card-title class="text-truncate" i18n
>Other Markets</gf-value >Continents</mat-card-title
>
</div>
<div
*ngIf="markets?.[UNKNOWN_KEY]?.value > 0"
class="col-xs-12 col-md my-2"
> >
<gf-value </mat-card-header>
i18n <mat-card-content>
size="large" <gf-portfolio-proportion-chart
[isPercent]="true" [isInPercent]="true"
[value]="markets?.[UNKNOWN_KEY]?.value" [keys]="['name']"
>No data available</gf-value [positions]="continents"
> />
</mat-card-content>
</mat-card>
</div>
}
</div>
@if (portfolioPublicDetails?.hasDetails) {
<div class="row world-map-chart">
<div class="col-lg">
<mat-card appearance="outlined" class="mb-3">
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="text-truncate" i18n>Regions</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="world-map-chart-container">
<gf-world-map-chart
format="{0}%"
[countries]="countries"
[isInPercent]="true"
/>
</div> </div>
</div> <div class="row">
</mat-card-content> <div class="col-xs-12 col-md my-2">
</mat-card> <gf-value
i18n
size="large"
[isPercent]="true"
[value]="markets?.developedMarkets?.value"
>Developed Markets</gf-value
>
</div>
<div class="col-xs-12 col-md my-2">
<gf-value
i18n
size="large"
[isPercent]="true"
[value]="markets?.emergingMarkets?.value"
>Emerging Markets</gf-value
>
</div>
<div class="col-xs-12 col-md my-2">
<gf-value
i18n
size="large"
[isPercent]="true"
[value]="markets?.otherMarkets?.value"
>Other Markets</gf-value
>
</div>
@if (markets?.[UNKNOWN_KEY]?.value > 0) {
<div class="col-xs-12 col-md my-2">
<gf-value
i18n
size="large"
[isPercent]="true"
[value]="markets?.[UNKNOWN_KEY]?.value"
>No data available</gf-value
>
</div>
}
</div>
</mat-card-content>
</mat-card>
</div>
</div> </div>
</div> }
<div class="row"> <div class="row">
<div class="col-lg"> <div class="col-lg">
<gf-holdings-table <gf-holdings-table

@ -14,44 +14,47 @@
</div> </div>
</div> </div>
<div *ngIf="hasPermissionToCreateUser" class="button-container row"> @if (hasPermissionToCreateUser) {
<div class="align-items-center col d-flex justify-content-center"> <div class="button-container row">
<div class="py-5 text-center"> <div class="align-items-center col d-flex justify-content-center">
<button <div class="py-5 text-center">
class="d-inline-block"
color="primary"
mat-flat-button
(click)="createAccount()"
>
<ng-container i18n>Create Account</ng-container>
</button>
<ng-container *ngIf="hasPermissionForSocialLogin">
<div class="my-3 text-muted" i18n>or</div>
<button <button
*ngIf="false" class="d-inline-block"
class="d-block mb-2 px-4 rounded-pill" color="primary"
mat-stroked-button mat-flat-button
(click)="onLoginWithInternetIdentity()" (click)="createAccount()"
> >
<img <ng-container i18n>Create Account</ng-container>
class="mr-2"
src="../assets/icons/internet-computer.svg"
style="height: 0.75rem"
/>
<span i18n>Continue with Internet Identity</span>
</button> </button>
<a @if (hasPermissionForSocialLogin) {
class="px-4 rounded-pill w-100" <div class="my-3 text-muted" i18n>or</div>
href="../api/v1/auth/google" @if (false) {
mat-stroked-button <button
><img class="d-block mb-2 px-4 rounded-pill"
class="mr-2" mat-stroked-button
src="../assets/icons/google.svg" (click)="onLoginWithInternetIdentity()"
style="height: 1rem" >
/><span i18n>Continue with Google</span></a <img
> class="mr-2"
</ng-container> src="../assets/icons/internet-computer.svg"
style="height: 0.75rem"
/>
<span i18n>Continue with Internet Identity</span>
</button>
}
<a
class="px-4 rounded-pill w-100"
href="../api/v1/auth/google"
mat-stroked-button
><img
class="mr-2"
src="../assets/icons/google.svg"
style="height: 1rem"
/><span i18n>Continue with Google</span></a
>
}
</div>
</div> </div>
</div> </div>
</div> }
</div> </div>

@ -1,8 +1,8 @@
<h1 mat-dialog-title> <h1 mat-dialog-title>
<span i18n>Create Account</span <span i18n>Create Account</span>
><span *ngIf="data.role === 'ADMIN'" class="badge badge-light ml-2">{{ @if (data.role === 'ADMIN') {
data.role <span class="badge badge-light ml-2">{{ data.role }}</span>
}}</span> }
</h1> </h1>
<div class="py-3" mat-dialog-content> <div class="py-3" mat-dialog-content>
<div> <div>

@ -88,16 +88,22 @@
Available in Available in
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
<ng-container @for (
*ngFor="let language of product1.languages; last as isLast" language of product1.languages;
>{{ language }}{{ isLast ? '' : ', ' }}</ng-container track language;
> let isLast = $last
) {
{{ language }}{{ isLast ? '' : ', ' }}
}
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
<ng-container @for (
*ngFor="let language of product2.languages; last as isLast" language of product2.languages;
>{{ language }}{{ isLast ? '' : ', ' }}</ng-container track language;
> let isLast = $last
) {
{{ language }}{{ isLast ? '' : ', ' }}
}
</td> </td>
</tr> </tr>
<tr class="mat-mdc-row"> <tr class="mat-mdc-row">
@ -105,18 +111,18 @@
Open Source Software Open Source Software
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
<ng-container *ngIf="product1.isOpenSource" i18n @if (product1.isOpenSource) {
>✅ Yes</ng-container <ng-container i18n>✅ Yes</ng-container>
><ng-container *ngIf="!product1.isOpenSource" i18n } @else {
>❌ No</ng-container <ng-container i18n>❌ No</ng-container>
> }
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
<ng-container *ngIf="product2.isOpenSource" i18n @if (product2.isOpenSource) {
>✅ Yes</ng-container <ng-container i18n>✅ Yes</ng-container>
><ng-container *ngIf="!product2.isOpenSource" i18n } @else {
>❌ No <ng-container i18n>❌ No </ng-container>
</ng-container> }
</td> </td>
</tr> </tr>
<tr class="mat-mdc-row"> <tr class="mat-mdc-row">
@ -124,26 +130,18 @@
Self-Hosting Self-Hosting
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
<ng-container @if (product1.hasSelfHostingAbility === true) {
*ngIf="product1.hasSelfHostingAbility === true" <ng-container i18n>✅ Yes</ng-container>
i18n } @else if (product1.hasSelfHostingAbility === false) {
>✅ Yes</ng-container <ng-container i18n>❌ No</ng-container>
><ng-container }
*ngIf="product1.hasSelfHostingAbility === false"
i18n
>❌ No</ng-container
>
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
<ng-container @if (product2.hasSelfHostingAbility === true) {
*ngIf="product2.hasSelfHostingAbility === true" <ng-container i18n>✅ Yes</ng-container>
i18n } @else if (product2.hasSelfHostingAbility === false) {
>✅ Yes</ng-container <ng-container i18n>❌ No</ng-container>
><ng-container }
*ngIf="product2.hasSelfHostingAbility === false"
i18n
>❌ No</ng-container
>
</td> </td>
</tr> </tr>
<tr class="mat-mdc-row"> <tr class="mat-mdc-row">
@ -151,18 +149,18 @@
Use anonymously Use anonymously
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
<ng-container *ngIf="product1.useAnonymously === true" i18n @if (product1.useAnonymously === true) {
>✅ Yes</ng-container <ng-container i18n>✅ Yes</ng-container>
><ng-container *ngIf="product1.useAnonymously === false" i18n } @else if (product1.useAnonymously === false) {
>❌ No</ng-container <ng-container i18n>❌ No</ng-container>
> }
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
<ng-container *ngIf="product2.useAnonymously === true" i18n @if (product2.useAnonymously === true) {
>✅ Yes</ng-container <ng-container i18n>✅ Yes</ng-container>
><ng-container *ngIf="product2.useAnonymously === false" i18n } @else if (product2.useAnonymously === false) {
>❌ No</ng-container <ng-container i18n>❌ No</ng-container>
> }
</td> </td>
</tr> </tr>
<tr class="mat-mdc-row"> <tr class="mat-mdc-row">
@ -170,18 +168,18 @@
Free Plan Free Plan
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
<ng-container *ngIf="product1.hasFreePlan === true" i18n @if (product1.hasFreePlan === true) {
>✅ Yes</ng-container <ng-container i18n>✅ Yes</ng-container>
><ng-container *ngIf="product1.hasFreePlan === false" i18n } @else if (product1.hasFreePlan === false) {
>❌ No</ng-container <ng-container i18n>❌ No</ng-container>
> }
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
<ng-container *ngIf="product2.hasFreePlan === true" i18n @if (product2.hasFreePlan === true) {
>✅ Yes</ng-container <ng-container i18n>✅ Yes</ng-container>
><ng-container *ngIf="product2.hasFreePlan === false" i18n } @else if (product2.hasFreePlan === false) {
>❌ No</ng-container <ng-container i18n>❌ No</ng-container>
> }
</td> </td>
</tr> </tr>
<tr class="mat-mdc-row"> <tr class="mat-mdc-row">
@ -191,18 +189,20 @@
<span i18n>year</span> <span i18n>year</span>
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
<ng-container *ngIf="product2.pricingPerYear" @if (product2.pricingPerYear) {
><span i18n>Starting from</span> <span i18n>Starting from</span>
{{ product2.pricingPerYear }} / {{ product2.pricingPerYear }} /
<span i18n>year</span></ng-container <span i18n>year</span>
> }
</td> </td>
</tr> </tr>
<tr *ngIf="product1.note || product2.note" class="mat-mdc-row"> @if (product1.note || product2.note) {
<td class="mat-mdc-cell px-3 py-2 text-right" i18n>Notes</td> <tr class="mat-mdc-row">
<td class="mat-mdc-cell px-1 py-2">{{ product1.note }}</td> <td class="mat-mdc-cell px-3 py-2 text-right" i18n>Notes</td>
<td class="mat-mdc-cell px-1 py-2">{{ product2.note }}</td> <td class="mat-mdc-cell px-1 py-2">{{ product1.note }}</td>
</tr> <td class="mat-mdc-cell px-1 py-2">{{ product2.note }}</td>
</tr>
}
</tbody> </tbody>
</table> </table>
</section> </section>

@ -8,22 +8,23 @@
[disablePagination]="true" [disablePagination]="true"
[tabPanel]="tabPanel" [tabPanel]="tabPanel"
> >
<ng-container *ngFor="let tab of tabs"> @for (tab of tabs; track tab) {
<a @if (tab.showCondition !== false) {
#rla="routerLinkActive" <a
*ngIf="tab.showCondition !== false" #rla="routerLinkActive"
class="no-min-width px-3" class="no-min-width px-3"
mat-tab-link mat-tab-link
routerLinkActive routerLinkActive
[active]="rla.isActive" [active]="rla.isActive"
[routerLink]="tab.path" [routerLink]="tab.path"
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
> >
<ion-icon <ion-icon
[name]="tab.iconName" [name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'" [size]="deviceType === 'mobile' ? 'large' : 'small'"
/> />
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div> <div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a> </a>
</ng-container> }
}
</nav> </nav>

@ -2,7 +2,6 @@ import { TokenStorageService } from '@ghostfolio/client/services/token-storage.s
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { GfLogoComponent } from '@ghostfolio/ui/logo'; import { GfLogoComponent } from '@ghostfolio/ui/logo';
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@ -12,12 +11,7 @@ import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
imports: [ imports: [GfLogoComponent, MatButtonModule, MatProgressSpinnerModule],
CommonModule,
GfLogoComponent,
MatButtonModule,
MatProgressSpinnerModule
],
selector: 'gf-webauthn-page', selector: 'gf-webauthn-page',
standalone: true, standalone: true,
styleUrls: ['./webauthn-page.scss'], styleUrls: ['./webauthn-page.scss'],

@ -8,22 +8,23 @@
[disablePagination]="true" [disablePagination]="true"
[tabPanel]="tabPanel" [tabPanel]="tabPanel"
> >
<ng-container *ngFor="let tab of tabs"> @for (tab of tabs; track tab) {
<a @if (tab.showCondition !== false) {
#rla="routerLinkActive" <a
*ngIf="tab.showCondition !== false" #rla="routerLinkActive"
class="no-min-width px-3" class="no-min-width px-3"
mat-tab-link mat-tab-link
routerLinkActive routerLinkActive
[active]="rla.isActive" [active]="rla.isActive"
[routerLink]="tab.path" [routerLink]="tab.path"
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
> >
<ion-icon <ion-icon
[name]="tab.iconName" [name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'" [size]="deviceType === 'mobile' ? 'large' : 'small'"
/> />
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div> <div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a> </a>
</ng-container> }
}
</nav> </nav>

Loading…
Cancel
Save