Browse Source

Merge branch 'main' into BindAllIf

pull/2407/head
Prashanth Mohan 2 years ago
parent
commit
111c0738e5
  1. 8
      CHANGELOG.md
  2. 68
      apps/api/src/app/portfolio/portfolio.service.ts
  3. 32
      apps/api/src/assets/sitemap.xml
  4. 9
      apps/api/src/models/rules/account-cluster-risk/current-investment.ts
  5. 9
      apps/api/src/models/rules/account-cluster-risk/single-account.ts
  6. 9
      apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts
  7. 9
      apps/api/src/models/rules/currency-cluster-risk/current-investment.ts
  8. 46
      apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts
  9. 19
      apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts
  10. 1
      apps/client/project.json
  11. 0
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  12. 0
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html
  13. 0
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.module.ts
  14. 0
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.scss
  15. 0
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts
  16. 146
      apps/client/src/app/components/user-account-access/user-account-access.component.ts
  17. 17
      apps/client/src/app/components/user-account-access/user-account-access.html
  18. 23
      apps/client/src/app/components/user-account-access/user-account-access.module.ts
  19. 12
      apps/client/src/app/components/user-account-access/user-account-access.scss
  20. 160
      apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
  21. 69
      apps/client/src/app/components/user-account-membership/user-account-membership.html
  22. 23
      apps/client/src/app/components/user-account-membership/user-account-membership.module.ts
  23. 8
      apps/client/src/app/components/user-account-membership/user-account-membership.scss
  24. 258
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  25. 197
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  26. 30
      apps/client/src/app/components/user-account-settings/user-account-settings.module.ts
  27. 13
      apps/client/src/app/components/user-account-settings/user-account-settings.scss
  28. 3
      apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
  29. 25
      apps/client/src/app/pages/portfolio/fire/fire-page.html
  30. 18
      apps/client/src/app/pages/resources/personal-finance-tools/products.ts
  31. 31
      apps/client/src/app/pages/resources/personal-finance-tools/products/finary-page.component.ts
  32. 31
      apps/client/src/app/pages/resources/personal-finance-tools/products/stockle-page.component.ts
  33. 20
      apps/client/src/app/pages/user-account/user-account-page-routing.module.ts
  34. 433
      apps/client/src/app/pages/user-account/user-account-page.component.ts
  35. 338
      apps/client/src/app/pages/user-account/user-account-page.html
  36. 33
      apps/client/src/app/pages/user-account/user-account-page.module.ts
  37. 12
      apps/client/src/app/pages/user-account/user-account-page.scss
  38. BIN
      apps/client/src/assets/fonts/Inter-Black.woff
  39. BIN
      apps/client/src/assets/fonts/Inter-Black.woff2
  40. BIN
      apps/client/src/assets/fonts/Inter-BlackItalic.woff
  41. BIN
      apps/client/src/assets/fonts/Inter-BlackItalic.woff2
  42. BIN
      apps/client/src/assets/fonts/Inter-Bold.woff
  43. BIN
      apps/client/src/assets/fonts/Inter-Bold.woff2
  44. BIN
      apps/client/src/assets/fonts/Inter-BoldItalic.woff
  45. BIN
      apps/client/src/assets/fonts/Inter-BoldItalic.woff2
  46. BIN
      apps/client/src/assets/fonts/Inter-ExtraBold.woff
  47. BIN
      apps/client/src/assets/fonts/Inter-ExtraBold.woff2
  48. BIN
      apps/client/src/assets/fonts/Inter-ExtraBoldItalic.woff
  49. BIN
      apps/client/src/assets/fonts/Inter-ExtraBoldItalic.woff2
  50. BIN
      apps/client/src/assets/fonts/Inter-ExtraLight.woff
  51. BIN
      apps/client/src/assets/fonts/Inter-ExtraLight.woff2
  52. BIN
      apps/client/src/assets/fonts/Inter-ExtraLightItalic.woff
  53. BIN
      apps/client/src/assets/fonts/Inter-ExtraLightItalic.woff2
  54. BIN
      apps/client/src/assets/fonts/Inter-Italic.woff
  55. BIN
      apps/client/src/assets/fonts/Inter-Italic.woff2
  56. BIN
      apps/client/src/assets/fonts/Inter-Light.woff
  57. BIN
      apps/client/src/assets/fonts/Inter-Light.woff2
  58. BIN
      apps/client/src/assets/fonts/Inter-LightItalic.woff
  59. BIN
      apps/client/src/assets/fonts/Inter-LightItalic.woff2
  60. BIN
      apps/client/src/assets/fonts/Inter-Medium.woff
  61. BIN
      apps/client/src/assets/fonts/Inter-Medium.woff2
  62. BIN
      apps/client/src/assets/fonts/Inter-MediumItalic.woff
  63. BIN
      apps/client/src/assets/fonts/Inter-MediumItalic.woff2
  64. BIN
      apps/client/src/assets/fonts/Inter-Regular.woff
  65. BIN
      apps/client/src/assets/fonts/Inter-Regular.woff2
  66. BIN
      apps/client/src/assets/fonts/Inter-SemiBold.woff
  67. BIN
      apps/client/src/assets/fonts/Inter-SemiBold.woff2
  68. BIN
      apps/client/src/assets/fonts/Inter-SemiBoldItalic.woff
  69. BIN
      apps/client/src/assets/fonts/Inter-SemiBoldItalic.woff2
  70. BIN
      apps/client/src/assets/fonts/Inter-Thin.woff
  71. BIN
      apps/client/src/assets/fonts/Inter-Thin.woff2
  72. BIN
      apps/client/src/assets/fonts/Inter-ThinItalic.woff
  73. BIN
      apps/client/src/assets/fonts/Inter-ThinItalic.woff2
  74. BIN
      apps/client/src/assets/fonts/Inter-italic.var.woff2
  75. BIN
      apps/client/src/assets/fonts/Inter-roman.var.woff2
  76. BIN
      apps/client/src/assets/fonts/Inter.var.woff2
  77. 226
      apps/client/src/assets/fonts/inter.css
  78. 40
      apps/client/src/locales/messages.de.xlf
  79. 40
      apps/client/src/locales/messages.es.xlf
  80. 40
      apps/client/src/locales/messages.fr.xlf
  81. 40
      apps/client/src/locales/messages.it.xlf
  82. 40
      apps/client/src/locales/messages.nl.xlf
  83. 40
      apps/client/src/locales/messages.pt.xlf
  84. 40
      apps/client/src/locales/messages.tr.xlf
  85. 37
      apps/client/src/locales/messages.xlf
  86. 2
      apps/client/src/styles.scss
  87. 2
      package.json

8
CHANGELOG.md

@ -5,10 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## 2.7.0 - 2023-09-30
### Added
- Added a new static portfolio analysis rule: Emergency fund setup
- Added tabs to the user account page
### Changed ### Changed
- Set up the _Inter_ font family
- Upgraded `yahoo-finance2` from version `2.7.0` to `2.8.0` - Upgraded `yahoo-finance2` from version `2.7.0` to `2.8.0`
### Fixed ### Fixed

68
apps/api/src/app/portfolio/portfolio.service.ts

@ -10,6 +10,7 @@ import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rule
import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment'; import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment';
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment'; import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup';
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment'; import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
@ -1214,12 +1215,6 @@ export class PortfolioService {
userId userId
}); });
if (isEmpty(orders)) {
return {
rules: {}
};
}
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
currency: userCurrency, currency: userCurrency,
currentRateService: this.currentRateService, currentRateService: this.currentRateService,
@ -1228,7 +1223,9 @@ export class PortfolioService {
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
const portfolioStart = parseDate(transactionPoints[0].date); const portfolioStart = parseDate(
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
);
const currentPositions = const currentPositions =
await portfolioCalculator.getCurrentPositions(portfolioStart); await portfolioCalculator.getCurrentPositions(portfolioStart);
@ -1249,33 +1246,48 @@ export class PortfolioService {
userId userId
}); });
const userSettings = <UserSettings>this.request.user.Settings.settings;
return { return {
rules: { rules: {
accountClusterRisk: await this.rulesService.evaluate( accountClusterRisk: isEmpty(orders)
[ ? undefined
new AccountClusterRiskCurrentInvestment( : await this.rulesService.evaluate(
this.exchangeRateDataService, [
accounts new AccountClusterRiskCurrentInvestment(
this.exchangeRateDataService,
accounts
),
new AccountClusterRiskSingleAccount(
this.exchangeRateDataService,
accounts
)
],
userSettings
), ),
new AccountClusterRiskSingleAccount( currencyClusterRisk: isEmpty(orders)
this.exchangeRateDataService, ? undefined
accounts : await this.rulesService.evaluate(
) [
], new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
<UserSettings>this.request.user.Settings.settings this.exchangeRateDataService,
), positions
currencyClusterRisk: await this.rulesService.evaluate( ),
[ new CurrencyClusterRiskCurrentInvestment(
new CurrencyClusterRiskBaseCurrencyCurrentInvestment( this.exchangeRateDataService,
this.exchangeRateDataService, positions
positions )
],
userSettings
), ),
new CurrencyClusterRiskCurrentInvestment( emergencyFund: await this.rulesService.evaluate(
[
new EmergencyFundSetup(
this.exchangeRateDataService, this.exchangeRateDataService,
positions userSettings.emergencyFund
) )
], ],
<UserSettings>this.request.user.Settings.settings userSettings
), ),
fees: await this.rulesService.evaluate( fees: await this.rulesService.evaluate(
[ [
@ -1285,7 +1297,7 @@ export class PortfolioService {
this.getFees({ userCurrency, activities: orders }).toNumber() this.getFees({ userCurrency, activities: orders }).toNumber()
) )
], ],
<UserSettings>this.request.user.Settings.settings userSettings
) )
} }
}; };

32
apps/api/src/assets/sitemap.xml

@ -78,6 +78,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-exirio</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-exirio</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-finary</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-folishare</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-folishare</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -146,6 +150,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-snowball-analytics</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-snowball-analytics</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-stockle</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-stockmarketeye</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-stockmarketeye</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -324,6 +332,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-exirio</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-exirio</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-finary</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-folishare</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-folishare</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -392,6 +404,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-snowball-analytics</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-snowball-analytics</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-stockle</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-stockmarketeye</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-stockmarketeye</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -598,6 +614,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-exirio</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-exirio</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-finary</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-folishare</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-folishare</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -666,6 +686,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-snowball-analytics</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-snowball-analytics</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-stockle</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-stockmarketeye</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-stockmarketeye</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -718,6 +742,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-exirio</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-exirio</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-finary</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-folishare</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-folishare</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -786,6 +814,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-snowball-analytics</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-snowball-analytics</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-stockle</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-stockmarketeye</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-stockmarketeye</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>

