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,15 +1,10 @@
<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="info-message-inner-container position-fixed w-100">
<div class="align-items-center d-flex h-100 justify-content-center"> <div class="align-items-center d-flex h-100 justify-content-center">
<a @if (canCreateAccount) {
*ngIf="canCreateAccount" <a class="text-center" [routerLink]="routerLinkRegister">
class="text-center"
[routerLink]="routerLinkRegister"
>
<div <div
class="cursor-pointer d-inline-block info-message" class="cursor-pointer d-inline-block info-message"
(click)="onCreateAccount()" (click)="onCreateAccount()"
@ -18,16 +13,19 @@
<span class="a ml-2" i18n>Create Account</span> <span class="a ml-2" i18n>Create Account</span>
</div></a </div></a
> >
}
@if (!canCreateAccount && user?.systemMessage) {
<div <div
*ngIf="!canCreateAccount && user?.systemMessage"
class="cursor-pointer d-inline-block info-message text-truncate" class="cursor-pointer d-inline-block info-message text-truncate"
(click)="onClickSystemMessage()" (click)="onClickSystemMessage()"
> >
{{ user.systemMessage.message }} {{ user.systemMessage.message }}
</div> </div>
}
</div> </div>
</div> </div>
</div> </div>
}
<gf-header <gf-header
class="position-fixed w-100" class="position-fixed w-100"
@ -45,7 +43,8 @@
<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) {
<footer class="d-flex justify-content-center py-4 w-100">
<div class="container"> <div class="container">
<div class="mb-3 row"> <div class="mb-3 row">
<div class="col-sm"> <div class="col-sm">
@ -54,9 +53,11 @@
<div class="col-sm"> <div class="col-sm">
<div class="h6 mt-2" i18n>Personal Finance</div> <div class="h6 mt-2" i18n>Personal Finance</div>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li *ngIf="hasPermissionToAccessFearAndGreedIndex"> @if (hasPermissionToAccessFearAndGreedIndex) {
<li>
<a i18n [routerLink]="routerLinkMarkets">Markets</a> <a i18n [routerLink]="routerLinkMarkets">Markets</a>
</li> </li>
}
<li><a i18n [routerLink]="routerLinkResources">Resources</a></li> <li><a i18n [routerLink]="routerLinkResources">Resources</a></li>
</ul> </ul>
</div> </div>
@ -64,33 +65,44 @@
<div class="h6 mt-2">Ghostfolio</div> <div class="h6 mt-2">Ghostfolio</div>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li><a i18n [routerLink]="routerLinkAbout">About</a></li> <li><a i18n [routerLink]="routerLinkAbout">About</a></li>
<li *ngIf="hasPermissionForSubscription"> @if (hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="['/blog']">Blog</a>
</li> </li>
}
<li> <li>
<a i18n [routerLink]="routerLinkAboutChangelog">Changelog</a> <a i18n [routerLink]="routerLinkAboutChangelog">Changelog</a>
</li> </li>
<li><a i18n [routerLink]="routerLinkFeatures">Features</a></li> <li><a i18n [routerLink]="routerLinkFeatures">Features</a></li>
<li *ngIf="hasPermissionForSubscription"> @if (hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="routerLinkFaq" <a i18n [routerLink]="routerLinkFaq"
>Frequently Asked Questions (FAQ)</a >Frequently Asked Questions (FAQ)</a
> >
</li> </li>
}
<li> <li>
<a i18n [routerLink]="routerLinkAboutLicense">License</a> <a i18n [routerLink]="routerLinkAboutLicense">License</a>
</li> </li>
<li *ngIf="hasPermissionForStatistics"> @if (hasPermissionForStatistics) {
<li>
<a [routerLink]="['/open']">Open Startup</a> <a [routerLink]="['/open']">Open Startup</a>
</li> </li>
<li *ngIf="hasPermissionForSubscription"> }
@if (hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="routerLinkPricing">Pricing</a> <a i18n [routerLink]="routerLinkPricing">Pricing</a>
</li> </li>
<li *ngIf="hasPermissionForSubscription"> }
@if (hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="routerLinkAboutPrivacyPolicy" <a i18n [routerLink]="routerLinkAboutPrivacyPolicy"
>Privacy Policy</a >Privacy Policy</a
> >
</li> </li>
<li *ngIf="hasPermissionForSubscription"> }
@if (hasPermissionForSubscription) {
<li>
<a <a
class="align-items-baseline d-flex" class="align-items-baseline d-flex"
href="https://status.ghostfol.io" href="https://status.ghostfol.io"
@ -99,6 +111,7 @@
>Status<ion-icon class="ml-1" name="open-outline" >Status<ion-icon class="ml-1" name="open-outline"
/></a> /></a>
</li> </li>
}
</ul> </ul>
</div> </div>
<div class="col-sm"> <div class="col-sm">
@ -169,13 +182,12 @@
</ul> </ul>
</div> </div>
</div> </div>
<div class="row text-center"> <div class="row text-center">
<div class="col"> <div class="col">
© 2021 - {{ currentYear }} <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="row text-center text-muted">
<div class="col"> <div class="col">
<small i18n <small i18n
@ -186,3 +198,4 @@
</div> </div>
</div> </div>
</footer> </footer>
}

@ -32,10 +32,8 @@
<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" /> <ion-icon class="mr-1" name="link-outline" />
<a <a
href="{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}" href="{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}"
@ -43,6 +41,7 @@
>{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}</a >{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}</a
> >
</div> </div>
}
</td> </td>
</ng-container> </ng-container>

