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
- Migrated the `@ghostfolio/client` components to control flow
- Improved the language localization for German (`de`)
- Upgraded `angular` from version `17.3.10` to `18.0.2`
- Upgraded `Nx` from version `19.0.5` to `19.2.2`

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

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

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

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

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

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

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

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

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

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

@ -4,8 +4,11 @@
(keyup.enter)="platformForm.valid && onSubmit()"
(ngSubmit)="onSubmit()"
>
<h1 *ngIf="data.platform.id" i18n mat-dialog-title>Update platform</h1>
<h1 *ngIf="!data.platform.id" i18n mat-dialog-title>Add platform</h1>
@if (data.platform.id) {
<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>
<mat-form-field appearance="outline" class="w-100">

@ -4,8 +4,11 @@
(keyup.enter)="tagForm.valid && onSubmit()"
(ngSubmit)="onSubmit()"
>
<h1 *ngIf="data.tag.id" i18n mat-dialog-title>Update tag</h1>
<h1 *ngIf="!data.tag.id" i18n mat-dialog-title>Add tag</h1>
@if (data.tag.id) {
<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>
<mat-form-field appearance="outline" class="w-100">

@ -49,43 +49,44 @@
}"
>{{ (element.id | slice: 0 : 5) + '...' }}</span
>
<gf-premium-indicator
*ngIf="element?.subscription?.type === 'Premium'"
class="ml-1"
[enableLink]="false"
[title]="
'Expires ' +
formatDistanceToNow(element.subscription.expiresAt) +
' (' +
(element.subscription.expiresAt | date: defaultDateFormat) +
')'
"
/>
@if (element?.subscription?.type === 'Premium') {
<gf-premium-indicator
class="ml-1"
[enableLink]="false"
[title]="
'Expires ' +
formatDistanceToNow(element.subscription.expiresAt) +
' (' +
(element.subscription.expiresAt
| date: defaultDateFormat) +
')'
"
/>
}
</div>
</td>
</ng-container>
<ng-container
*ngIf="hasPermissionForSubscription"
matColumnDef="country"
>
<th
*matHeaderCellDef
class="mat-mdc-header-cell px-1 py-2"
mat-header-cell
>
<ng-container i18n>Country</ng-container>
</th>
<td
*matCellDef="let element"
class="mat-mdc-cell px-1 py-2"
mat-cell
>
<span class="h5" [title]="element.country">{{
getEmojiFlag(element.country)
}}</span>
</td>
</ng-container>
@if (hasPermissionForSubscription) {
<ng-container matColumnDef="country">
<th
*matHeaderCellDef
class="mat-mdc-header-cell px-1 py-2"
mat-header-cell
>
<ng-container i18n>Country</ng-container>
</th>
<td
*matCellDef="let element"
class="mat-mdc-cell px-1 py-2"
mat-cell
>
<span class="h5" [title]="element.country">{{
getEmojiFlag(element.country)
}}</span>
</td>
</ng-container>
}
<ng-container matColumnDef="registration">
<th
@ -146,51 +147,49 @@
</td>
</ng-container>
<ng-container
*ngIf="hasPermissionForSubscription"
matColumnDef="engagementPerDay"
>
<th
*matHeaderCellDef
class="mat-mdc-header-cell px-1 py-2 text-right"
mat-header-cell
>
<ng-container i18n>Engagement per Day</ng-container>
</th>
<td
*matCellDef="let element"
class="mat-mdc-cell px-1 py-2 text-right"
mat-cell
>
<gf-value
class="d-inline-block justify-content-end"
[locale]="user?.settings?.locale"
[precision]="0"
[value]="element.engagement"
/>
</td>
</ng-container>
@if (hasPermissionForSubscription) {
<ng-container matColumnDef="engagementPerDay">
<th
*matHeaderCellDef
class="mat-mdc-header-cell px-1 py-2 text-right"
mat-header-cell
>
<ng-container i18n>Engagement per Day</ng-container>
</th>
<td
*matCellDef="let element"
class="mat-mdc-cell px-1 py-2 text-right"
mat-cell
>
<gf-value
class="d-inline-block justify-content-end"
[locale]="user?.settings?.locale"
[precision]="0"
[value]="element.engagement"
/>
</td>
</ng-container>
}
<ng-container
*ngIf="hasPermissionForSubscription"
matColumnDef="lastRequest"
>
<th
*matHeaderCellDef
class="mat-mdc-header-cell px-1 py-2"
i18n
mat-header-cell
>
Last Request
</th>
<td
*matCellDef="let element"
class="mat-mdc-cell px-1 py-2"
mat-cell
>
{{ formatDistanceToNow(element.lastActivity) }}
</td>
</ng-container>
@if (hasPermissionForSubscription) {
<ng-container matColumnDef="lastRequest">
<th
*matHeaderCellDef
class="mat-mdc-header-cell px-1 py-2"
i18n
mat-header-cell
>
Last Request
</th>
<td
*matCellDef="let element"
class="mat-mdc-cell px-1 py-2"
mat-cell
>
{{ formatDistanceToNow(element.lastActivity) }}
</td>
</ng-container>
}
<ng-container matColumnDef="actions" stickyEnd>
<th
@ -212,16 +211,14 @@
<ion-icon name="ellipsis-horizontal" />
</button>
<mat-menu #userMenu="matMenu" xPosition="before">
<button
*ngIf="hasPermissionToImpersonateAllUsers"
mat-menu-item
(click)="onImpersonateUser(element.id)"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="contract-outline" />
<span i18n>Impersonate User</span>
</span>
</button>
@if (hasPermissionToImpersonateAllUsers) {
<button mat-menu-item (click)="onImpersonateUser(element.id)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="contract-outline" />
<span i18n>Impersonate User</span>
</span>
</button>
}
<button
mat-menu-item
[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"
>
<span i18n>Performance</span>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
/>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</div>
</div>
<div class="col-md-6 col-xs-12 d-flex justify-content-end">
@ -24,33 +23,33 @@
(selectionChange)="onChangeBenchmark($event.value)"
>
<mat-option [value]="null" />
<mat-option
*ngFor="let symbolProfile of benchmarks"
[value]="symbolProfile.id"
>{{ symbolProfile.name }}</mat-option
>
<mat-option
*ngIf="hasPermissionToAccessAdminControl"
[routerLink]="['/admin', 'market-data']"
>
<div class="align-items-center d-flex">
<ion-icon class="mr-2 text-muted" name="arrow-forward-outline" />
<span i18n>Manage Benchmarks</span>
</div>
</mat-option>
@for (symbolProfile of benchmarks; track symbolProfile) {
<mat-option [value]="symbolProfile.id">{{
symbolProfile.name
}}</mat-option>
}
@if (hasPermissionToAccessAdminControl) {
<mat-option [routerLink]="['/admin', 'market-data']">
<div class="align-items-center d-flex">
<ion-icon class="mr-2 text-muted" name="arrow-forward-outline" />
<span i18n>Manage Benchmarks</span>
</div>
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<div class="chart-container">
<ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse"
[theme]="{
height: '100%',
width: '100%'
}"
/>
@if (isLoading) {
<ngx-skeleton-loader
animation="pulse"
[theme]="{
height: '100%',
width: '100%'
}"
/>
}
<canvas
#chartCanvas
class="h-100"

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

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

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

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

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

@ -1,30 +1,32 @@
<div class="container">
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Markets</h1>
<div *ngIf="hasPermissionToAccessFearAndGreedIndex" class="mb-5 row">
<div class="col-xs-12 col-md-8 offset-md-2">
<div class="mb-2 text-center text-muted">
<small i18n>Last {{ numberOfDays }} Days</small>
@if (hasPermissionToAccessFearAndGreedIndex) {
<div class="mb-5 row">
<div class="col-xs-12 col-md-8 offset-md-2">
<div class="mb-2 text-center text-muted">
<small i18n>Last {{ numberOfDays }} Days</small>
</div>
<gf-line-chart
class="mb-3"
symbol="Fear & Greed Index"
[colorScheme]="user?.settings?.colorScheme"
[historicalDataItems]="historicalDataItems"
[isAnimated]="true"
[locale]="user?.settings?.locale || undefined"
[showXAxis]="true"
[showYAxis]="true"
[yMax]="100"
[yMaxLabel]="greedLabel"
[yMin]="0"
[yMinLabel]="fearLabel"
/>
<gf-fear-and-greed-index
class="d-flex justify-content-center"
[fearAndGreedIndex]="fearAndGreedIndex"
/>
</div>
<gf-line-chart
class="mb-3"
symbol="Fear & Greed Index"
[colorScheme]="user?.settings?.colorScheme"
[historicalDataItems]="historicalDataItems"
[isAnimated]="true"
[locale]="user?.settings?.locale || undefined"
[showXAxis]="true"
[showYAxis]="true"
[yMax]="100"
[yMaxLabel]="greedLabel"
[yMin]="0"
[yMinLabel]="fearLabel"
/>
<gf-fear-and-greed-index
class="d-flex justify-content-center"
[fearAndGreedIndex]="fearAndGreedIndex"
/>
</div>
</div>
}
<div class="mb-3 row">
<div class="col-xs-12 col-md-8 offset-md-2">
@ -33,15 +35,16 @@
[locale]="user?.settings?.locale || undefined"
[user]="user"
/>
<ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse"
class="px-2 py-3"
[theme]="{
height: '1.5rem',
width: '100%'
}"
/>
@if (isLoading) {
<ngx-skeleton-loader
animation="pulse"
class="px-2 py-3"
[theme]="{
height: '1.5rem',
width: '100%'
}"
/>
}
</div>
</div>
</div>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -125,12 +125,13 @@
href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)"
>&#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"
>hi&#64;ghostfol.io</a
></ng-container
>
>
}
or
<a
href="https://github.com/ghostfolio/ghostfolio"
@ -154,12 +155,13 @@
href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)"
>&#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"
>hi&#64;ghostfol.io</a
></ng-container
>
>
}
or start a discussion at
<a
href="https://github.com/ghostfolio/ghostfolio"