9
apps/api/src/models/rules/account-cluster-risk/current-investment.ts

@ -1,4 +1,5 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { import {
PortfolioDetails, PortfolioDetails,
@ -6,16 +7,18 @@ import {
UserSettings UserSettings
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule';
export class AccountClusterRiskCurrentInvestment extends Rule<Settings> { export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
private accounts: PortfolioDetails['accounts'];
public constructor( public constructor(
protected exchangeRateDataService: ExchangeRateDataService, protected exchangeRateDataService: ExchangeRateDataService,
private accounts: PortfolioDetails['accounts'] accounts: PortfolioDetails['accounts']
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
name: 'Investment' name: 'Investment'
}); });
this.accounts = accounts;
} }
public evaluate(ruleSettings: Settings) { public evaluate(ruleSettings: Settings) {

9
apps/api/src/models/rules/account-cluster-risk/single-account.ts

@ -1,17 +1,20 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioDetails, UserSettings } from '@ghostfolio/common/interfaces'; import { PortfolioDetails, UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule';
export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> { export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
private accounts: PortfolioDetails['accounts'];
public constructor( public constructor(
protected exchangeRateDataService: ExchangeRateDataService, protected exchangeRateDataService: ExchangeRateDataService,
private accounts: PortfolioDetails['accounts'] accounts: PortfolioDetails['accounts']
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
name: 'Single Account' name: 'Single Account'
}); });
this.accounts = accounts;
} }
public evaluate() { public evaluate() {

9
apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts

@ -1,17 +1,20 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces'; import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule';
export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Settings> { export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Settings> {
private positions: TimelinePosition[];
public constructor( public constructor(
protected exchangeRateDataService: ExchangeRateDataService, protected exchangeRateDataService: ExchangeRateDataService,
private positions: TimelinePosition[] positions: TimelinePosition[]
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
name: 'Investment: Base Currency' name: 'Investment: Base Currency'
}); });
this.positions = positions;
} }
public evaluate(ruleSettings: Settings) { public evaluate(ruleSettings: Settings) {

9
apps/api/src/models/rules/currency-cluster-risk/current-investment.ts

@ -1,17 +1,20 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces'; import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule';
export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> { export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
private positions: TimelinePosition[];
public constructor( public constructor(
protected exchangeRateDataService: ExchangeRateDataService, protected exchangeRateDataService: ExchangeRateDataService,
private positions: TimelinePosition[] positions: TimelinePosition[]
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
name: 'Investment' name: 'Investment'
}); });
this.positions = positions;
} }
public evaluate(ruleSettings: Settings) { public evaluate(ruleSettings: Settings) {

46
apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts

@ -0,0 +1,46 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { UserSettings } from '@ghostfolio/common/interfaces';
export class EmergencyFundSetup extends Rule<Settings> {
private emergencyFund: number;
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
emergencyFund: number
) {
super(exchangeRateDataService, {
name: 'Emergency Fund: Set up'
});
this.emergencyFund = emergencyFund;
}
public evaluate(ruleSettings: Settings) {
if (this.emergencyFund > ruleSettings.threshold) {
return {
evaluation: 'An emergency fund has been set up',
value: true
};
}
return {
evaluation: 'No emergency fund has been set up',
value: false
};
}
public getSettings(aUserSettings: UserSettings): Settings {
return {
baseCurrency: aUserSettings.baseCurrency,
isActive: true,
threshold: 0
};
}
}
interface Settings extends RuleSettings {
baseCurrency: string;
threshold: number;
}

19
apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts

@ -1,22 +1,29 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { UserSettings } from '@ghostfolio/common/interfaces'; import { UserSettings } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule';
export class FeeRatioInitialInvestment extends Rule<Settings> { export class FeeRatioInitialInvestment extends Rule<Settings> {
private fees: number;
private totalInvestment: number;
public constructor( public constructor(
protected exchangeRateDataService: ExchangeRateDataService, protected exchangeRateDataService: ExchangeRateDataService,
private totalInvestment: number, totalInvestment: number,
private fees: number fees: number
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
name: 'Investment' name: 'Fee Ratio'
}); });
this.fees = fees;
this.totalInvestment = totalInvestment;
} }
public evaluate(ruleSettings: Settings) { public evaluate(ruleSettings: Settings) {
const feeRatio = this.fees / this.totalInvestment; const feeRatio = this.totalInvestment
? this.fees / this.totalInvestment
: 0;
if (feeRatio > ruleSettings.threshold) { if (feeRatio > ruleSettings.threshold) {
return { return {

1
apps/client/project.json

@ -21,6 +21,7 @@
"tsConfig": "apps/client/tsconfig.app.json", "tsConfig": "apps/client/tsconfig.app.json",
"assets": [], "assets": [],
"styles": [ "styles": [
"apps/client/src/assets/fonts/inter.css",
"apps/client/src/styles/theme.scss", "apps/client/src/styles/theme.scss",
"apps/client/src/styles.scss" "apps/client/src/styles.scss"
], ],

0
apps/client/src/app/pages/user-account/create-or-update-access-dialog/create-or-update-access-dialog.component.ts → apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts

0
apps/client/src/app/pages/user-account/create-or-update-access-dialog/create-or-update-access-dialog.html → apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html

0
apps/client/src/app/pages/user-account/create-or-update-access-dialog/create-or-update-access-dialog.module.ts → apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.module.ts

0
apps/client/src/app/pages/user-account/create-or-update-access-dialog/create-or-update-access-dialog.scss → apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.scss

0
apps/client/src/app/pages/user-account/create-or-update-access-dialog/interfaces/interfaces.ts → apps/client/src/app/components/user-account-access/create-or-update-access-dialog/interfaces/interfaces.ts

146
apps/client/src/app/components/user-account-access/user-account-access.component.ts

@ -0,0 +1,146 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
OnDestroy,
OnInit
} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Access, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog/create-or-update-access-dialog.component';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'gf-user-account-access',
styleUrls: ['./user-account-access.scss'],
templateUrl: './user-account-access.html'
})
export class UserAccountAccessComponent implements OnDestroy, OnInit {
public accesses: Access[];
public deviceType: string;
public hasPermissionToCreateAccess: boolean;
public hasPermissionToDeleteAccess: boolean;
public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private route: ActivatedRoute,
private router: Router,
private userService: UserService
) {
const { globalPermissions } = this.dataService.fetchInfo();
this.hasPermissionToDeleteAccess = hasPermission(
globalPermissions,
permissions.deleteAccess
);
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.hasPermissionToCreateAccess = hasPermission(
this.user.permissions,
permissions.createAccess
);
this.hasPermissionToDeleteAccess = hasPermission(
this.user.permissions,
permissions.deleteAccess
);
this.changeDetectorRef.markForCheck();
}
});
this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (params['createDialog']) {
this.openCreateAccessDialog();
}
});
}
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.update();
}
public onDeleteAccess(aId: string) {
this.dataService
.deleteAccess(aId)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe({
next: () => {
this.update();
}
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private openCreateAccessDialog(): void {
const dialogRef = this.dialog.open(CreateOrUpdateAccessDialog, {
data: {
access: {
alias: '',
type: 'PUBLIC'
}
},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data: any) => {
const access: CreateAccessDto = data?.access;
if (access) {
this.dataService
.postAccess({ alias: access.alias })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe({
next: () => {
this.update();
}
});
}
this.router.navigate(['.'], { relativeTo: this.route });
});
}
private update() {
this.dataService
.fetchAccesses()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((accesses) => {
this.accesses = accesses;
this.changeDetectorRef.markForCheck();
});
}
}

17
apps/client/src/app/components/user-account-access/user-account-access.html

@ -0,0 +1,17 @@
<div class="container">
<h1
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"
></gf-premium-indicator>
</h1>
<gf-access-table
[accesses]="accesses"
[hasPermissionToCreateAccess]="hasPermissionToCreateAccess"
[showActions]="hasPermissionToDeleteAccess"
(accessDeleted)="onDeleteAccess($event)"
></gf-access-table>
</div>

23
apps/client/src/app/components/user-account-access/user-account-access.module.ts

@ -0,0 +1,23 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatDialogModule } from '@angular/material/dialog';
import { RouterModule } from '@angular/router';
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-dialog/create-or-update-access-dialog.module';
import { UserAccountAccessComponent } from './user-account-access.component';
@NgModule({
declarations: [UserAccountAccessComponent],
exports: [UserAccountAccessComponent],
imports: [
CommonModule,
GfCreateOrUpdateAccessDialogModule,
GfPortfolioAccessTableModule,
GfPremiumIndicatorModule,
MatDialogModule,
RouterModule
]
})
export class GfUserAccountAccessModule {}

12
apps/client/src/app/components/user-account-access/user-account-access.scss

@ -0,0 +1,12 @@
:host {
color: rgb(var(--dark-primary-text));
display: block;
gf-access-table {
overflow-x: auto;
}
}
:host-context(.is-dark-theme) {
color: rgb(var(--light-primary-text));
}

160
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts

@ -0,0 +1,160 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
OnDestroy,
OnInit
} from '@angular/core';
import {
MatSnackBar,
MatSnackBarRef,
TextOnlySnackBar
} from '@angular/material/snack-bar';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { getDateFormatString } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { StripeService } from 'ngx-stripe';
import { EMPTY, Subject } from 'rxjs';
import { catchError, switchMap, takeUntil } from 'rxjs/operators';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'gf-user-account-membership',
styleUrls: ['./user-account-membership.scss'],
templateUrl: './user-account-membership.html'
})
export class UserAccountMembershipComponent implements OnDestroy, OnInit {
public baseCurrency: string;
public coupon: number;
public couponId: string;
public defaultDateFormat: string;
public hasPermissionForSubscription: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public price: number;
public priceId: string;
public routerLinkPricing = ['/' + $localize`pricing`];
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
public trySubscriptionMail =
'mailto:hi@ghostfol.io?Subject=Ghostfolio Premium Trial&body=Hello%0D%0DI am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?%0D%0DKind regards';
public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private snackBar: MatSnackBar,
private stripeService: StripeService,
private userService: UserService
) {
const { baseCurrency, globalPermissions, subscriptions } =
this.dataService.fetchInfo();
this.baseCurrency = baseCurrency;
this.hasPermissionForSubscription = hasPermission(
globalPermissions,
permissions.enableSubscription
);
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.defaultDateFormat = getDateFormatString(
this.user.settings.locale
);
this.hasPermissionToUpdateUserSettings = hasPermission(
this.user.permissions,
permissions.updateUserSettings
);
this.coupon = subscriptions?.[this.user.subscription.offer]?.coupon;
this.couponId =
subscriptions?.[this.user.subscription.offer]?.couponId;
this.price = subscriptions?.[this.user.subscription.offer]?.price;
this.priceId = subscriptions?.[this.user.subscription.offer]?.priceId;
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnInit() {}
public onCheckout() {
this.dataService
.createCheckoutSession({ couponId: this.couponId, priceId: this.priceId })
.pipe(
switchMap(({ sessionId }: { sessionId: string }) => {
return this.stripeService.redirectToCheckout({ sessionId });
}),
catchError((error) => {
alert(error.message);
throw error;
})
)
.subscribe((result) => {
if (result.error) {
alert(result.error.message);
}
});
}
public onRedeemCoupon() {
let couponCode = prompt($localize`Please enter your coupon code:`);
couponCode = couponCode?.trim();
if (couponCode) {
this.dataService
.redeemCoupon(couponCode)
.pipe(
takeUntil(this.unsubscribeSubject),
catchError(() => {
this.snackBar.open(
'😞 ' + $localize`Could not redeem coupon code`,
undefined,
{
duration: 3000
}
);
return EMPTY;
})
)
.subscribe(() => {
this.snackBarRef = this.snackBar.open(
'✅ ' + $localize`Coupon code has been redeemed`,
$localize`Reload`,
{
duration: 3000
}
);
this.snackBarRef
.afterDismissed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
window.location.reload();
});
this.snackBarRef
.onAction()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
window.location.reload();
});
});
}
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