@ -1,4 +1,5 @@
<div *ngIf="showActions" class="d-flex justify-content-end"> @if (showActions) {
<div class="d-flex justify-content-end">
<button <button
class="align-items-center d-flex" class="align-items-center d-flex"
mat-stroked-button mat-stroked-button
@ -9,6 +10,7 @@
<ng-container i18n>Transfer Cash Balance</ng-container>... <ng-container i18n>Transfer Cash Balance</ng-container>...
</button> </button>
</div> </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>
@if (element.Platform?.url) {
<gf-asset-profile-icon <gf-asset-profile-icon
*ngIf="element.Platform?.url"
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">
@if (element.Platform?.url) {
<gf-asset-profile-icon <gf-asset-profile-icon
*ngIf="element.Platform?.url"
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,8 +242,8 @@
class="d-none d-lg-table-cell px-1" class="d-none d-lg-table-cell px-1"
mat-cell mat-cell
> >
@if (element.comment) {
<button <button
*ngIf="element.comment"
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
mat-button mat-button
title="Note" title="Note"
@ -245,6 +251,7 @@
> >
<ion-icon name="document-text-outline" /> <ion-icon name="document-text-outline" />
</button> </button>
}
</td> </td>
<td <td
*matFooterCellDef *matFooterCellDef
@ -303,8 +310,8 @@
</table> </table>
</div> </div>
@if (isLoading) {
<ngx-skeleton-loader <ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse" animation="pulse"
class="px-4 py-3" class="px-4 py-3"
[theme]="{ [theme]="{
@ -312,3 +319,4 @@
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>
@if (element.state === 'active') {
<ion-icon class="h6 mb-0" name="play-outline" />
} @else if (element.state === 'completed') {
<ion-icon <ion-icon
*ngIf="element.state === 'active'"
class="h6 mb-0"
name="play-outline"
/>
<ion-icon
*ngIf="element.state === 'completed'"
class="h6 mb-0 text-success" class="h6 mb-0 text-success"
name="checkmark-circle-outline" name="checkmark-circle-outline"
/> />
} @else if (element.state === 'delayed') {
<ion-icon <ion-icon
*ngIf="element.state === 'delayed'"
class="h6 mb-0" class="h6 mb-0"
name="time-outline" name="time-outline"
[ngClass]="{ 'text-danger': element.stacktrace?.length > 0 }" [ngClass]="{ 'text-danger': element.stacktrace?.length > 0 }"
/> />
} @else if (element.state === 'failed') {
<ion-icon <ion-icon
*ngIf="element.state === 'failed'"
class="h6 mb-0 text-danger" class="h6 mb-0 text-danger"
name="alert-circle-outline" name="alert-circle-outline"
/> />
<ion-icon } @else if (element.state === 'paused') {
*ngIf="element.state === 'paused'" <ion-icon class="h6 mb-0" name="pause-outline" />
class="h6 mb-0" } @else if (element.state === 'waiting') {
name="pause-outline" <ion-icon class="h6 mb-0" name="cafe-outline" />
/> }
<ion-icon
*ngIf="element.state === 'waiting'"
class="h6 mb-0"
name="cafe-outline"
/>
</td> </td>
</ng-container> </ng-container>

@ -9,11 +9,12 @@
[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="d-flex">
<div class="date px-1 text-nowrap">{{ itemByMonth.key }}</div> <div class="date px-1 text-nowrap">{{ itemByMonth.key }}</div>
<div class="align-items-center d-flex flex-grow-1 px-1"> <div class="align-items-center d-flex flex-grow-1 px-1">
@for (dayItem of days; track dayItem; let i = $index) {
<div <div
*ngFor="let dayItem of days; let i = index"
class="day" class="day"
[ngClass]="{ [ngClass]="{
'cursor-pointer valid': isDateOfInterest( 'cursor-pointer valid': isDateOfInterest(
@ -38,6 +39,8 @@
}) })
" "
></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>
<small class="text-muted">{{
element.symbol | gfSymbol
}}</small>
</div> </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,8 +224,8 @@
(page)="onChangePage($event)" (page)="onChangePage($event)"
/> />
@if (isLoading && totalItems === 0) {
<ngx-skeleton-loader <ngx-skeleton-loader
*ngIf="isLoading && totalItems === 0"
animation="pulse" animation="pulse"
class="px-4 py-3" class="px-4 py-3"
[theme]="{ [theme]="{
@ -231,6 +233,7 @@
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,7 +20,8 @@
</mat-radio-group> </mat-radio-group>
</div> </div>
<div *ngIf="mode === 'auto'"> @if (mode === 'auto') {
<div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name, symbol or ISIN</mat-label> <mat-label i18n>Name, symbol or ISIN</mat-label>
<gf-symbol-autocomplete <gf-symbol-autocomplete
@ -29,12 +30,14 @@
/> />
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngIf="mode === 'manual'"> } @else if (mode === 'manual') {
<div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Symbol</mat-label> <mat-label i18n>Symbol</mat-label>
<input formControlName="addSymbol" matInput /> <input formControlName="addSymbol" matInput />
</mat-form-field> </mat-form-field>
</div> </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,17 +27,20 @@
[precision]="0" [precision]="0"
[value]="transactionCount" [value]="transactionCount"
/> />
<div *ngIf="transactionCount && userCount"> @if (transactionCount && userCount) {
<div>
{{ transactionCount / userCount | number: '1.2-2' }} {{ transactionCount / userCount | number: '1.2-2' }}
<span i18n>per User</span> <span i18n>per User</span>
</div> </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) {
<tr>
<td> <td>
<gf-value [locale]="user?.settings?.locale" [value]="1" /> <gf-value [locale]="user?.settings?.locale" [value]="1" />
</td> </td>
@ -80,8 +83,8 @@
<span i18n>Edit</span> <span i18n>Edit</span>
</span> </span>
</a> </a>
@if (customCurrencies.includes(exchangeRate.label2)) {
<button <button
*ngIf="customCurrencies.includes(exchangeRate.label2)"
mat-menu-item mat-menu-item
(click)="onDeleteCurrency(exchangeRate.label2)" (click)="onDeleteCurrency(exchangeRate.label2)"
> >
@ -90,9 +93,11 @@
<span i18n>Delete</span> <span i18n>Delete</span>
</span> </span>
</button> </button>
}
</mat-menu> </mat-menu>
</td> </td>
</tr> </tr>
}
</table> </table>
<div class="mt-2"> <div class="mt-2">
<button <button
@ -119,7 +124,8 @@
/> />
</div> </div>
</div> </div>
<div *ngIf="hasPermissionToToggleReadOnlyMode" class="d-flex my-3"> @if (hasPermissionToToggleReadOnlyMode) {
<div class="d-flex my-3">
<div class="w-50" i18n>Read-only Mode</div> <div class="w-50" i18n>Read-only Mode</div>
<div class="w-50"> <div class="w-50">
<mat-slide-toggle <mat-slide-toggle
@ -130,6 +136,7 @@
/> />
</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,10 +148,12 @@
/> />
</div> </div>
</div> </div>
<div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3"> @if (hasPermissionForSystemMessage) {
<div class="d-flex my-3">
<div class="w-50" i18n>System Message</div> <div class="w-50" i18n>System Message</div>
<div class="w-50"> <div class="w-50">
<div *ngIf="systemMessage" class="align-items-center d-flex"> @if (systemMessage) {
<div class="align-items-center d-flex">
<div class="text-truncate">{{ systemMessage | json }}</div> <div class="text-truncate">{{ systemMessage | json }}</div>
<button <button
class="h-100 mx-1 no-min-width px-2" class="h-100 mx-1 no-min-width px-2"
@ -154,8 +163,9 @@
<ion-icon name="trash-outline" /> <ion-icon name="trash-outline" />
</button> </button>
</div> </div>
}
@if (!info?.systemMessage) {
<button <button
*ngIf="!info?.systemMessage"
class="mt-2" class="mt-2"
color="accent" color="accent"
mat-flat-button mat-flat-button
@ -164,16 +174,17 @@
<ion-icon class="mr-1" name="information-circle-outline" /> <ion-icon class="mr-1" name="information-circle-outline" />
<span i18n>Set Message</span> <span i18n>Set Message</span>
</button> </button>
}
</div> </div>
</div> </div>
<div }
*ngIf="hasPermissionForSubscription" @if (hasPermissionForSubscription) {
class="d-flex my-3 subscription" <div class="d-flex my-3 subscription">
>
<div class="w-50" i18n>Coupons</div> <div class="w-50" i18n>Coupons</div>
<div class="w-50"> <div class="w-50">
<table> <table>
<tr *ngFor="let coupon of coupons"> @for (coupon of coupons; track coupon) {
<tr>
<td class="text-monospace">{{ coupon.code }}</td> <td class="text-monospace">{{ coupon.code }}</td>
<td class="pl-2 text-right">{{ coupon.duration }}</td> <td class="pl-2 text-right">{{ coupon.duration }}</td>
<td> <td>
@ -202,6 +213,7 @@
</mat-menu> </mat-menu>
</td> </td>
</tr> </tr>
}
</table> </table>
<div class="mt-2"> <div class="mt-2">
<form #couponForm="ngForm" class="align-items-center d-flex"> <form #couponForm="ngForm" class="align-items-center d-flex">
@ -234,6 +246,7 @@
</div> </div>
</div> </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>
@if (element.url) {
<gf-asset-profile-icon <gf-asset-profile-icon
*ngIf="element.url"
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,26 +49,26 @@
}" }"
>{{ (element.id | slice: 0 : 5) + '...' }}</span >{{ (element.id | slice: 0 : 5) + '...' }}</span
> >
@if (element?.subscription?.type === 'Premium') {
<gf-premium-indicator <gf-premium-indicator
*ngIf="element?.subscription?.type === 'Premium'"
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 <th
*matHeaderCellDef *matHeaderCellDef
class="mat-mdc-header-cell px-1 py-2" class="mat-mdc-header-cell px-1 py-2"
@ -86,6 +86,7 @@
}}</span> }}</span>
</td> </td>
</ng-container> </ng-container>
}
<ng-container matColumnDef="registration"> <ng-container matColumnDef="registration">
<th <th
@ -146,10 +147,8 @@
</td> </td>
</ng-container> </ng-container>
<ng-container @if (hasPermissionForSubscription) {
*ngIf="hasPermissionForSubscription" <ng-container matColumnDef="engagementPerDay">
matColumnDef="engagementPerDay"
>
<th <th
*matHeaderCellDef *matHeaderCellDef
class="mat-mdc-header-cell px-1 py-2 text-right" class="mat-mdc-header-cell px-1 py-2 text-right"
@ -170,11 +169,10 @@
/> />
</td> </td>
</ng-container> </ng-container>
}
<ng-container @if (hasPermissionForSubscription) {
*ngIf="hasPermissionForSubscription" <ng-container matColumnDef="lastRequest">
matColumnDef="lastRequest"
>
<th <th
*matHeaderCellDef *matHeaderCellDef
class="mat-mdc-header-cell px-1 py-2" class="mat-mdc-header-cell px-1 py-2"
@ -191,6 +189,7 @@
{{ formatDistanceToNow(element.lastActivity) }} {{ formatDistanceToNow(element.lastActivity) }}
</td> </td>
</ng-container> </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
(click)="onImpersonateUser(element.id)"
>
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon class="mr-2" name="contract-outline" /> <ion-icon class="mr-2" name="contract-outline" />
<span i18n>Impersonate User</span> <span i18n>Impersonate User</span>
</span> </span>
</button> </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"> <div class="align-items-center d-flex">
<ion-icon class="mr-2 text-muted" name="arrow-forward-outline" /> <ion-icon class="mr-2 text-muted" name="arrow-forward-outline" />
<span i18n>Manage Benchmarks</span> <span i18n>Manage Benchmarks</span>
</div> </div>
</mat-option> </mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div class="chart-container"> <div class="chart-container">
@if (isLoading) {
<ngx-skeleton-loader <ngx-skeleton-loader
*ngIf="isLoading"
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
(click)="onClickCloseButton()"
>
<ion-icon name="close" size="large" /> <ion-icon name="close" size="large" />
</button> </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"
mat-button
(click)="onClickCloseButton()"
>
<ion-icon name="close" size="large" /> <ion-icon name="close" size="large" />
</button> </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>
@if (!fearAndGreedIndex) {
<ngx-skeleton-loader <ngx-skeleton-loader
*ngIf="!fearAndGreedIndex"
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,7 +54,8 @@
>Accounts</a >Accounts</a
> >
</li> </li>
<li *ngIf="hasPermissionToAccessAdminControl" class="list-inline-item"> @if (hasPermissionToAccessAdminControl) {
<li class="list-inline-item">
<a <a
class="d-none d-sm-block" class="d-none d-sm-block"
i18n i18n
@ -67,6 +68,7 @@
>Admin Control</a >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,12 +82,10 @@
>Resources</a >Resources</a
> >
</li> </li>
<li @if (
*ngIf="
hasPermissionForSubscription && user?.subscription?.type === 'Basic' hasPermissionForSubscription && user?.subscription?.type === 'Basic'
" ) {
class="list-inline-item" <li class="list-inline-item">
>
<a <a
class="d-none d-sm-block" class="d-none d-sm-block"
i18n i18n
@ -98,6 +98,7 @@
>Pricing</a >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,7 +112,8 @@
>About</a >About</a
> >
</li> </li>
<li *ngIf="hasPermissionToAccessAssistant" class="list-inline-item"> @if (hasPermissionToAccessAssistant) {
<li class="list-inline-item">
<button <button
#assistantTrigger="matMenuTrigger" #assistantTrigger="matMenuTrigger"
class="h-100 no-min-width px-2" class="h-100 no-min-width px-2"
@ -147,6 +149,7 @@
/> />
</mat-menu> </mat-menu>
</li> </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
>
<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 }
></span </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,11 +205,8 @@
<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
(click)="impersonateAccount(accessItem.id)"
>
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon <ion-icon
class="mr-2" class="mr-2"
@ -226,12 +217,16 @@
: 'radio-button-off-outline' : 'radio-button-off-outline'
" "
/> />
<span *ngIf="accessItem.alias">{{ accessItem.alias }}</span> @if (accessItem.alias) {
<span *ngIf="!accessItem.alias" i18n>User</span> <span>{{ accessItem.alias }}</span>
} @else {
<span 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,8 +263,8 @@
[routerLink]="['/account']" [routerLink]="['/account']"
>My Ghostfolio</a >My Ghostfolio</a
> >
@if (hasPermissionToAccessAdminControl) {
<a <a
*ngIf="hasPermissionToAccessAdminControl"
class="d-flex d-sm-none" class="d-flex d-sm-none"
i18n i18n
mat-menu-item mat-menu-item
@ -277,6 +272,7 @@
[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,11 +284,10 @@
[routerLink]="routerLinkResources" [routerLink]="routerLinkResources"
>Resources</a >Resources</a
> >
@if (
hasPermissionForSubscription && user?.subscription?.type === 'Basic'
) {
<a <a
*ngIf="
hasPermissionForSubscription &&
user?.subscription?.type === 'Basic'
"
class="d-flex d-sm-none" class="d-flex d-sm-none"
i18n i18n
mat-menu-item mat-menu-item
@ -300,6 +295,7 @@
[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,7 +353,8 @@
>About</a >About</a
> >
</li> </li>
<li *ngIf="hasPermissionForSubscription" class="list-inline-item"> @if (hasPermissionForSubscription) {
<li class="list-inline-item">
<a <a
class="d-sm-block" class="d-sm-block"
i18n i18n
@ -370,10 +367,9 @@
>Pricing</a >Pricing</a
> >
</li> </li>
<li }
*ngIf="hasPermissionToAccessFearAndGreedIndex" @if (hasPermissionToAccessFearAndGreedIndex) {
class="list-inline-item" <li class="list-inline-item">
>
<a <a
class="d-none d-sm-block" class="d-none d-sm-block"
i18n i18n
@ -386,6 +382,7 @@
>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,10 +396,8 @@
<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 <a
class="d-none d-sm-block" class="d-none d-sm-block"
color="primary" color="primary"
@ -411,6 +406,7 @@
><ng-container i18n>Get started</ng-container> ><ng-container i18n>Get started</ng-container>
</a> </a>
</li> </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,6 +1,7 @@
<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="mb-5 row">
<div class="col-xs-12 col-md-8 offset-md-2"> <div class="col-xs-12 col-md-8 offset-md-2">
<div class="mb-2 text-center text-muted"> <div class="mb-2 text-center text-muted">
<small i18n>Last {{ numberOfDays }} Days</small> <small i18n>Last {{ numberOfDays }} Days</small>
@ -25,6 +26,7 @@
/> />
</div> </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,8 +35,8 @@
[locale]="user?.settings?.locale || undefined" [locale]="user?.settings?.locale || undefined"
[user]="user" [user]="user"
/> />
@if (isLoading) {
<ngx-skeleton-loader <ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse" animation="pulse"
class="px-2 py-3" class="px-2 py-3"
[theme]="{ [theme]="{
@ -42,6 +44,7 @@
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"
mat-flat-button
[routerLink]="['/accounts']"
>
<ng-container i18n>Setup accounts</ng-container> <ng-container i18n>Setup accounts</ng-container>
</a> </a>
} @else if (user?.accounts?.length > 1) {
<a <a
*ngIf="user?.accounts?.length > 1"
color="primary" color="primary"
mat-flat-button mat-flat-button
[routerLink]="['/portfolio', 'activities']" [routerLink]="['/portfolio', 'activities']"
> >
<ng-container i18n>Add activity</ng-container> <ng-container i18n>Add activity</ng-container>
</a> </a>
}
</div> </div>
</div> </div>
</div> </div>

@ -1,11 +1,12 @@
@if (isLoading) {
<ngx-skeleton-loader <ngx-skeleton-loader
*ngIf="isLoading"
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,7 +10,8 @@
/> />
} }
</div> </div>
<div *ngIf="isLoading" class="align-items-center d-flex"> @if (isLoading) {
<div class="align-items-center d-flex">
<ngx-skeleton-loader <ngx-skeleton-loader
animation="pulse" animation="pulse"
class="mb-2" class="mb-2"
@ -20,6 +21,7 @@
}" }"
/> />
</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,14 +36,17 @@
{{ unit }} {{ unit }}
</div> </div>
</div> </div>
<div *ngIf="showDetails" class="row"> @if (showDetails) {
<div class="row">
<div class="d-flex col justify-content-end"> <div class="d-flex col justify-content-end">
<gf-value <gf-value
[colorizeSign]="true" [colorizeSign]="true"
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]=" [value]="
isLoading ? undefined : performance?.netPerformanceWithCurrencyEffect isLoading
? undefined
: performance?.netPerformanceWithCurrencyEffect
" "
/> />
</div> </div>
@ -58,4 +63,5 @@
/> />
</div> </div>
</div> </div>
}
</div> </div>

@ -1,6 +1,7 @@
<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) {
<div>
<ngx-skeleton-loader <ngx-skeleton-loader
animation="pulse" animation="pulse"
class="mr-2" class="mr-2"
@ -10,15 +11,20 @@
}" }"
/> />
</div> </div>
} @else {
<div <div
*ngIf="!isLoading"
class="align-items-center d-flex icon-container mr-2 px-2" class="align-items-center d-flex icon-container mr-2 px-2"
[ngClass]="{ okay: rule?.value === true, warn: rule?.value === false }" [ngClass]="{ okay: rule?.value === true, warn: rule?.value === false }"
> >
<ion-icon *ngIf="rule?.value === true" name="checkmark-circle-outline" /> @if (rule?.value === true) {
<ion-icon *ngIf="rule?.value === false" name="warning-outline" /> <ion-icon name="checkmark-circle-outline" />
} @else {
<ion-icon name="warning-outline" />
}
</div> </div>
<div *ngIf="isLoading" class="flex-grow-1"> }
@if (isLoading) {
<div class="flex-grow-1">
<ngx-skeleton-loader <ngx-skeleton-loader
animation="pulse" animation="pulse"
class="mt-1 mb-1" class="mt-1 mb-1"
@ -35,9 +41,11 @@
}" }"
/> />
</div> </div>
<div *ngIf="!isLoading" class="flex-grow-1"> } @else {
<div class="flex-grow-1">
<div class="h6 my-1">{{ rule?.name }}</div> <div class="h6 my-1">{{ rule?.name }}</div>
<div class="evaluation">{{ rule?.evaluation }}</div> <div class="evaluation">{{ rule?.evaluation }}</div>
</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"
class="my-2 text-center"
>
<mat-card-content> <mat-card-content>
<gf-no-transactions-info-indicator [hasBorder]="false" /> <gf-no-transactions-info-indicator [hasBorder]="false" />
</mat-card-content> </mat-card-content>
</mat-card> </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()"
> >
@for (option of options; track option) {
<mat-radio-button <mat-radio-button
*ngFor="let option of options"
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,17 +3,17 @@
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) {
<div class="fab-container">
<a <a
class="align-items-center d-flex justify-content-center" class="align-items-center d-flex justify-content-center"
color="primary" color="primary"
@ -24,4 +24,5 @@
<ion-icon name="add-outline" size="large" /> <ion-icon name="add-outline" size="large" />
</a> </a>
</div> </div>
}
</div> </div>

@ -6,55 +6,46 @@
[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()">
<ng-container *ngIf="user.subscription.offer === 'default'" i18n @if (user.subscription.offer === 'default') {
>Upgrade Plan</ng-container <ng-container i18n>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>
<div *ngIf="price" class="mt-1 text-center"> @if (price) {
<ng-container *ngIf="coupon" <div class="mt-1 text-center">
><del class="text-muted" @if (coupon) {
<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
>&nbsp;<span i18n>per year</span>
</div> </div>
</ng-container> }
}
<div class="align-items-center d-flex justify-content-center mt-4"> <div class="align-items-center d-flex justify-content-center mt-4">
<a @if (!user?.subscription?.expiresAt) {
*ngIf="!user?.subscription?.expiresAt" <a class="mx-1" mat-stroked-button [href]="trySubscriptionMail"
class="mx-1"
mat-stroked-button
[href]="trySubscriptionMail"
><span i18n>Try Premium</span> ><span i18n>Try Premium</span>
<gf-premium-indicator <gf-premium-indicator
class="d-inline-block ml-1" class="d-inline-block ml-1"
[enableLink]="false" [enableLink]="false"
/> />
</a> </a>
}
@if (hasPermissionToUpdateUserSettings) {
<a <a
*ngIf="hasPermissionToUpdateUserSettings"
class="mx-1" class="mx-1"
i18n i18n
mat-stroked-button mat-stroked-button
@ -62,8 +53,10 @@
(click)="onRedeemCoupon()" (click)="onRedeemCoupon()"
>Redeem Coupon</a >Redeem Coupon</a
> >
}
</div> </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,11 +46,8 @@
<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"
i18n
>
If a translation is missing, kindly support us in extending it If a translation is missing, kindly support us in extending it
<a <a
href="https://github.com/ghostfolio/ghostfolio/blob/main/apps/client/src/locales/messages.{{ href="https://github.com/ghostfolio/ghostfolio/blob/main/apps/client/src/locales/messages.{{
@ -62,6 +57,7 @@
>here</a >here</a
>. >.
</div> </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,10 +194,8 @@
/> />
</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 class="pr-1 w-50">
<div i18n>Experimental Features</div> <div i18n>Experimental Features</div>
<div class="hint-text text-muted" i18n> <div class="hint-text text-muted" i18n>
@ -218,6 +212,7 @@
/> />
</div> </div>
</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 @@
@if (isLoading) {
<ngx-skeleton-loader <ngx-skeleton-loader
*ngIf="isLoading"
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,10 +8,10 @@
[disablePagination]="true" [disablePagination]="true"
[tabPanel]="tabPanel" [tabPanel]="tabPanel"
> >
<ng-container *ngFor="let tab of tabs"> @for (tab of tabs; track tab) {
@if (tab.showCondition !== false) {
<a <a
#rla="routerLinkActive" #rla="routerLinkActive"
*ngIf="tab.showCondition !== false"
class="no-min-width px-3" class="no-min-width px-3"
mat-tab-link mat-tab-link
routerLinkActive routerLinkActive
@ -25,5 +25,6 @@
/> />
<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,8 +81,8 @@
> >
<ion-icon name="logo-x" /> <ion-icon name="logo-x" />
</a> </a>
@if (user?.subscription?.type === 'Premium') {
<a <a
*ngIf="user?.subscription?.type === 'Premium'"
class="mx-2" class="mx-2"
href="mailto:hi@ghostfol.io" href="mailto:hi@ghostfol.io"
mat-icon-button mat-icon-button
@ -88,6 +90,7 @@
> >
<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,19 +108,15 @@
<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 <div
class="independent-and-bootstrapped-logo mb-2" class="independent-and-bootstrapped-logo mb-2"
title="Ghostfolio is an independent & bootstrapped business" title="Ghostfolio is an independent & bootstrapped business"
></div> ></div>
</div> </div>
<div } @else {
*ngIf="!hasPermissionForSubscription" <div class="d-flex justify-content-center">
class="d-flex justify-content-center"
>
<a <a
href="https://www.buymeacoffee.com/ghostfolio" href="https://www.buymeacoffee.com/ghostfolio"
target="_blank" target="_blank"
@ -128,6 +127,7 @@
width="180" width="180"
/></a> /></a>
</div> </div>
}
</div> </div>
</div> </div>
</div> </div>

@ -22,14 +22,12 @@
</div> </div>
</div> </div>
<div @if (
*ngIf="
!hasImpersonationId && !hasImpersonationId &&
hasPermissionToCreateAccount && hasPermissionToCreateAccount &&
!user.settings.isRestrictedView !user.settings.isRestrictedView
" ) {
class="fab-container" <div class="fab-container">
>
<a <a
class="align-items-center d-flex justify-content-center" class="align-items-center d-flex justify-content-center"
color="primary" color="primary"
@ -40,4 +38,5 @@
<ion-icon name="add-outline" size="large" /> <ion-icon name="add-outline" size="large" />
</a> </a>
</div> </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) {
<mat-option [value]="account.id">
<div class="d-flex"> <div class="d-flex">
@if (account.Platform?.url) {
<gf-asset-profile-icon <gf-asset-profile-icon
*ngIf="account.Platform?.url"
class="mr-1" class="mr-1"
[tooltip]="account.Platform?.name" [tooltip]="account.Platform?.name"
[url]="account.Platform?.url" [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>
@ -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) {
<mat-option [value]="account.id">
<div class="d-flex"> <div class="d-flex">
@if (account.Platform?.url) {
<gf-asset-profile-icon <gf-asset-profile-icon
*ngIf="account.Platform?.url"
class="mr-1" class="mr-1"
[tooltip]="account.Platform?.name" [tooltip]="account.Platform?.name"
[url]="account.Platform?.url" [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>

@ -8,10 +8,10 @@
[disablePagination]="true" [disablePagination]="true"
[tabPanel]="tabPanel" [tabPanel]="tabPanel"
> >
<ng-container *ngFor="let tab of tabs"> @for (tab of tabs; track tab) {
@if (tab.showCondition !== false) {
<a <a
#rla="routerLinkActive" #rla="routerLinkActive"
*ngIf="tab.showCondition !== false"
class="no-min-width px-3" class="no-min-width px-3"
mat-tab-link mat-tab-link
routerLinkActive routerLinkActive
@ -25,5 +25,6 @@
/> />
<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,10 +8,10 @@
[disablePagination]="true" [disablePagination]="true"
[tabPanel]="tabPanel" [tabPanel]="tabPanel"
> >
<ng-container *ngFor="let tab of tabs"> @for (tab of tabs; track tab) {
@if (tab.showCondition !== false) {
<a <a
#rla="routerLinkActive" #rla="routerLinkActive"
*ngIf="tab.showCondition !== false"
class="no-min-width px-3" class="no-min-width px-3"
mat-tab-link mat-tab-link
routerLinkActive routerLinkActive
@ -25,5 +25,6 @@
/> />
<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,11 +98,8 @@
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"
class="mb-3"
>
<mat-card-header> <mat-card-header>
<mat-card-title <mat-card-title
>I cannot find my broker in the list of platforms. What can I >I cannot find my broker in the list of platforms. What can I
@ -115,6 +112,7 @@
happy to add it. happy to add it.
</mat-card-content> </mat-card-content>
</mat-card> </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,10 +8,10 @@
[disablePagination]="true" [disablePagination]="true"
[tabPanel]="tabPanel" [tabPanel]="tabPanel"
> >
<ng-container *ngFor="let tab of tabs"> @for (tab of tabs; track tab) {
@if (tab.showCondition !== false) {
<a <a
#rla="routerLinkActive" #rla="routerLinkActive"
*ngIf="tab.showCondition !== false"
class="no-min-width px-3" class="no-min-width px-3"
mat-tab-link mat-tab-link
routerLinkActive routerLinkActive
@ -25,5 +25,6 @@
/> />
<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,17 +41,18 @@
> >
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 class="row mb-5">
<div <div
class="col-md-4 d-flex my-1" class="col-md-4 d-flex my-1"
[ngClass]="{ 'justify-content-center': deviceType !== 'mobile' }" [ngClass]="{ 'justify-content-center': deviceType !== 'mobile' }"
@ -107,6 +108,7 @@
</a> </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,31 +322,38 @@
</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 gf-carousel-item>
<div class="d-flex px-4"> <div class="d-flex px-4">
<gf-logo class="mr-3 mt-2 pt-1" size="medium" [showLabel]="false" /> <gf-logo
class="mr-3 mt-2 pt-1"
size="medium"
[showLabel]="false"
/>
<div> <div>
<div>{{ testimonial.quote }}</div> <div>{{ testimonial.quote }}</div>
<div class="mt-2 text-muted"> <div class="mt-2 text-muted">
<a @if (testimonial.url) {
*ngIf="testimonial.url" <a target="_blank" [href]="testimonial.url">{{
target="_blank" testimonial.author
[href]="testimonial.url" }}</a>
>{{ testimonial.author }}</a } @else {
> <span>{{ testimonial.author }}</span>
<span *ngIf="!testimonial.url">{{ testimonial.author }}</span }
>, ,
{{ testimonial.country }} {{ 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="row my-5">
<div class="col-12"> <div class="col-12">
<h2 class="h4 text-center" i18n> <h2 class="h4 text-center" i18n>
Members from around the globe are using Members from around the globe are using
@ -352,11 +361,16 @@
</h2> </h2>
</div> </div>
<div class="col-md-8 customer-map-container offset-md-2"> <div class="col-md-8 customer-map-container offset-md-2">
<gf-world-map-chart format="👻" [countries]="countriesOfSubscribersMap" /> <gf-world-map-chart
format="👻"
[countries]="countriesOfSubscribersMap"
/>
</div> </div>
</div> </div>
}
<div *ngIf="hasPermissionForSubscription" class="row my-3"> @if (hasPermissionForSubscription) {
<div class="row my-3">
<div class="col-12"> <div class="col-12">
<h2 class="h4 mb-1 text-center" i18n> <h2 class="h4 mb-1 text-center" i18n>
How does <strong>Ghostfolio</strong> work? How does <strong>Ghostfolio</strong> work?
@ -401,14 +415,19 @@
</mat-card> </mat-card>
</div> </div>
</div> </div>
}
<div *ngIf="hasPermissionToCreateUser" class="row my-5"> @if (hasPermissionToCreateUser) {
<div class="row my-5">
<div class="col"> <div class="col">
<h2 class="h4 mb-1 text-center" i18n>Are <strong>you</strong> ready?</h2> <h2 class="h4 mb-1 text-center" i18n>
Are <strong>you</strong> ready?
</h2>
<p class="lead mb-3 text-center" i18n> <p class="lead mb-3 text-center" i18n>
Join now<ng-container *ngIf="hasPermissionForDemo"> Join now
or check out the example account</ng-container @if (hasPermissionForDemo) {
> or check out the example account
}
</p> </p>
<div class="align-items-center d-flex justify-content-center py-2"> <div class="align-items-center d-flex justify-content-center py-2">
<a <a
@ -419,13 +438,14 @@
> >
Get Started Get Started
</a> </a>
<ng-container *ngIf="hasPermissionForDemo"> @if (hasPermissionForDemo) {
<div class="mx-3 text-muted" i18n>or</div> <div class="mx-3 text-muted" i18n>or</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> </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)
" ) {
[value]="null" <mat-option [value]="null" />
/> }
<mat-option @for (account of data.accounts; track account) {
*ngFor="let account of data.accounts" <mat-option [value]="account.id">
[value]="account.id"
>
<div class="d-flex"> <div class="d-flex">
@if (account.Platform?.url) {
<gf-asset-profile-icon <gf-asset-profile-icon
*ngIf="account.Platform?.url"
class="mr-1" class="mr-1"
[tooltip]="account.Platform?.name" [tooltip]="account.Platform?.name"
[url]="account.Platform?.url" [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,18 +219,17 @@
[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 }} {{ currency }}
</mat-option> </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
@ -230,13 +238,14 @@
activityForm.get('date')?.value | date: defaultDateFormat activityForm.get('date')?.value | date: defaultDateFormat
}}</mat-error }}</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"
@ -245,21 +254,32 @@
> >
<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>
@if (activityForm.get('feeInCustomCurrency').hasError('invalid')) {
<mat-error <mat-error
*ngIf="activityForm.get('feeInCustomCurrency').hasError('invalid')"
><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 activityForm.get('date')?.value | date: defaultDateFormat
}}</mat-error }}</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,8 +376,8 @@
<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>
@for (tag of activityForm.get('tags')?.value; track tag) {
<mat-chip-row <mat-chip-row
*ngFor="let tag of activityForm.get('tags')?.value"
matChipRemove matChipRemove
[removable]="true" [removable]="true"
(removed)="onRemoveTag(tag)" (removed)="onRemoveTag(tag)"
@ -363,6 +385,7 @@
{{ 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 }} {{ tag.name }}
</mat-option> </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,8 +34,8 @@
<mat-select-trigger>{{ <mat-select-trigger>{{
uniqueAssetForm.get('uniqueAsset')?.value?.name uniqueAssetForm.get('uniqueAsset')?.value?.name
}}</mat-select-trigger> }}</mat-select-trigger>
@for (holding of holdings; track holding) {
<mat-option <mat-option
*ngFor="let holding of holdings"
class="line-height-1" class="line-height-1"
[value]="{ [value]="{
dataSource: holding.dataSource, dataSource: holding.dataSource,
@ -53,12 +52,11 @@
{{ holding.currency }}</small {{ holding.currency }}</small
> >
</mat-option> </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,17 +109,16 @@
<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) {
@if (importStep === 1) {
<gf-activities-table <gf-activities-table
*ngIf="importStep === 1"
[baseCurrency]="data?.user?.settings?.baseCurrency" [baseCurrency]="data?.user?.settings?.baseCurrency"
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="data?.deviceType" [deviceType]="data?.deviceType"
@ -141,6 +138,7 @@
[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,10 +155,8 @@
</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-expansion-panel-header class="pl-1">
<mat-panel-title> <mat-panel-title>
<div class="d-flex"> <div class="d-flex">
@ -171,11 +167,11 @@
</div> </div>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<pre @if (details[i]) {
*ngIf="details[i]" <pre class="m-0"><code>{{ details[i] | json }}</code></pre>
class="m-0" }
><code>{{ details[i] | json }}</code></pre>
</mat-expansion-panel> </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,7 +168,8 @@
</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) {
<li class="py-1">
<a <a
class="d-flex" class="d-flex"
[queryParams]="{ [queryParams]="{
@ -193,16 +192,18 @@
</div> </div>
</a> </a>
</li> </li>
}
</ol> </ol>
<div> <div>
@if (!top3) {
<ngx-skeleton-loader <ngx-skeleton-loader
*ngIf="!top3"
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,7 +217,8 @@
</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) {
<li class="py-1">
<a <a
class="d-flex" class="d-flex"
[queryParams]="{ [queryParams]="{
@ -239,16 +241,18 @@
</div> </div>
</a> </a>
</li> </li>
}
</ol> </ol>
<div> <div>
@if (!bottom3) {
<ngx-skeleton-loader <ngx-skeleton-loader
*ngIf="!bottom3"
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,7 +307,8 @@
(change)="onChangeGroupBy($event.value)" (change)="onChangeGroupBy($event.value)"
/> />
</div> </div>
<div *ngIf="streaks" class="row"> @if (streaks) {
<div class="row">
<div class="col-md-6 col-xs-12 my-2"> <div class="col-md-6 col-xs-12 my-2">
<gf-value <gf-value
i18n i18n
@ -325,6 +328,7 @@
> >
</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,13 +37,13 @@
</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) {
<div>
<ngx-skeleton-loader <ngx-skeleton-loader
animation="pulse" animation="pulse"
class="my-1" class="my-1"
@ -61,8 +60,8 @@
}" }"
/> />
</div> </div>
} @else {
<div <div
*ngIf="!isLoading"
i18n i18n
[ngClass]="{ 'text-muted': user?.subscription?.type === 'Basic' }" [ngClass]="{ 'text-muted': user?.subscription?.type === 'Basic' }"
> >
@ -99,6 +98,7 @@
</span> </span>
and a withdrawal rate of 4%. and a withdrawal rate of 4%.
</div> </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,10 +8,10 @@
[disablePagination]="true" [disablePagination]="true"
[tabPanel]="tabPanel" [tabPanel]="tabPanel"
> >
<ng-container *ngFor="let tab of tabs"> @for (tab of tabs; track tab) {
@if (tab.showCondition !== false) {
<a <a
#rla="routerLinkActive" #rla="routerLinkActive"
*ngIf="tab.showCondition !== false"
class="no-min-width px-3" class="no-min-width px-3"
mat-tab-link mat-tab-link
routerLinkActive routerLinkActive
@ -25,5 +25,6 @@
/> />
<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,7 +9,8 @@
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') {
<p>
If you plan to open an account at <i>DEGIRO</i>, <i>finpension</i>, If you plan to open an account at <i>DEGIRO</i>, <i>finpension</i>,
<i>frankly</i>, <i>Interactive Brokers</i>, <i>Swissquote</i>, <i>frankly</i>, <i>Interactive Brokers</i>, <i>Swissquote</i>,
<i>VIAC</i>, or <i>Zak</i>, please <i>VIAC</i>, or <i>Zak</i>, please
@ -18,9 +19,10 @@
> >
to use our referral link and get a Ghostfolio Premium membership for to use our referral link and get a Ghostfolio Premium membership for
one year. Looking for a student discount? Request it one year. Looking for a student discount? Request it
<a href="mailto:hi@ghostfol.io?Subject=Student Discount">here</a> with <a href="mailto:hi@ghostfol.io?Subject=Student Discount">here</a>
your university e-mail address. with your university e-mail address.
</p> </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> <a color="primary" mat-flat-button>&nbsp;</a>
<p class="m-0 text-muted"> <p class="m-0 text-muted">
<small>&nbsp;</small> <small>&nbsp;</small>
</p> </p>
</div> </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') {
<div>
<ion-icon class="mr-1" name="checkmark-outline" /> <ion-icon class="mr-1" name="checkmark-outline" />
</div> </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> <a color="primary" mat-flat-button>&nbsp;</a>
<p class="m-0 text-muted"> <p class="m-0 text-muted">
<small>&nbsp;</small> <small>&nbsp;</small>
</p> </p>
</div> </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') {
<div>
<ion-icon class="mr-1" name="checkmark-outline" /> <ion-icon class="mr-1" name="checkmark-outline" />
</div> </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'
" ) {
class="mt-3 text-center" <div class="mt-3 text-center">
<button
color="primary"
mat-flat-button
(click)="onCheckout()"
> >
<button color="primary" mat-flat-button (click)="onCheckout()"> @if (user.subscription.offer === 'default') {
<ng-container <ng-container i18n>Upgrade Plan</ng-container>
*ngIf="user.subscription.offer === 'default'" } @else if (
i18n
>Upgrade Plan</ng-container
>
<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>
<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="row">
<div class="col mt-3 text-center"> <div class="col mt-3 text-center">
<a color="primary" i18n mat-flat-button [routerLink]="routerLinkRegister"> <a
color="primary"
i18n
mat-flat-button
[routerLink]="routerLinkRegister"
>
Get Started Get Started
</a> </a>
<p class="m-0 text-muted"><small i18n>Its free.</small></p> <p class="m-0 text-muted"><small i18n>Its free.</small></p>
</div> </div>
</div> </div>
}
</div> </div>

@ -24,10 +24,13 @@
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
<div *ngIf="portfolioPublicDetails?.hasDetails" class="col-md-4"> @if (portfolioPublicDetails?.hasDetails) {
<div class="col-md-4">
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-header class="overflow-hidden w-100"> <mat-card-header class="overflow-hidden w-100">
<mat-card-title class="text-truncate" i18n>Currencies</mat-card-title> <mat-card-title class="text-truncate" i18n
>Currencies</mat-card-title
>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
@ -39,7 +42,9 @@
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
<div *ngIf="portfolioPublicDetails?.hasDetails" class="col-md-4"> }
@if (portfolioPublicDetails?.hasDetails) {
<div class="col-md-4">
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-header class="overflow-hidden w-100"> <mat-card-header class="overflow-hidden w-100">
<mat-card-title class="text-truncate" i18n>Sectors</mat-card-title> <mat-card-title class="text-truncate" i18n>Sectors</mat-card-title>
@ -54,10 +59,14 @@
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
<div *ngIf="portfolioPublicDetails?.hasDetails" class="col-md-4"> }
@if (portfolioPublicDetails?.hasDetails) {
<div class="col-md-4">
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-header class="overflow-hidden w-100"> <mat-card-header class="overflow-hidden w-100">
<mat-card-title class="text-truncate" i18n>Continents</mat-card-title> <mat-card-title class="text-truncate" i18n
>Continents</mat-card-title
>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
@ -68,8 +77,10 @@
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
}
</div> </div>
<div *ngIf="portfolioPublicDetails?.hasDetails" class="row world-map-chart"> @if (portfolioPublicDetails?.hasDetails) {
<div class="row world-map-chart">
<div class="col-lg"> <div class="col-lg">
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-header class="overflow-hidden w-100"> <mat-card-header class="overflow-hidden w-100">
@ -111,10 +122,8 @@
>Other Markets</gf-value >Other Markets</gf-value
> >
</div> </div>
<div @if (markets?.[UNKNOWN_KEY]?.value > 0) {
*ngIf="markets?.[UNKNOWN_KEY]?.value > 0" <div class="col-xs-12 col-md my-2">
class="col-xs-12 col-md my-2"
>
<gf-value <gf-value
i18n i18n
size="large" size="large"
@ -123,11 +132,13 @@
>No data available</gf-value >No data available</gf-value
> >
</div> </div>
}
</div> </div>
</mat-card-content> </mat-card-content>
</mat-card> </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,7 +14,8 @@
</div> </div>
</div> </div>
<div *ngIf="hasPermissionToCreateUser" class="button-container row"> @if (hasPermissionToCreateUser) {
<div class="button-container row">
<div class="align-items-center col d-flex justify-content-center"> <div class="align-items-center col d-flex justify-content-center">
<div class="py-5 text-center"> <div class="py-5 text-center">
<button <button
@ -25,10 +26,10 @@
> >
<ng-container i18n>Create Account</ng-container> <ng-container i18n>Create Account</ng-container>
</button> </button>
<ng-container *ngIf="hasPermissionForSocialLogin"> @if (hasPermissionForSocialLogin) {
<div class="my-3 text-muted" i18n>or</div> <div class="my-3 text-muted" i18n>or</div>
@if (false) {
<button <button
*ngIf="false"
class="d-block mb-2 px-4 rounded-pill" class="d-block mb-2 px-4 rounded-pill"
mat-stroked-button mat-stroked-button
(click)="onLoginWithInternetIdentity()" (click)="onLoginWithInternetIdentity()"
@ -40,6 +41,7 @@
/> />
<span i18n>Continue with Internet Identity</span> <span i18n>Continue with Internet Identity</span>
</button> </button>
}
<a <a
class="px-4 rounded-pill w-100" class="px-4 rounded-pill w-100"
href="../api/v1/auth/google" href="../api/v1/auth/google"
@ -50,8 +52,9 @@
style="height: 1rem" style="height: 1rem"
/><span i18n>Continue with Google</span></a /><span i18n>Continue with Google</span></a
> >
</ng-container> }
</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) {
<tr class="mat-mdc-row">
<td class="mat-mdc-cell px-3 py-2 text-right" i18n>Notes</td> <td class="mat-mdc-cell px-3 py-2 text-right" i18n>Notes</td>
<td class="mat-mdc-cell px-1 py-2">{{ product1.note }}</td> <td class="mat-mdc-cell px-1 py-2">{{ product1.note }}</td>
<td class="mat-mdc-cell px-1 py-2">{{ product2.note }}</td> <td class="mat-mdc-cell px-1 py-2">{{ product2.note }}</td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</section> </section>

@ -8,10 +8,10 @@
[disablePagination]="true" [disablePagination]="true"
[tabPanel]="tabPanel" [tabPanel]="tabPanel"
> >
<ng-container *ngFor="let tab of tabs"> @for (tab of tabs; track tab) {
@if (tab.showCondition !== false) {
<a <a
#rla="routerLinkActive" #rla="routerLinkActive"
*ngIf="tab.showCondition !== false"
class="no-min-width px-3" class="no-min-width px-3"
mat-tab-link mat-tab-link
routerLinkActive routerLinkActive
@ -25,5 +25,6 @@
/> />
<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,10 +8,10 @@
[disablePagination]="true" [disablePagination]="true"
[tabPanel]="tabPanel" [tabPanel]="tabPanel"
> >
<ng-container *ngFor="let tab of tabs"> @for (tab of tabs; track tab) {
@if (tab.showCondition !== false) {
<a <a
#rla="routerLinkActive" #rla="routerLinkActive"
*ngIf="tab.showCondition !== false"
class="no-min-width px-3" class="no-min-width px-3"
mat-tab-link mat-tab-link
routerLinkActive routerLinkActive
@ -25,5 +25,6 @@
/> />
<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