@ -98,23 +98,21 @@
start a new subscription.</mat-card-content
>
</mat-card>
<mat-card
*ngIf="user?.subscription?.type === 'Premium'"
appearance="outlined"
class="mb-3"
>
<mat-card-header>
<mat-card-title
>I cannot find my broker in the list of platforms. What can I
do?</mat-card-title
>
</mat-card-header>
<mat-card-content>
Please send an e-mail with the web address of your broker to
<a href="mailto:hi@ghostfol.io">hi&#64;ghostfol.io</a> and we are
happy to add it.
</mat-card-content>
</mat-card>
@if (user?.subscription?.type === 'Premium') {
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>I cannot find my broker in the list of platforms. What can I
do?</mat-card-title
>
</mat-card-header>
<mat-card-content>
Please send an e-mail with the web address of your broker to
<a href="mailto:hi@ghostfol.io">hi&#64;ghostfol.io</a> and we are
happy to add it.
</mat-card-content>
</mat-card>
}
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Which devices are supported?</mat-card-title>
@ -156,12 +154,13 @@
href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)"
>&#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"
>hi&#64;ghostfol.io</a
></ng-container
>
>
}
or start a discussion at
<a
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 { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
@ -14,7 +13,6 @@ import { Subject, takeUntil } from 'rxjs';
@Component({
host: { class: 'page' },
imports: [
CommonModule,
GfPremiumIndicatorComponent,
MatButtonModule,
MatCardModule,

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

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

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

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

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

@ -27,10 +27,9 @@
class="align-items-center d-flex flex-grow-1 mr-2 text-truncate"
>
<span i18n>Absolute Asset Performance</span>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
/>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</div>
<div class="d-flex justify-content-end">
<gf-value
@ -71,10 +70,9 @@
class="align-items-center d-flex flex-grow-1 mr-2 text-truncate"
>
<span i18n>Absolute Currency Performance</span>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
/>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</div>
<div class="d-flex justify-content-end">
<gf-value
@ -170,39 +168,42 @@
</mat-card-header>
<mat-card-content>
<ol class="mb-0 ml-1 pl-3">
<li *ngFor="let holding of top3" class="py-1">
<a
class="d-flex"
[queryParams]="{
dataSource: holding.dataSource,
holdingDetailDialog: true,
symbol: holding.symbol
}"
[routerLink]="[]"
>
<div class="flex-grow-1 mr-2">{{ holding.name }}</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="holding.netPerformancePercentWithCurrencyEffect"
/>
</div>
</a>
</li>
@for (holding of top3; track holding) {
<li class="py-1">
<a
class="d-flex"
[queryParams]="{
dataSource: holding.dataSource,
holdingDetailDialog: true,
symbol: holding.symbol
}"
[routerLink]="[]"
>
<div class="flex-grow-1 mr-2">{{ holding.name }}</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="holding.netPerformancePercentWithCurrencyEffect"
/>
</div>
</a>
</li>
}
</ol>
<div>
<ngx-skeleton-loader
*ngIf="!top3"
animation="pulse"
[theme]="{
height: '1.5rem',
width: '100%'
}"
/>
@if (!top3) {
<ngx-skeleton-loader
animation="pulse"
[theme]="{
height: '1.5rem',
width: '100%'
}"
/>
}
</div>
</mat-card-content>
</mat-card>
@ -216,39 +217,42 @@
</mat-card-header>
<mat-card-content>
<ol class="mb-0 ml-1 pl-3">
<li *ngFor="let holding of bottom3" class="py-1">
<a
class="d-flex"
[queryParams]="{
dataSource: holding.dataSource,
holdingDetailDialog: true,
symbol: holding.symbol
}"
[routerLink]="[]"
>
<div class="flex-grow-1 mr-2">{{ holding.name }}</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="holding.netPerformancePercentWithCurrencyEffect"
/>
</div>
</a>
</li>
@for (holding of bottom3; track holding) {
<li class="py-1">
<a
class="d-flex"
[queryParams]="{
dataSource: holding.dataSource,
holdingDetailDialog: true,
symbol: holding.symbol
}"
[routerLink]="[]"
>
<div class="flex-grow-1 mr-2">{{ holding.name }}</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="holding.netPerformancePercentWithCurrencyEffect"
/>
</div>
</a>
</li>
}
</ol>
<div>
<ngx-skeleton-loader
*ngIf="!bottom3"
animation="pulse"
[theme]="{
height: '1.5rem',
width: '100%'
}"
/>
@if (!bottom3) {
<ngx-skeleton-loader
animation="pulse"
[theme]="{
height: '1.5rem',
width: '100%'
}"
/>
}
</div>
</mat-card-content>
</mat-card>
@ -262,10 +266,9 @@
class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate"
>
<span i18n>Portfolio Evolution</span>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
/>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</div>
</div>
<div class="chart-container">
@ -292,10 +295,9 @@
class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate"
>
<span i18n>Investment Timeline</span>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
/>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</div>
<gf-toggle
class="d-none d-lg-block"
@ -305,26 +307,28 @@
(change)="onChangeGroupBy($event.value)"
/>
</div>
<div *ngIf="streaks" class="row">
<div class="col-md-6 col-xs-12 my-2">
<gf-value
i18n
size="large"
[unit]="unitCurrentStreak"
[value]="streaks?.currentStreak"
>Current Streak</gf-value
>
</div>
<div class="col-md-6 col-xs-12 my-2">
<gf-value
i18n
size="large"
[unit]="unitLongestStreak"
[value]="streaks?.longestStreak"
>Longest Streak</gf-value
>
@if (streaks) {
<div class="row">
<div class="col-md-6 col-xs-12 my-2">
<gf-value
i18n
size="large"
[unit]="unitCurrentStreak"
[value]="streaks?.currentStreak"
>Current Streak</gf-value
>
</div>
<div class="col-md-6 col-xs-12 my-2">
<gf-value
i18n
size="large"
[unit]="unitLongestStreak"
[value]="streaks?.longestStreak"
>Longest Streak</gf-value
>
</div>
</div>
</div>
}
<div class="chart-container">
<gf-investment-chart
class="h-100"
@ -350,10 +354,9 @@
class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate"
>
<span i18n>Dividend Timeline</span>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
/>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</div>
<gf-toggle
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>
<div>
<h4 class="align-items-center d-flex mb-3">
<span i18n>Calculator</span
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
/>
<span i18n>Calculator</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-fire-calculator
[annualInterestRate]="user?.settings?.annualInterestRate"
@ -38,67 +37,68 @@
</div>
<div>
<h4 class="align-items-center d-flex">
<span i18n>4% Rule</span
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
/>
<span i18n>4% Rule</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<div *ngIf="isLoading">
<ngx-skeleton-loader
animation="pulse"
class="my-1"
[theme]="{
height: '1rem',
width: '100%'
}"
/>
<ngx-skeleton-loader
animation="pulse"
[theme]="{
height: '1rem',
width: '10rem'
}"
/>
</div>
<div
*ngIf="!isLoading"
i18n
[ngClass]="{ 'text-muted': user?.subscription?.type === 'Basic' }"
>
If you retire today, you would be able to withdraw
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerYear?.toNumber()"
/>
per year</span
>
or
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerMonth?.toNumber()"
@if (isLoading) {
<div>
<ngx-skeleton-loader
animation="pulse"
class="my-1"
[theme]="{
height: '1rem',
width: '100%'
}"
/>
per month</span
>, based on your total assets of
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="fireWealth?.toNumber()"
<ngx-skeleton-loader
animation="pulse"
[theme]="{
height: '1rem',
width: '10rem'
}"
/>
</span>
and a withdrawal rate of 4%.
</div>
</div>
} @else {
<div
i18n
[ngClass]="{ 'text-muted': user?.subscription?.type === 'Basic' }"
>
If you retire today, you would be able to withdraw
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerYear?.toNumber()"
/>
per year</span
>
or
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerMonth?.toNumber()"
/>
per month</span
>, based on your total assets of
<span class="font-weight-bold"
><gf-value
class="d-inline-block"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="fireWealth?.toNumber()"
/>
</span>
and a withdrawal rate of 4%.
</div>
}
</div>
</div>
@ -119,11 +119,10 @@
</p>
<div class="mb-4">
<h4 class="align-items-center d-flex m-0">
<span i18n>Emergency Fund</span
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
/>
<span i18n>Emergency Fund</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
@ -132,11 +131,10 @@
</div>
<div class="mb-4">
<h4 class="align-items-center d-flex m-0">
<span i18n>Currency Cluster Risks</span
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
/>
<span i18n>Currency Cluster Risks</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
@ -145,11 +143,10 @@
</div>
<div class="mb-4">
<h4 class="align-items-center d-flex m-0">
<span i18n>Account Cluster Risks</span
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
/>
<span i18n>Account Cluster Risks</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
@ -158,11 +155,10 @@
</div>
<div>
<h4 class="align-items-center d-flex m-0">
<span i18n>Fees</span
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
/>
<span i18n>Fees</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save