69
apps/client/src/app/components/user-account-membership/user-account-membership.html

@ -0,0 +1,69 @@
<div class="container">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Membership</h1>
<div class="row">
<div class="col">
<div class="d-flex">
<div class="mx-auto">
<div class="align-items-center d-flex mb-1">
<a [routerLink]="routerLinkPricing"
>{{ user?.subscription?.type }}</a
>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Premium'"
class="ml-1"
></gf-premium-indicator>
</div>
<div *ngIf="user?.subscription?.type === 'Premium'">
<ng-container i18n>Valid until</ng-container> {{
user?.subscription?.expiresAt | date: defaultDateFormat }}
</div>
<div *ngIf="user?.subscription?.type === 'Basic'">
<ng-container
*ngIf="hasPermissionForSubscription && hasPermissionToUpdateUserSettings"
>
<button color="primary" mat-flat-button (click)="onCheckout()">
<ng-container *ngIf="user.subscription.offer === 'default'" i18n
>Upgrade</ng-container
>
<ng-container *ngIf="user.subscription.offer === 'renewal'" i18n
>Renew</ng-container
>
</button>
<div *ngIf="price" class="mt-1">
<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>
</div>
</ng-container>
<a
*ngIf="!user?.subscription?.expiresAt"
class="mr-2 my-2"
mat-stroked-button
[href]="trySubscriptionMail"
><span i18n>Try Premium</span>
<gf-premium-indicator
class="d-inline-block ml-1"
[enableLink]="false"
></gf-premium-indicator
></a>
<a
*ngIf="hasPermissionToUpdateUserSettings"
class="mr-2 my-2"
i18n
mat-stroked-button
[routerLink]=""
(click)="onRedeemCoupon()"
>Redeem Coupon</a
>
</div>
</div>
</div>
</div>
</div>
</div>

23
apps/client/src/app/components/user-account-membership/user-account-membership.module.ts

@ -0,0 +1,23 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfValueModule } from '@ghostfolio/ui/value';
import { UserAccountMembershipComponent } from './user-account-membership.component';
@NgModule({
declarations: [UserAccountMembershipComponent],
exports: [UserAccountMembershipComponent],
imports: [
CommonModule,
GfPremiumIndicatorModule,
GfValueModule,
MatButtonModule,
MatCardModule,
RouterModule
]
})
export class GfUserAccountMembershipModule {}

8
apps/client/src/app/components/user-account-membership/user-account-membership.scss

@ -0,0 +1,8 @@
:host {
color: rgb(var(--dark-primary-text));
display: block;
}
:host-context(.is-dark-theme) {
color: rgb(var(--light-primary-text));
}

258
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts

@ -0,0 +1,258 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
import { MatCheckbox, MatCheckboxChange } from '@angular/material/checkbox';
import { DataService } from '@ghostfolio/client/services/data.service';
import {
STAY_SIGNED_IN,
SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { downloadAsFile } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { format, parseISO } from 'date-fns';
import { uniq } from 'lodash';
import { EMPTY, Subject } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'gf-user-account-settings',
styleUrls: ['./user-account-settings.scss'],
templateUrl: './user-account-settings.html'
})
export class UserAccountSettingsComponent implements OnDestroy, OnInit {
@ViewChild('toggleSignInWithFingerprintEnabledElement')
signInWithFingerprintElement: MatCheckbox;
public appearancePlaceholder = $localize`Auto`;
public baseCurrency: string;
public currencies: string[] = [];
public hasPermissionToUpdateViewMode: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public language = document.documentElement.lang;
public locales = [
'de',
'de-CH',
'en-GB',
'en-US',
'es',
'fr',
'it',
'nl',
'pt',
'tr'
];
public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private settingsStorageService: SettingsStorageService,
private userService: UserService,
public webAuthnService: WebAuthnService
) {
const { baseCurrency, currencies } = this.dataService.fetchInfo();
this.baseCurrency = baseCurrency;
this.currencies = currencies;
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.hasPermissionToUpdateUserSettings = hasPermission(
this.user.permissions,
permissions.updateUserSettings
);
this.hasPermissionToUpdateViewMode = hasPermission(
this.user.permissions,
permissions.updateViewMode
);
this.locales.push(this.user.settings.locale);
this.locales = uniq(this.locales.sort());
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnInit() {
this.update();
}
public onChangeUserSetting(aKey: string, aValue: string) {
this.dataService
.putUserSetting({ [aKey]: aValue })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
if (aKey === 'language') {
if (aValue) {
window.location.href = `../${aValue}/account`;
} else {
window.location.href = `../`;
}
}
});
});
}
public onExperimentalFeaturesChange(aEvent: MatCheckboxChange) {
this.dataService
.putUserSetting({ isExperimentalFeatures: aEvent.checked })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
}
public onExport() {
this.dataService
.fetchExport()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => {
for (const activity of data.activities) {
delete activity.id;
}
downloadAsFile({
content: data,
fileName: `ghostfolio-export-${format(
parseISO(data.meta.date),
'yyyyMMddHHmm'
)}.json`,
format: 'json'
});
});
}
public onRestrictedViewChange(aEvent: MatCheckboxChange) {
this.dataService
.putUserSetting({ isRestrictedView: aEvent.checked })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
}
public onSignInWithFingerprintChange(aEvent: MatCheckboxChange) {
if (aEvent.checked) {
this.registerDevice();
} else {
const confirmation = confirm(
$localize`Do you really want to remove this sign in method?`
);
if (confirmation) {
this.deregisterDevice();
} else {
this.update();
}
}
}
public onViewModeChange(aEvent: MatCheckboxChange) {
this.dataService
.putUserSetting({ viewMode: aEvent.checked === true ? 'ZEN' : 'DEFAULT' })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private deregisterDevice() {
this.webAuthnService
.deregister()
.pipe(
takeUntil(this.unsubscribeSubject),
catchError(() => {
this.update();
return EMPTY;
})
)
.subscribe(() => {
this.update();
});
}
private registerDevice() {
this.webAuthnService
.register()
.pipe(
takeUntil(this.unsubscribeSubject),
catchError(() => {
this.update();
return EMPTY;
})
)
.subscribe(() => {
this.settingsStorageService.removeSetting(STAY_SIGNED_IN);
this.update();
});
}
private update() {
if (this.signInWithFingerprintElement) {
this.signInWithFingerprintElement.checked =
this.webAuthnService.isEnabled() ?? false;
}
}
}

197
apps/client/src/app/components/user-account-settings/user-account-settings.html

@ -0,0 +1,197 @@
<div class="container">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Settings</h1>
<div class="row">
<div class="col">
<div class="align-items-center d-flex py-1">
<div class="pr-1 w-50">
<div i18n>Presenter View</div>
<div class="hint-text text-muted" i18n>
Protection for sensitive information like absolute performances and
quantity values
</div>
</div>
<div class="pl-1 w-50">
<mat-checkbox
color="primary"
[checked]="user.settings.isRestrictedView"
[disabled]="!hasPermissionToUpdateUserSettings"
(change)="onRestrictedViewChange($event)"
></mat-checkbox>
</div>
</div>
<div class="d-flex mt-4 py-1">
<form #changeUserSettingsForm="ngForm" class="w-100">
<div class="d-flex mb-2">
<div class="align-items-center d-flex pt-1 pt-1 w-50">
<ng-container i18n>Base Currency</ng-container>
</div>
<div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-select
name="baseCurrency"
[disabled]="!hasPermissionToUpdateUserSettings"
[value]="user.settings.baseCurrency"
(selectionChange)="onChangeUserSetting('baseCurrency', $event.value)"
>
<mat-option
*ngFor="let currency of currencies"
[value]="currency"
>{{ currency }}</mat-option
>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="align-items-center d-flex mb-2">
<div class="pr-1 w-50">
<div i18n>Language</div>
</div>
<div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-select
name="language"
[disabled]="!hasPermissionToUpdateUserSettings"
[value]="language"
(selectionChange)="onChangeUserSetting('language', $event.value)"
>
<mat-option [value]="null"></mat-option>
<mat-option value="de">Deutsch</mat-option>
<mat-option value="en">English</mat-option>
<mat-option value="es"
>Español (<ng-container i18n>Community</ng-container
>)</mat-option
>
<mat-option value="fr"
>Français (<ng-container i18n>Community</ng-container
>)</mat-option
>
<mat-option value="it"
>Italiano (<ng-container i18n>Community</ng-container
>)</mat-option
>
<mat-option value="nl"
>Nederlands (<ng-container i18n>Community</ng-container
>)</mat-option
>
<mat-option value="pt"
>Português (<ng-container i18n>Community</ng-container
>)</mat-option
>
<mat-option value="tr"
>Türkçe (<ng-container i18n>Community</ng-container
>)</mat-option
>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="align-items-center d-flex mb-2">
<div class="pr-1 w-50">
<div i18n>Locale</div>
<div class="hint-text text-muted">
<ng-container i18n>Date and number format</ng-container>
</div>
</div>
<div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-select
name="locale"
[disabled]="!hasPermissionToUpdateUserSettings"
[value]="user.settings.locale"
(selectionChange)="onChangeUserSetting('locale', $event.value)"
>
<mat-option [value]="null"></mat-option>
<mat-option *ngFor="let locale of locales" [value]="locale"
>{{ locale }}</mat-option
>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="d-flex">
<div class="align-items-center d-flex pr-1 pt-1 w-50">
<ng-container i18n>Appearance</ng-container>
</div>
<div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-select
class="with-placeholder-as-option"
name="colorScheme"
[disabled]="!hasPermissionToUpdateUserSettings"
[placeholder]="appearancePlaceholder"
[value]="user?.settings?.colorScheme"
(selectionChange)="onChangeUserSetting('colorScheme', $event.value)"
>
<mat-option i18n [value]="null">Auto</mat-option>
<mat-option i18n value="LIGHT">Light</mat-option>
<mat-option i18n value="DARK">Dark</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</form>
</div>
<div class="d-flex mt-4 py-1">
<div class="pr-1 w-50">
<div i18n>Zen Mode</div>
<div class="hint-text text-muted" i18n>
Distraction-free experience for turbulent times
</div>
</div>
<div class="pl-1 w-50">
<mat-checkbox
color="primary"
[checked]="user.settings.viewMode === 'ZEN'"
[disabled]="!hasPermissionToUpdateViewMode"
(change)="onViewModeChange($event)"
></mat-checkbox>
</div>
</div>
<div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50">
<div i18n>Biometric Authentication</div>
<div class="hint-text text-muted" i18n>Sign in with fingerprint</div>
</div>
<div class="pl-1 w-50">
<mat-checkbox
#toggleSignInWithFingerprintEnabledElement
color="primary"
[disabled]="!hasPermissionToUpdateUserSettings"
(change)="onSignInWithFingerprintChange($event)"
></mat-checkbox>
</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
</div>
</div>
<div class="pl-1 w-50">
<mat-checkbox
color="primary"
[checked]="user.settings.isExperimentalFeatures"
[disabled]="!hasPermissionToUpdateUserSettings"
(change)="onExperimentalFeaturesChange($event)"
></mat-checkbox>
</div>
</div>
<div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50" i18n>User ID</div>
<div class="pl-1 text-monospace w-50">{{ user?.id }}</div>
</div>
<div class="align-items-center d-flex py-1">
<div class="pr-1 w-50"></div>
<div class="pl-1 text-monospace w-50">
<button color="primary" mat-flat-button (click)="onExport()">
<span i18n>Export Data</span>
</button>
</div>
</div>
</div>
</div>
</div>

30
apps/client/src/app/components/user-account-settings/user-account-settings.module.ts

@ -0,0 +1,30 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router';
import { GfValueModule } from '@ghostfolio/ui/value';
import { UserAccountSettingsComponent } from './user-account-settings.component';
@NgModule({
declarations: [UserAccountSettingsComponent],
exports: [UserAccountSettingsComponent],
imports: [
CommonModule,
FormsModule,
GfValueModule,
MatButtonModule,
MatCardModule,
MatCheckboxModule,
MatFormFieldModule,
MatSelectModule,
ReactiveFormsModule,
RouterModule
]
})
export class GfUserAccountSettingsModule {}

13
apps/client/src/app/components/user-account-settings/user-account-settings.scss

@ -0,0 +1,13 @@
:host {
color: rgb(var(--dark-primary-text));
display: block;
.hint-text {
font-size: 90%;
line-height: 1.2;
}
}
:host-context(.is-dark-theme) {
color: rgb(var(--light-primary-text));
}

3
apps/client/src/app/pages/portfolio/fire/fire-page.component.ts

@ -18,6 +18,7 @@ export class FirePageComponent implements OnDestroy, OnInit {
public accountClusterRiskRules: PortfolioReportRule[]; public accountClusterRiskRules: PortfolioReportRule[];
public currencyClusterRiskRules: PortfolioReportRule[]; public currencyClusterRiskRules: PortfolioReportRule[];
public deviceType: string; public deviceType: string;
public emergencyFundRules: PortfolioReportRule[];
public feeRules: PortfolioReportRule[]; public feeRules: PortfolioReportRule[];
public fireWealth: Big; public fireWealth: Big;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
@ -67,6 +68,8 @@ export class FirePageComponent implements OnDestroy, OnInit {
portfolioReport.rules['accountClusterRisk'] || null; portfolioReport.rules['accountClusterRisk'] || null;
this.currencyClusterRiskRules = this.currencyClusterRiskRules =
portfolioReport.rules['currencyClusterRisk'] || null; portfolioReport.rules['currencyClusterRisk'] || null;
this.emergencyFundRules =
portfolioReport.rules['emergencyFund'] || null;
this.feeRules = portfolioReport.rules['fees'] || null; this.feeRules = portfolioReport.rules['fees'] || null;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();

25
apps/client/src/app/pages/portfolio/fire/fire-page.html

@ -96,8 +96,10 @@
<div class="col"> <div class="col">
<h2 class="h3 mb-3 text-center">X-ray</h2> <h2 class="h3 mb-3 text-center">X-ray</h2>
<p class="mb-4"> <p class="mb-4">
Ghostfolio X-ray uses static analysis to identify potential issues and <span i18n
risks in your portfolio. >Ghostfolio X-ray uses static analysis to identify potential issues
and risks in your portfolio.</span
>
<span class="d-none" <span class="d-none"
>It will be highly configurable in the future: activate / deactivate >It will be highly configurable in the future: activate / deactivate
rules and customize the thresholds to match your personal investment rules and customize the thresholds to match your personal investment
@ -106,7 +108,20 @@
</p> </p>
<div class="mb-4"> <div class="mb-4">
<h4 class="align-items-center d-flex m-0"> <h4 class="align-items-center d-flex m-0">
<span>Currency Cluster Risks</span <span i18n>Emergency Fund</span
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
></gf-premium-indicator>
</h4>
<gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
[rules]="emergencyFundRules"
></gf-rules>
</div>
<div class="mb-4">
<h4 class="align-items-center d-flex m-0">
<span i18n>Currency Cluster Risks</span
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
@ -119,7 +134,7 @@
</div> </div>
<div class="mb-4"> <div class="mb-4">
<h4 class="align-items-center d-flex m-0"> <h4 class="align-items-center d-flex m-0">
<span>Account Cluster Risks</span <span i18n>Account Cluster Risks</span
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
@ -132,7 +147,7 @@
</div> </div>
<div> <div>
<h4 class="align-items-center d-flex m-0"> <h4 class="align-items-center d-flex m-0">
<span>Fees</span <span i18n>Fees</span
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"

18
apps/client/src/app/pages/resources/personal-finance-tools/products.ts

@ -6,6 +6,7 @@ import { CopilotMoneyPageComponent } from './products/copilot-money-page.compone
import { DeltaPageComponent } from './products/delta-page.component'; import { DeltaPageComponent } from './products/delta-page.component';
import { DivvyDiaryPageComponent } from './products/divvydiary-page.component'; import { DivvyDiaryPageComponent } from './products/divvydiary-page.component';
import { ExirioPageComponent } from './products/exirio-page.component'; import { ExirioPageComponent } from './products/exirio-page.component';
import { FinaryPageComponent } from './products/finary-page.component';
import { FolisharePageComponent } from './products/folishare-page.component'; import { FolisharePageComponent } from './products/folishare-page.component';
import { GetquinPageComponent } from './products/getquin-page.component'; import { GetquinPageComponent } from './products/getquin-page.component';
import { GoSpatzPageComponent } from './products/gospatz-page.component'; import { GoSpatzPageComponent } from './products/gospatz-page.component';
@ -23,6 +24,7 @@ import { SeekingAlphaPageComponent } from './products/seeking-alpha-page.compone
import { SharesightPageComponent } from './products/sharesight-page.component'; import { SharesightPageComponent } from './products/sharesight-page.component';
import { SimplePortfolioPageComponent } from './products/simple-portfolio-page.component'; import { SimplePortfolioPageComponent } from './products/simple-portfolio-page.component';
import { SnowballAnalyticsPageComponent } from './products/snowball-analytics-page.component'; import { SnowballAnalyticsPageComponent } from './products/snowball-analytics-page.component';
import { StocklePageComponent } from './products/stockle-page.component';
import { StockMarketEyePageComponent } from './products/stockmarketeye-page.component'; import { StockMarketEyePageComponent } from './products/stockmarketeye-page.component';
import { SumioPageComponent } from './products/sumio-page.component'; import { SumioPageComponent } from './products/sumio-page.component';
import { UtlunaPageComponent } from './products/utluna-page.component'; import { UtlunaPageComponent } from './products/utluna-page.component';
@ -115,6 +117,15 @@ export const products: Product[] = [
pricingPerYear: '$100', pricingPerYear: '$100',
slogan: 'All your wealth, in one place.' slogan: 'All your wealth, in one place.'
}, },
{
component: FinaryPageComponent,
founded: 2020,
key: 'finary',
languages: ['Deutsch', 'English', 'Français'],
name: 'Finary',
origin: $localize`United States`,
slogan: 'Real-Time Portfolio Tracker & Stock Tracker'
},
{ {
component: FolisharePageComponent, component: FolisharePageComponent,
hasFreePlan: true, hasFreePlan: true,
@ -304,6 +315,13 @@ export const products: Product[] = [
pricingPerYear: '$80', pricingPerYear: '$80',
slogan: 'Simple and powerful portfolio tracker' slogan: 'Simple and powerful portfolio tracker'
}, },
{
component: StocklePageComponent,
key: 'stockle',
name: 'Stockle',
origin: $localize`Finland`,
slogan: 'Supercharge your investments tracking experience'
},
{ {
component: StockMarketEyePageComponent, component: StockMarketEyePageComponent,
founded: 2008, founded: 2008,

31
apps/client/src/app/pages/resources/personal-finance-tools/products/finary-page.component.ts

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-finary-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class FinaryPageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'finary';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

31
apps/client/src/app/pages/resources/personal-finance-tools/products/stockle-page.component.ts

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-stockle-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class StocklePageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'stockle';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

20
apps/client/src/app/pages/user-account/user-account-page-routing.module.ts

@ -1,5 +1,8 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { UserAccountAccessComponent } from '@ghostfolio/client/components/user-account-access/user-account-access.component';
import { UserAccountMembershipComponent } from '@ghostfolio/client/components/user-account-membership/user-account-membership.component';
import { UserAccountSettingsComponent } from '@ghostfolio/client/components/user-account-settings/user-account-settings.component';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { UserAccountPageComponent } from './user-account-page.component'; import { UserAccountPageComponent } from './user-account-page.component';
@ -7,6 +10,23 @@ import { UserAccountPageComponent } from './user-account-page.component';
const routes: Routes = [ const routes: Routes = [
{ {
canActivate: [AuthGuard], canActivate: [AuthGuard],
children: [
{
path: '',
component: UserAccountSettingsComponent,
title: $localize`Settings`
},
{
path: 'membership',
component: UserAccountMembershipComponent,
title: $localize`Membership`
},
{
path: 'access',
component: UserAccountAccessComponent,
title: $localize`Access`
}
],
component: UserAccountPageComponent, component: UserAccountPageComponent,
path: '', path: '',
title: $localize`My Ghostfolio` title: $localize`My Ghostfolio`

433
apps/client/src/app/pages/user-account/user-account-page.component.ts

@ -1,448 +1,63 @@
import { import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
ChangeDetectorRef,
Component,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
import { MatCheckbox, MatCheckboxChange } from '@angular/material/checkbox';
import { MatDialog } from '@angular/material/dialog';
import {
MatSnackBar,
MatSnackBarRef,
TextOnlySnackBar
} from '@angular/material/snack-bar';
import { ActivatedRoute, Router } from '@angular/router';
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { DataService } from '@ghostfolio/client/services/data.service';
import {
STAY_SIGNED_IN,
SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
import { downloadAsFile, getDateFormatString } from '@ghostfolio/common/helper';
import { Access, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { format, parseISO } from 'date-fns';
import { uniq } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { StripeService } from 'ngx-stripe'; import { Subject, takeUntil } from 'rxjs';
import { EMPTY, Subject } from 'rxjs';
import { catchError, switchMap, takeUntil } from 'rxjs/operators';
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog/create-or-update-access-dialog.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page has-tabs' },
selector: 'gf-user-account-page', selector: 'gf-user-account-page',
styleUrls: ['./user-account-page.scss'], styleUrls: ['./user-account-page.scss'],
templateUrl: './user-account-page.html' templateUrl: './user-account-page.html'
}) })
export class UserAccountPageComponent implements OnDestroy, OnInit { export class UserAccountPageComponent implements OnDestroy, OnInit {
@ViewChild('toggleSignInWithFingerprintEnabledElement')
signInWithFingerprintElement: MatCheckbox;
public accesses: Access[];
public appearancePlaceholder = $localize`Auto`;
public baseCurrency: string;
public coupon: number;
public couponId: string;
public currencies: string[] = [];
public defaultDateFormat: string;
public deviceType: string; public deviceType: string;
public hasPermissionForSubscription: boolean; public tabs: TabConfiguration[] = [];
public hasPermissionToCreateAccess: boolean;
public hasPermissionToDeleteAccess: boolean;
public hasPermissionToUpdateViewMode: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public language = document.documentElement.lang;
public locales = [
'de',
'de-CH',
'en-GB',
'en-US',
'es',
'fr',
'it',
'nl',
'pt',
'tr'
];
public price: number;
public priceId: string;
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
public trySubscriptionMail =
'mailto:hi@ghostfol.io?Subject=Ghostfolio Premium Trial&body=Hello%0D%0DI am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?%0D%0DKind regards';
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private userService: UserService
private snackBar: MatSnackBar,
private route: ActivatedRoute,
private router: Router,
private settingsStorageService: SettingsStorageService,
private stripeService: StripeService,
private userService: UserService,
public webAuthnService: WebAuthnService
) { ) {
const { baseCurrency, currencies, globalPermissions, subscriptions } =
this.dataService.fetchInfo();
this.baseCurrency = baseCurrency;
this.currencies = currencies;
this.hasPermissionForSubscription = hasPermission(
globalPermissions,
permissions.enableSubscription
);
this.hasPermissionToDeleteAccess = hasPermission(
globalPermissions,
permissions.deleteAccess
);
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
this.defaultDateFormat = getDateFormatString( this.tabs = [
this.user.settings.locale {
); iconName: 'cog-outline',
label: $localize`Settings`,
this.hasPermissionToCreateAccess = hasPermission( path: ['/account']
this.user.permissions, },
permissions.createAccess {
); iconName: 'diamond-outline',
label: $localize`Membership`,
this.hasPermissionToDeleteAccess = hasPermission( path: ['/account/membership'],
this.user.permissions, showCondition: !!this.user?.subscription
permissions.deleteAccess },
); {
iconName: 'share-social-outline',
this.hasPermissionToUpdateUserSettings = hasPermission( label: $localize`Access`,
this.user.permissions, path: ['/account', 'access']
permissions.updateUserSettings }
); ];
this.hasPermissionToUpdateViewMode = hasPermission(
this.user.permissions,
permissions.updateViewMode
);
this.locales.push(this.user.settings.locale);
this.locales = uniq(this.locales.sort());
this.coupon = subscriptions?.[this.user.subscription.offer]?.coupon;
this.couponId =
subscriptions?.[this.user.subscription.offer]?.couponId;
this.price = subscriptions?.[this.user.subscription.offer]?.price;
this.priceId = subscriptions?.[this.user.subscription.offer]?.priceId;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
}); });
this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (params['createDialog']) {
this.openCreateAccessDialog();
}
});
} }
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.update();
}
public onChangeUserSetting(aKey: string, aValue: string) {
this.dataService
.putUserSetting({ [aKey]: aValue })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
if (aKey === 'language') {
if (aValue) {
window.location.href = `../${aValue}/account`;
} else {
window.location.href = `../`;
}
}
});
});
}
public onCheckout() {
this.dataService
.createCheckoutSession({ couponId: this.couponId, priceId: this.priceId })
.pipe(
switchMap(({ sessionId }: { sessionId: string }) => {
return this.stripeService.redirectToCheckout({ sessionId });
}),
catchError((error) => {
alert(error.message);
throw error;
})
)
.subscribe((result) => {
if (result.error) {
alert(result.error.message);
}
});
}
public onDeleteAccess(aId: string) {
this.dataService
.deleteAccess(aId)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe({
next: () => {
this.update();
}
});
}
public onExperimentalFeaturesChange(aEvent: MatCheckboxChange) {
this.dataService
.putUserSetting({ isExperimentalFeatures: aEvent.checked })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
}
public onExport() {
this.dataService
.fetchExport()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => {
for (const activity of data.activities) {
delete activity.id;
}
downloadAsFile({
content: data,
fileName: `ghostfolio-export-${format(
parseISO(data.meta.date),
'yyyyMMddHHmm'
)}.json`,
format: 'json'
});
});
}
public onRedeemCoupon() {
let couponCode = prompt($localize`Please enter your coupon code:`);
couponCode = couponCode?.trim();
if (couponCode) {
this.dataService
.redeemCoupon(couponCode)
.pipe(
takeUntil(this.unsubscribeSubject),
catchError(() => {
this.snackBar.open(
'😞 ' + $localize`Could not redeem coupon code`,
undefined,
{
duration: 3000
}
);
return EMPTY;
})
)
.subscribe(() => {
this.snackBarRef = this.snackBar.open(
'✅ ' + $localize`Coupon code has been redeemed`,
$localize`Reload`,
{
duration: 3000
}
);
this.snackBarRef
.afterDismissed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
window.location.reload();
});
this.snackBarRef
.onAction()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
window.location.reload();
});
});
}
}
public onRestrictedViewChange(aEvent: MatCheckboxChange) {
this.dataService
.putUserSetting({ isRestrictedView: aEvent.checked })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
}
public onSignInWithFingerprintChange(aEvent: MatCheckboxChange) {
if (aEvent.checked) {
this.registerDevice();
} else {
const confirmation = confirm(
$localize`Do you really want to remove this sign in method?`
);
if (confirmation) {
this.deregisterDevice();
} else {
this.update();
}
}
}
public onViewModeChange(aEvent: MatCheckboxChange) {
this.dataService
.putUserSetting({ viewMode: aEvent.checked === true ? 'ZEN' : 'DEFAULT' })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
} }
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private openCreateAccessDialog(): void {
const dialogRef = this.dialog.open(CreateOrUpdateAccessDialog, {
data: {
access: {
alias: '',
type: 'PUBLIC'
}
},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data: any) => {
const access: CreateAccessDto = data?.access;
if (access) {
this.dataService
.postAccess({ alias: access.alias })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe({
next: () => {
this.update();
}
});
}
this.router.navigate(['.'], { relativeTo: this.route });
});
}
private deregisterDevice() {
this.webAuthnService
.deregister()
.pipe(
takeUntil(this.unsubscribeSubject),
catchError(() => {
this.update();
return EMPTY;
})
)
.subscribe(() => {
this.update();
});
}
private registerDevice() {
this.webAuthnService
.register()
.pipe(
takeUntil(this.unsubscribeSubject),
catchError(() => {
this.update();
return EMPTY;
})
)
.subscribe(() => {
this.settingsStorageService.removeSetting(STAY_SIGNED_IN);
this.update();
});
}
private update() {
this.dataService
.fetchAccesses()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.accesses = response;
if (this.signInWithFingerprintElement) {
this.signInWithFingerprintElement.checked =
this.webAuthnService.isEnabled() ?? false;
}
this.changeDetectorRef.markForCheck();
});
}
} }

338
apps/client/src/app/pages/user-account/user-account-page.html

@ -1,309 +1,29 @@
<div class="container"> <mat-tab-nav-panel #tabPanel class="flex-grow-1 overflow-auto">
<div class="row"> <router-outlet></router-outlet>
<div class="col"> </mat-tab-nav-panel>
<h2 class="h3 mb-3 text-center" i18n>Account</h2>
</div> <nav
</div> mat-align-tabs="center"
<div *ngIf="user?.settings" class="mb-5 row"> mat-tab-nav-bar
<div class="col"> [disablePagination]="true"
<mat-card appearance="outlined" class="mb-3"> [tabPanel]="tabPanel"
<mat-card-content> >
<div *ngIf="user?.subscription" class="d-flex py-1"> <ng-container *ngFor="let tab of tabs">
<div class="pr-1 w-50" i18n>Membership</div> <a
<div class="pl-1 w-50"> #rla="routerLinkActive"
<div class="align-items-center d-flex mb-1"> *ngIf="tab.showCondition !== false"
<a [routerLink]="routerLinkPricing" class="px-3"
>{{ user?.subscription?.type }}</a mat-tab-link
> routerLinkActive
<gf-premium-indicator [active]="rla.isActive"
*ngIf="user?.subscription?.type === 'Premium'" [routerLink]="tab.path"
class="ml-1" [routerLinkActiveOptions]="{ exact: true }"
></gf-premium-indicator> >
</div> <ion-icon
<div *ngIf="user?.subscription?.type === 'Premium'"> [name]="tab.iconName"
<ng-container i18n>Valid until</ng-container> {{ [size]="deviceType === 'mobile' ? 'large': 'small'"
user?.subscription?.expiresAt | date: defaultDateFormat }} ></ion-icon>
</div> <div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
<div *ngIf="user?.subscription?.type === 'Basic'"> </a>
<ng-container </ng-container>
*ngIf="hasPermissionForSubscription && hasPermissionToUpdateUserSettings" </nav>
>
<button
color="primary"
mat-flat-button
(click)="onCheckout()"
>
<ng-container
*ngIf="user.subscription.offer === 'default'"
i18n
>Upgrade</ng-container
>
<ng-container
*ngIf="user.subscription.offer === 'renewal'"
i18n
>Renew</ng-container
>
</button>
<div *ngIf="price" class="mt-1">
<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>
</div>
</ng-container>
<a
*ngIf="!user?.subscription?.expiresAt"
class="mr-2 my-2"
mat-stroked-button
[href]="trySubscriptionMail"
><span i18n>Try Premium</span>
<gf-premium-indicator
class="d-inline-block ml-1"
[enableLink]="false"
></gf-premium-indicator
></a>
<a
*ngIf="hasPermissionToUpdateUserSettings"
class="mr-2 my-2"
i18n
mat-stroked-button
[routerLink]=""
(click)="onRedeemCoupon()"
>Redeem Coupon</a
>
</div>
</div>
</div>
<div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50">
<div i18n>Presenter View</div>
<div class="hint-text text-muted" i18n>
Protection for sensitive information like absolute performances
and quantity values
</div>
</div>
<div class="pl-1 w-50">
<mat-checkbox
color="primary"
[checked]="user.settings.isRestrictedView"
[disabled]="!hasPermissionToUpdateUserSettings"
(change)="onRestrictedViewChange($event)"
></mat-checkbox>
</div>
</div>
<div class="d-flex mt-4 py-1">
<form #changeUserSettingsForm="ngForm" class="w-100">
<div class="d-flex mb-2">
<div class="align-items-center d-flex pt-1 pt-1 w-50">
<ng-container i18n>Base Currency</ng-container>
</div>
<div class="pl-1 w-50">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-select
name="baseCurrency"
[disabled]="!hasPermissionToUpdateUserSettings"
[value]="user.settings.baseCurrency"
(selectionChange)="onChangeUserSetting('baseCurrency', $event.value)"
>
<mat-option
*ngFor="let currency of currencies"
[value]="currency"
>{{ currency }}</mat-option
>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="align-items-center d-flex mb-2">
<div class="pr-1 w-50">
<div i18n>Language</div>
</div>
<div class="pl-1 w-50">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-select
name="language"
[disabled]="!hasPermissionToUpdateUserSettings"
[value]="language"
(selectionChange)="onChangeUserSetting('language', $event.value)"
>
<mat-option [value]="null"></mat-option>
<mat-option value="de">Deutsch</mat-option>
<mat-option value="en">English</mat-option>
<mat-option value="es"
>Español (<ng-container i18n>Community</ng-container
>)</mat-option
>
<mat-option value="fr"
>Français (<ng-container i18n>Community</ng-container
>)</mat-option
>
<mat-option value="it"
>Italiano (<ng-container i18n>Community</ng-container
>)</mat-option
>
<mat-option value="nl"
>Nederlands (<ng-container i18n>Community</ng-container
>)</mat-option
>
<mat-option value="pt"
>Português (<ng-container i18n>Community</ng-container
>)</mat-option
>
<mat-option value="tr"
>Türkçe (<ng-container i18n>Community</ng-container
>)</mat-option
>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="align-items-center d-flex mb-2">
<div class="pr-1 w-50">
<div i18n>Locale</div>
<div class="hint-text text-muted">
<ng-container i18n>Date and number format</ng-container>
</div>
</div>
<div class="pl-1 w-50">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-select
name="locale"
[disabled]="!hasPermissionToUpdateUserSettings"
[value]="user.settings.locale"
(selectionChange)="onChangeUserSetting('locale', $event.value)"
>
<mat-option [value]="null"></mat-option>
<mat-option
*ngFor="let locale of locales"
[value]="locale"
>{{ locale }}</mat-option
>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="d-flex">
<div class="align-items-center d-flex pr-1 pt-1 w-50">
<ng-container i18n>Appearance</ng-container>
</div>
<div class="pl-1 w-50">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-select
class="with-placeholder-as-option"
name="colorScheme"
[disabled]="!hasPermissionToUpdateUserSettings"
[placeholder]="appearancePlaceholder"
[value]="user?.settings?.colorScheme"
(selectionChange)="onChangeUserSetting('colorScheme', $event.value)"
>
<mat-option i18n [value]="null">Auto</mat-option>
<mat-option i18n value="LIGHT">Light</mat-option>
<mat-option i18n value="DARK">Dark</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</form>
</div>
<div class="d-flex mt-4 py-1">
<div class="pr-1 w-50">
<div i18n>Zen Mode</div>
<div class="hint-text text-muted" i18n>
Distraction-free experience for turbulent times
</div>
</div>
<div class="pl-1 w-50">
<mat-checkbox
color="primary"
[checked]="user.settings.viewMode === 'ZEN'"
[disabled]="!hasPermissionToUpdateViewMode"
(change)="onViewModeChange($event)"
></mat-checkbox>
</div>
</div>
<div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50">
<div i18n>Biometric Authentication</div>
<div class="hint-text text-muted" i18n>
Sign in with fingerprint
</div>
</div>
<div class="pl-1 w-50">
<mat-checkbox
#toggleSignInWithFingerprintEnabledElement
color="primary"
[disabled]="!hasPermissionToUpdateUserSettings"
(change)="onSignInWithFingerprintChange($event)"
></mat-checkbox>
</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
</div>
</div>
<div class="pl-1 w-50">
<mat-checkbox
color="primary"
[checked]="user.settings.isExperimentalFeatures"
[disabled]="!hasPermissionToUpdateUserSettings"
(change)="onExperimentalFeaturesChange($event)"
></mat-checkbox>
</div>
</div>
<div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50" i18n>User ID</div>
<div class="pl-1 text-monospace w-50">{{ user?.id }}</div>
</div>
<div class="align-items-center d-flex py-1">
<div class="pr-1 w-50"></div>
<div class="pl-1 text-monospace w-50">
<button color="primary" mat-flat-button (click)="onExport()">
<span i18n>Export Data</span>
</button>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
<div class="row">
<div class="col">
<h2 class="align-items-center d-flex h3 justify-content-center mb-3">
<span i18n>Granted Access</span>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
></gf-premium-indicator>
</h2>
<gf-access-table
[accesses]="accesses"
[hasPermissionToCreateAccess]="hasPermissionToCreateAccess"
[showActions]="hasPermissionToDeleteAccess"
(accessDeleted)="onDeleteAccess($event)"
></gf-access-table>
</div>
</div>
</div>

33
apps/client/src/app/pages/user-account/user-account-page.module.ts

@ -1,18 +1,10 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatTabsModule } from '@angular/material/tabs';
import { MatButtonModule } from '@angular/material/button'; import { GfUserAccountAccessModule } from '@ghostfolio/client/components/user-account-access/user-account-access.module';
import { MatCardModule } from '@angular/material/card'; import { GfUserAccountMembershipModule } from '@ghostfolio/client/components/user-account-membership/user-account-membership.module';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { GfUserAccountSettingsModule } from '@ghostfolio/client/components/user-account-settings/user-account-settings.module';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router';
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfValueModule } from '@ghostfolio/ui/value';
import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-dialog/create-or-update-access-dialog.module';
import { UserAccountPageRoutingModule } from './user-account-page-routing.module'; import { UserAccountPageRoutingModule } from './user-account-page-routing.module';
import { UserAccountPageComponent } from './user-account-page.component'; import { UserAccountPageComponent } from './user-account-page.component';
@ -20,19 +12,10 @@ import { UserAccountPageComponent } from './user-account-page.component';
declarations: [UserAccountPageComponent], declarations: [UserAccountPageComponent],
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, GfUserAccountAccessModule,
GfCreateOrUpdateAccessDialogModule, GfUserAccountMembershipModule,
GfPortfolioAccessTableModule, GfUserAccountSettingsModule,
GfPremiumIndicatorModule, MatTabsModule,
GfValueModule,
MatButtonModule,
MatCardModule,
MatCheckboxModule,
MatDialogModule,
MatFormFieldModule,
MatSelectModule,
ReactiveFormsModule,
RouterModule,
UserAccountPageRoutingModule UserAccountPageRoutingModule
] ]
}) })

12
apps/client/src/app/pages/user-account/user-account-page.scss

@ -1,15 +1,7 @@
@import 'apps/client/src/styles/ghostfolio-style';
:host { :host {
color: rgb(var(--dark-primary-text)); color: rgb(var(--dark-primary-text));
display: block;
gf-access-table {
overflow-x: auto;
}
.hint-text {
font-size: 90%;
line-height: 1.2;
}
} }
:host-context(.is-dark-theme) { :host-context(.is-dark-theme) {

BIN
apps/client/src/assets/fonts/Inter-Black.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Black.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-BlackItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-BlackItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Bold.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Bold.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-BoldItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-BoldItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraBold.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraBold.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraBoldItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraBoldItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraLight.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraLight.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraLightItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ExtraLightItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Italic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Italic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Light.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Light.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-LightItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-LightItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Medium.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Medium.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-MediumItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-MediumItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Regular.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Regular.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-SemiBold.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-SemiBold.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-SemiBoldItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-SemiBoldItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Thin.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-Thin.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ThinItalic.woff

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-ThinItalic.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-italic.var.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter-roman.var.woff2

Binary file not shown.

BIN
apps/client/src/assets/fonts/Inter.var.woff2

Binary file not shown.

226
apps/client/src/assets/fonts/inter.css

@ -0,0 +1,226 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100;
font-display: swap;
src:
url('Inter-Thin.woff2?v=3.19') format('woff2'),
url('Inter-Thin.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 100;
font-display: swap;
src:
url('Inter-ThinItalic.woff2?v=3.19') format('woff2'),
url('Inter-ThinItalic.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 200;
font-display: swap;
src:
url('Inter-ExtraLight.woff2?v=3.19') format('woff2'),
url('Inter-ExtraLight.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 200;
font-display: swap;
src:
url('Inter-ExtraLightItalic.woff2?v=3.19') format('woff2'),
url('Inter-ExtraLightItalic.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src:
url('Inter-Light.woff2?v=3.19') format('woff2'),
url('Inter-Light.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 300;
font-display: swap;
src:
url('Inter-LightItalic.woff2?v=3.19') format('woff2'),
url('Inter-LightItalic.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src:
url('Inter-Regular.woff2?v=3.19') format('woff2'),
url('Inter-Regular.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src:
url('Inter-Italic.woff2?v=3.19') format('woff2'),
url('Inter-Italic.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src:
url('Inter-Medium.woff2?v=3.19') format('woff2'),
url('Inter-Medium.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src:
url('Inter-MediumItalic.woff2?v=3.19') format('woff2'),
url('Inter-MediumItalic.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src:
url('Inter-SemiBold.woff2?v=3.19') format('woff2'),
url('Inter-SemiBold.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src:
url('Inter-SemiBoldItalic.woff2?v=3.19') format('woff2'),
url('Inter-SemiBoldItalic.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src:
url('Inter-Bold.woff2?v=3.19') format('woff2'),
url('Inter-Bold.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src:
url('Inter-BoldItalic.woff2?v=3.19') format('woff2'),
url('Inter-BoldItalic.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src:
url('Inter-ExtraBold.woff2?v=3.19') format('woff2'),
url('Inter-ExtraBold.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 800;
font-display: swap;
src:
url('Inter-ExtraBoldItalic.woff2?v=3.19') format('woff2'),
url('Inter-ExtraBoldItalic.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src:
url('Inter-Black.woff2?v=3.19') format('woff2'),
url('Inter-Black.woff?v=3.19') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 900;
font-display: swap;
src:
url('Inter-BlackItalic.woff2?v=3.19') format('woff2'),
url('Inter-BlackItalic.woff?v=3.19') format('woff');
}
/* -------------------------------------------------------
Variable font.
Usage:
html { font-family: 'Inter', sans-serif; }
@supports (font-variation-settings: normal) {
html { font-family: 'Inter var', sans-serif; }
}
*/
@font-face {
font-family: 'Inter var';
font-weight: 100 900;
font-display: swap;
font-style: normal;
font-named-instance: 'Regular';
src: url('Inter-roman.var.woff2?v=3.19') format('woff2');
}
@font-face {
font-family: 'Inter var';
font-weight: 100 900;
font-display: swap;
font-style: italic;
font-named-instance: 'Italic';
src: url('Inter-italic.var.woff2?v=3.19') format('woff2');
}
/* --------------------------------------------------------------------------
[EXPERIMENTAL] Multi-axis, single variable font.
Slant axis is not yet widely supported (as of February 2019) and thus this
multi-axis single variable font is opt-in rather than the default.
When using this, you will probably need to set font-variation-settings
explicitly, e.g.
* { font-variation-settings: "slnt" 0deg }
.italic { font-variation-settings: "slnt" 10deg }
*/
@font-face {
font-family: 'Inter var experimental';
font-weight: 100 900;
font-display: swap;
font-style: oblique 0deg 10deg;
src: url('Inter.var.woff2?v=3.19') format('woff2');
}

40
apps/client/src/locales/messages.de.xlf

@ -1274,6 +1274,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context>
<context context-type="linenumber">89</context> <context context-type="linenumber">89</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">150</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="8cce9d03787606e0052d19c2ae7e7fa5ff785e94" datatype="html"> <trans-unit id="8cce9d03787606e0052d19c2ae7e7fa5ff785e94" datatype="html">
<source>Buying Power</source> <source>Buying Power</source>
@ -1590,6 +1594,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context>
<context context-type="linenumber">273</context> <context context-type="linenumber">273</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.html</context>
<context context-type="linenumber">181</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context>
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
@ -3734,6 +3742,10 @@
<context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context>
<context context-type="linenumber">148</context> <context context-type="linenumber">148</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="a6f322a22d0ae0b95dd9c70d43ba2c92de7ed49b" datatype="html"> <trans-unit id="a6f322a22d0ae0b95dd9c70d43ba2c92de7ed49b" datatype="html">
<source>Portfolio Allocations</source> <source>Portfolio Allocations</source>
@ -8130,6 +8142,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context>
<context context-type="linenumber">14</context> <context context-type="linenumber">14</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component.ts</context>
<context context-type="linenumber">13</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context>
<context context-type="linenumber">25</context> <context context-type="linenumber">25</context>
@ -9927,6 +9943,30 @@
<context context-type="linenumber">312</context> <context context-type="linenumber">312</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="288b1a62b35a6fab0130723db4cfa06e433922eb" datatype="html">
<source>Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio.</source>
<target state="translated">Ghostfolio X-ray nutzt statische Analysen, um potenzielle Probleme und Risiken in deinem Portfolio zu identifizieren.</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">100,101</context>
</context-group>
</trans-unit>
<trans-unit id="50760c2140335b2324deaee120f40d9aa64b3238" datatype="html">
<source>Currency Cluster Risks</source>
<target state="translated">Währungsklumpenrisiken</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">111</context>
</context-group>
</trans-unit>
<trans-unit id="c7b797e5df289241021db16010efb6ac3c6cfb86" datatype="html">
<source>Account Cluster Risks</source>
<target state="translated">Kontoklumpenrisiken</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">124</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

40
apps/client/src/locales/messages.es.xlf

@ -1272,6 +1272,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context>
<context context-type="linenumber">89</context> <context context-type="linenumber">89</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">150</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="8cce9d03787606e0052d19c2ae7e7fa5ff785e94" datatype="html"> <trans-unit id="8cce9d03787606e0052d19c2ae7e7fa5ff785e94" datatype="html">
<source>Buying Power</source> <source>Buying Power</source>
@ -1588,6 +1592,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context>
<context context-type="linenumber">273</context> <context context-type="linenumber">273</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.html</context>
<context context-type="linenumber">181</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context>
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
@ -3732,6 +3740,10 @@
<context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context>
<context context-type="linenumber">148</context> <context context-type="linenumber">148</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="a6f322a22d0ae0b95dd9c70d43ba2c92de7ed49b" datatype="html"> <trans-unit id="a6f322a22d0ae0b95dd9c70d43ba2c92de7ed49b" datatype="html">
<source>Portfolio Allocations</source> <source>Portfolio Allocations</source>
@ -8128,6 +8140,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context>
<context context-type="linenumber">14</context> <context context-type="linenumber">14</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component.ts</context>
<context context-type="linenumber">13</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context>
<context context-type="linenumber">25</context> <context context-type="linenumber">25</context>
@ -9925,6 +9941,30 @@
<context context-type="linenumber">312</context> <context context-type="linenumber">312</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="288b1a62b35a6fab0130723db4cfa06e433922eb" datatype="html">
<source>Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio.</source>
<target state="new">Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio.</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">100,101</context>
</context-group>
</trans-unit>
<trans-unit id="50760c2140335b2324deaee120f40d9aa64b3238" datatype="html">
<source>Currency Cluster Risks</source>
<target state="new">Currency Cluster Risks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">111</context>
</context-group>
</trans-unit>
<trans-unit id="c7b797e5df289241021db16010efb6ac3c6cfb86" datatype="html">
<source>Account Cluster Risks</source>
<target state="new">Account Cluster Risks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">124</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

40
apps/client/src/locales/messages.fr.xlf

@ -1603,6 +1603,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context>
<context context-type="linenumber">89</context> <context context-type="linenumber">89</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">150</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="8cce9d03787606e0052d19c2ae7e7fa5ff785e94" datatype="html"> <trans-unit id="8cce9d03787606e0052d19c2ae7e7fa5ff785e94" datatype="html">
<source>Buying Power</source> <source>Buying Power</source>
@ -2339,6 +2343,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context>
<context context-type="linenumber">273</context> <context context-type="linenumber">273</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.html</context>
<context context-type="linenumber">181</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context>
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
@ -3731,6 +3739,10 @@
<context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context>
<context context-type="linenumber">148</context> <context context-type="linenumber">148</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="a6f322a22d0ae0b95dd9c70d43ba2c92de7ed49b" datatype="html"> <trans-unit id="a6f322a22d0ae0b95dd9c70d43ba2c92de7ed49b" datatype="html">
<source>Portfolio Allocations</source> <source>Portfolio Allocations</source>
@ -8127,6 +8139,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context>
<context context-type="linenumber">14</context> <context context-type="linenumber">14</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component.ts</context>
<context context-type="linenumber">13</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context>
<context context-type="linenumber">25</context> <context context-type="linenumber">25</context>
@ -9924,6 +9940,30 @@
<context context-type="linenumber">312</context> <context context-type="linenumber">312</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="288b1a62b35a6fab0130723db4cfa06e433922eb" datatype="html">
<source>Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio.</source>
<target state="new">Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio.</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">100,101</context>
</context-group>
</trans-unit>
<trans-unit id="50760c2140335b2324deaee120f40d9aa64b3238" datatype="html">
<source>Currency Cluster Risks</source>
<target state="new">Currency Cluster Risks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">111</context>
</context-group>
</trans-unit>
<trans-unit id="c7b797e5df289241021db16010efb6ac3c6cfb86" datatype="html">
<source>Account Cluster Risks</source>
<target state="new">Account Cluster Risks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">124</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

40
apps/client/src/locales/messages.it.xlf

@ -1272,6 +1272,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context>
<context context-type="linenumber">89</context> <context context-type="linenumber">89</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">150</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="8cce9d03787606e0052d19c2ae7e7fa5ff785e94" datatype="html"> <trans-unit id="8cce9d03787606e0052d19c2ae7e7fa5ff785e94" datatype="html">
<source>Buying Power</source> <source>Buying Power</source>
@ -1588,6 +1592,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context>
<context context-type="linenumber">273</context> <context context-type="linenumber">273</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.html</context>
<context context-type="linenumber">181</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context>
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
@ -3732,6 +3740,10 @@
<context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context>
<context context-type="linenumber">148</context> <context context-type="linenumber">148</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="a6f322a22d0ae0b95dd9c70d43ba2c92de7ed49b" datatype="html"> <trans-unit id="a6f322a22d0ae0b95dd9c70d43ba2c92de7ed49b" datatype="html">
<source>Portfolio Allocations</source> <source>Portfolio Allocations</source>
@ -8128,6 +8140,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context>
<context context-type="linenumber">14</context> <context context-type="linenumber">14</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component.ts</context>
<context context-type="linenumber">13</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context>
<context context-type="linenumber">25</context> <context context-type="linenumber">25</context>
@ -9925,6 +9941,30 @@
<context context-type="linenumber">312</context> <context context-type="linenumber">312</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="288b1a62b35a6fab0130723db4cfa06e433922eb" datatype="html">
<source>Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio.</source>
<target state="new">Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio.</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">100,101</context>
</context-group>
</trans-unit>
<trans-unit id="50760c2140335b2324deaee120f40d9aa64b3238" datatype="html">
<source>Currency Cluster Risks</source>
<target state="new">Currency Cluster Risks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">111</context>
</context-group>
</trans-unit>
<trans-unit id="c7b797e5df289241021db16010efb6ac3c6cfb86" datatype="html">
<source>Account Cluster Risks</source>
<target state="new">Account Cluster Risks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">124</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

40
apps/client/src/locales/messages.nl.xlf

@ -1271,6 +1271,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context>
<context context-type="linenumber">89</context> <context context-type="linenumber">89</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">150</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="8cce9d03787606e0052d19c2ae7e7fa5ff785e94" datatype="html"> <trans-unit id="8cce9d03787606e0052d19c2ae7e7fa5ff785e94" datatype="html">
<source>Buying Power</source> <source>Buying Power</source>
@ -1587,6 +1591,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context>
<context context-type="linenumber">273</context> <context context-type="linenumber">273</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.html</context>
<context context-type="linenumber">181</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context>
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
@ -3731,6 +3739,10 @@
<context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context>
<context context-type="linenumber">148</context> <context context-type="linenumber">148</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="a6f322a22d0ae0b95dd9c70d43ba2c92de7ed49b" datatype="html"> <trans-unit id="a6f322a22d0ae0b95dd9c70d43ba2c92de7ed49b" datatype="html">
<source>Portfolio Allocations</source> <source>Portfolio Allocations</source>
@ -8127,6 +8139,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context>
<context context-type="linenumber">14</context> <context context-type="linenumber">14</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component.ts</context>
<context context-type="linenumber">13</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context>
<context context-type="linenumber">25</context> <context context-type="linenumber">25</context>
@ -9924,6 +9940,30 @@
<context context-type="linenumber">312</context> <context context-type="linenumber">312</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="288b1a62b35a6fab0130723db4cfa06e433922eb" datatype="html">
<source>Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio.</source>
<target state="new">Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio.</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">100,101</context>
</context-group>
</trans-unit>
<trans-unit id="50760c2140335b2324deaee120f40d9aa64b3238" datatype="html">
<source>Currency Cluster Risks</source>
<target state="new">Currency Cluster Risks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">111</context>
</context-group>
</trans-unit>
<trans-unit id="c7b797e5df289241021db16010efb6ac3c6cfb86" datatype="html">
<source>Account Cluster Risks</source>
<target state="new">Account Cluster Risks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">124</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

40
apps/client/src/locales/messages.pt.xlf

@ -1499,6 +1499,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context>
<context context-type="linenumber">89</context> <context context-type="linenumber">89</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">150</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="8cce9d03787606e0052d19c2ae7e7fa5ff785e94" datatype="html"> <trans-unit id="8cce9d03787606e0052d19c2ae7e7fa5ff785e94" datatype="html">
<source>Buying Power</source> <source>Buying Power</source>
@ -2263,6 +2267,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context>
<context context-type="linenumber">273</context> <context context-type="linenumber">273</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.html</context>
<context context-type="linenumber">181</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context>
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
@ -3731,6 +3739,10 @@
<context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context>
<context context-type="linenumber">148</context> <context context-type="linenumber">148</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="a6f322a22d0ae0b95dd9c70d43ba2c92de7ed49b" datatype="html"> <trans-unit id="a6f322a22d0ae0b95dd9c70d43ba2c92de7ed49b" datatype="html">
<source>Portfolio Allocations</source> <source>Portfolio Allocations</source>
@ -8127,6 +8139,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context>
<context context-type="linenumber">14</context> <context context-type="linenumber">14</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component.ts</context>
<context context-type="linenumber">13</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context>
<context context-type="linenumber">25</context> <context context-type="linenumber">25</context>
@ -9924,6 +9940,30 @@
<context context-type="linenumber">312</context> <context context-type="linenumber">312</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="288b1a62b35a6fab0130723db4cfa06e433922eb" datatype="html">
<source>Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio.</source>
<target state="new">Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio.</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">100,101</context>
</context-group>
</trans-unit>
<trans-unit id="50760c2140335b2324deaee120f40d9aa64b3238" datatype="html">
<source>Currency Cluster Risks</source>
<target state="new">Currency Cluster Risks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">111</context>
</context-group>
</trans-unit>
<trans-unit id="c7b797e5df289241021db16010efb6ac3c6cfb86" datatype="html">
<source>Account Cluster Risks</source>
<target state="new">Account Cluster Risks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">124</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

40
apps/client/src/locales/messages.tr.xlf

@ -64,6 +64,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context>
<context context-type="linenumber">14</context> <context context-type="linenumber">14</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component.ts</context>
<context context-type="linenumber">13</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context>
<context context-type="linenumber">25</context> <context context-type="linenumber">25</context>
@ -804,6 +808,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context>
<context context-type="linenumber">273</context> <context context-type="linenumber">273</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.html</context>
<context context-type="linenumber">181</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context>
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
@ -2671,6 +2679,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context>
<context context-type="linenumber">89</context> <context context-type="linenumber">89</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">150</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="311828ec96f91f6f530219b07d5531a8aa507d64" datatype="html"> <trans-unit id="311828ec96f91f6f530219b07d5531a8aa507d64" datatype="html">
<source>Cash</source> <source>Cash</source>
@ -2823,6 +2835,10 @@
<context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context>
<context context-type="linenumber">148</context> <context context-type="linenumber">148</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="bf3df1f4eb29a071630eed167406c06f974480b2" datatype="html"> <trans-unit id="bf3df1f4eb29a071630eed167406c06f974480b2" datatype="html">
<source>First Buy Date</source> <source>First Buy Date</source>
@ -9924,6 +9940,30 @@
<context context-type="linenumber">312</context> <context context-type="linenumber">312</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="288b1a62b35a6fab0130723db4cfa06e433922eb" datatype="html">
<source>Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio.</source>
<target state="new">Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio.</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">100,101</context>
</context-group>
</trans-unit>
<trans-unit id="50760c2140335b2324deaee120f40d9aa64b3238" datatype="html">
<source>Currency Cluster Risks</source>
<target state="new">Currency Cluster Risks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">111</context>
</context-group>
</trans-unit>
<trans-unit id="c7b797e5df289241021db16010efb6ac3c6cfb86" datatype="html">
<source>Account Cluster Risks</source>
<target state="new">Account Cluster Risks</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">124</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

37
apps/client/src/locales/messages.xlf

@ -64,6 +64,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts</context>
<context context-type="linenumber">14</context> <context context-type="linenumber">14</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component.ts</context>
<context context-type="linenumber">13</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.component.ts</context>
<context context-type="linenumber">25</context> <context context-type="linenumber">25</context>
@ -791,6 +795,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html</context>
<context context-type="linenumber">273</context> <context context-type="linenumber">273</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.html</context>
<context context-type="linenumber">181</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/blog/blog-page.html</context>
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
@ -2504,6 +2512,10 @@
<context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context>
<context context-type="linenumber">89</context> <context context-type="linenumber">89</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">150</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="311828ec96f91f6f530219b07d5531a8aa507d64" datatype="html"> <trans-unit id="311828ec96f91f6f530219b07d5531a8aa507d64" datatype="html">
<source>Cash</source> <source>Cash</source>
@ -2641,6 +2653,10 @@
<context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html</context>
<context context-type="linenumber">148</context> <context context-type="linenumber">148</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="bf3df1f4eb29a071630eed167406c06f974480b2" datatype="html"> <trans-unit id="bf3df1f4eb29a071630eed167406c06f974480b2" datatype="html">
<source>First Buy Date</source> <source>First Buy Date</source>
@ -9370,6 +9386,27 @@
<context context-type="linenumber">11,13</context> <context context-type="linenumber">11,13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="288b1a62b35a6fab0130723db4cfa06e433922eb" datatype="html">
<source>Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio.</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">100,101</context>
</context-group>
</trans-unit>
<trans-unit id="50760c2140335b2324deaee120f40d9aa64b3238" datatype="html">
<source>Currency Cluster Risks</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">111</context>
</context-group>
</trans-unit>
<trans-unit id="c7b797e5df289241021db16010efb6ac3c6cfb86" datatype="html">
<source>Account Cluster Risks</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">124</context>
</context-group>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

2
apps/client/src/styles.scss

@ -5,7 +5,7 @@
:root { :root {
--dark-background: rgb(25, 25, 25); --dark-background: rgb(25, 25, 25);
--font-family-sans-serif: Roboto, 'Helvetica Neue', sans-serif; --font-family-sans-serif: 'Inter', Roboto, 'Helvetica Neue', sans-serif;
--light-background: rgb(255, 255, 255); --light-background: rgb(255, 255, 255);
--dark-primary-text: 0, 0, 0, 0.87; --dark-primary-text: 0, 0, 0, 0.87;

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.6.0", "version": "2.7.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",

Loading…
Cancel
Save