Browse Source

Merge remote-tracking branch 'upstream/main' into localize-x-ray-rule

merge upstream/main
pull/4835/head
tobikugel 3 months ago
parent
commit
3e044f467c
  1. 2
      .github/workflows/build-code.yml
  2. 2
      .github/workflows/docker-image.yml
  3. 2
      .nvmrc
  4. 53
      CHANGELOG.md
  5. 2
      DEVELOPMENT.md
  6. 4
      Dockerfile
  7. 70
      apps/api/src/app/admin/admin.controller.ts
  8. 5
      apps/api/src/app/admin/admin.service.ts
  9. 2
      apps/api/src/app/app.module.ts
  10. 173
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts
  11. 16
      apps/api/src/app/portfolio/portfolio.service.ts
  12. 4
      apps/api/src/app/redis-cache/redis-cache.service.ts
  13. 4
      apps/api/src/assets/sitemap.xml
  14. 4
      apps/api/src/helper/object.helper.spec.ts
  15. 11
      apps/api/src/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor.ts
  16. 21
      apps/api/src/services/data-provider/data-provider.service.ts
  17. 17
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  18. 44
      apps/client/src/app/app-routing.module.ts
  19. 4
      apps/client/src/app/app.component.html
  20. 35
      apps/client/src/app/app.component.ts
  21. 3
      apps/client/src/app/components/access-table/access-table.component.ts
  22. 5
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  23. 17
      apps/client/src/app/components/admin-settings/admin-settings.component.ts
  24. 2
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html
  25. 5
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts
  26. 42
      apps/client/src/app/components/header/header.component.html
  27. 28
      apps/client/src/app/components/header/header.component.ts
  28. 5
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  29. 5
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  30. 2
      apps/client/src/app/components/home-holdings/home-holdings.html
  31. 8
      apps/client/src/app/components/home-market/home-market.html
  32. 7
      apps/client/src/app/components/home-overview/home-overview.component.ts
  33. 14
      apps/client/src/app/components/home-overview/home-overview.html
  34. 6
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  35. 4
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts
  36. 3
      apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
  37. 37
      apps/client/src/app/core/auth.guard.ts
  38. 5
      apps/client/src/app/core/http-response.interceptor.ts
  39. 12
      apps/client/src/app/core/paths.ts
  40. 6
      apps/client/src/app/pages/about/about-page-routing.module.ts
  41. 13
      apps/client/src/app/pages/about/about-page.component.ts
  42. 7
      apps/client/src/app/pages/about/overview/about-overview-page.component.ts
  43. 6
      apps/client/src/app/pages/about/overview/about-overview-page.html
  44. 9
      apps/client/src/app/pages/admin/admin-page-routing.module.ts
  45. 11
      apps/client/src/app/pages/admin/admin-page.component.ts
  46. 7
      apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts
  47. 2
      apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.html
  48. 7
      apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.component.ts
  49. 2
      apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.html
  50. 5
      apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts
  51. 2
      apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.html
  52. 6
      apps/client/src/app/pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.component.ts
  53. 2
      apps/client/src/app/pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.html
  54. 5
      apps/client/src/app/pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.component.ts
  55. 2
      apps/client/src/app/pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.html
  56. 7
      apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.component.ts
  57. 2
      apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.html
  58. 6
      apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.component.ts
  59. 2
      apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.html
  60. 6
      apps/client/src/app/pages/blog/2022/11/black-friday-2022/black-friday-2022-page.component.ts
  61. 2
      apps/client/src/app/pages/blog/2022/11/black-friday-2022/black-friday-2022-page.html
  62. 6
      apps/client/src/app/pages/blog/2022/12/the-importance-of-tracking-your-personal-finances/the-importance-of-tracking-your-personal-finances-page.component.ts
  63. 2
      apps/client/src/app/pages/blog/2022/12/the-importance-of-tracking-your-personal-finances/the-importance-of-tracking-your-personal-finances-page.html
  64. 6
      apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.component.ts
  65. 2
      apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.html
  66. 6
      apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.component.ts
  67. 2
      apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.html
  68. 7
      apps/client/src/app/pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.component.ts
  69. 2
      apps/client/src/app/pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.html
  70. 7
      apps/client/src/app/pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.component.ts
  71. 2
      apps/client/src/app/pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.html
  72. 5
      apps/client/src/app/pages/blog/2023/07/exploring-the-path-to-fire/exploring-the-path-to-fire-page.component.ts
  73. 2
      apps/client/src/app/pages/blog/2023/07/exploring-the-path-to-fire/exploring-the-path-to-fire-page.html
  74. 8
      apps/client/src/app/pages/blog/2023/08/ghostfolio-joins-oss-friends/ghostfolio-joins-oss-friends-page.component.ts
  75. 2
      apps/client/src/app/pages/blog/2023/08/ghostfolio-joins-oss-friends/ghostfolio-joins-oss-friends-page.html
  76. 14
      apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts
  77. 2
      apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html
  78. 6
      apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component.ts
  79. 12
      apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.html
  80. 6
      apps/client/src/app/pages/blog/2023/11/black-week-2023/black-week-2023-page.component.ts
  81. 2
      apps/client/src/app/pages/blog/2023/11/black-week-2023/black-week-2023-page.html
  82. 7
      apps/client/src/app/pages/blog/2023/11/hacktoberfest-2023-debriefing/hacktoberfest-2023-debriefing-page.component.ts
  83. 2
      apps/client/src/app/pages/blog/2023/11/hacktoberfest-2023-debriefing/hacktoberfest-2023-debriefing-page.html
  84. 6
      apps/client/src/app/pages/blog/2024/09/hacktoberfest-2024/hacktoberfest-2024-page.component.ts
  85. 12
      apps/client/src/app/pages/blog/2024/09/hacktoberfest-2024/hacktoberfest-2024-page.html
  86. 6
      apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.component.ts
  87. 2
      apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.html
  88. 5
      apps/client/src/app/pages/faq/faq-page-routing.module.ts
  89. 7
      apps/client/src/app/pages/faq/faq-page.component.ts
  90. 7
      apps/client/src/app/pages/faq/overview/faq-overview-page.component.ts
  91. 11
      apps/client/src/app/pages/faq/saas/saas-page.component.ts
  92. 4
      apps/client/src/app/pages/faq/saas/saas-page.html
  93. 6
      apps/client/src/app/pages/faq/self-hosting/self-hosting-page.component.ts
  94. 5
      apps/client/src/app/pages/features/features-page.component.ts
  95. 14
      apps/client/src/app/pages/home/home-page-routing.module.ts
  96. 11
      apps/client/src/app/pages/home/home-page.component.ts
  97. 7
      apps/client/src/app/pages/landing/landing-page.component.ts
  98. 12
      apps/client/src/app/pages/landing/landing-page.html
  99. 12
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  100. 4
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

2
.github/workflows/build-code.yml

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node_version: node_version:
- 20 - 22
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4

2
.github/workflows/docker-image.yml

@ -19,7 +19,7 @@ jobs:
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4
with: with:
images: ghostfolio/ghostfolio images: ${{ vars.DOCKER_REPOSITORY || 'ghostfolio/ghostfolio' }}
tags: | tags: |
type=semver,pattern={{major}} type=semver,pattern={{major}}
type=semver,pattern={{version}} type=semver,pattern={{version}}

2
.nvmrc

@ -1 +1 @@
v20 v22

53
CHANGELOG.md

@ -5,30 +5,63 @@ 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.164.0 - 2025-05-28
### Changed
- Improved the language localization for Dutch (`nl`)
- Improved the language localization for French (`fr`)
- Improved the language localization for Polish (`pl`)
- Improved the language localization for Spanish (`es`)
- Upgraded `Node.js` from version `20` to `22` (`Dockerfile`)
- Upgraded `yahoo-finance2` from version `3.3.4` to `3.3.5`
## 2.163.0 - 2025-05-26
### Changed
- Improved the language localization for Italian (`it`)
- Improved the language localization for Turkish (`tr`)
- Upgraded `yahoo-finance2` from version `3.3.3` to `3.3.4`
## 2.162.1 - 2025-05-24
### Added ### Added
- Added a hint about delayed market data to the markets overview
- Added the asset profile count per data provider to the endpoint `GET api/v1/admin` - Added the asset profile count per data provider to the endpoint `GET api/v1/admin`
### Changed ### Changed
- Increased the robustness of the search in the _Yahoo Finance_ service by catching schema validation errors
- Improved the symbol lookup results by removing the currency from the name of cryptocurrencies (experimental)
- Harmonized the data providers management style of the admin control panel - Harmonized the data providers management style of the admin control panel
- Extended the data providers management of the admin control panel by the asset profile count - Extended the data providers management of the admin control panel by the asset profile count
- Restricted the permissions of the demo user - Restricted the permissions of the demo user
- Renamed `Order` to `activities` in the `User` database schema - Renamed `Order` to `activities` in the `User` database schema
- Removed the deprecated endpoint `GET api/v1/admin/market-data/:dataSource/:symbol`
- Removed the deprecated endpoint `POST api/v1/admin/market-data/:dataSource/:symbol`
- Removed the deprecated endpoint `PUT api/v1/admin/market-data/:dataSource/:symbol/:dateString`
- Improved the language localization for Catalan (`ca`) - Improved the language localization for Catalan (`ca`)
- Improved the language localization for Chinese (`zh`) - Improved the language localization for Chinese (`zh`)
- Improved the language localization for Dutch (`nl`) - Improved the language localization for Dutch (`nl`)
- Improved the language localization for French (`fr`)
- Improved the language localization for German (`de`) - Improved the language localization for German (`de`)
- Improved the language localization for Italian (`it`) - Improved the language localization for Italian (`it`)
- Improved the language localization for Polish (`pl`)
- Improved the language localization for Portuguese (`pt`)
- Improved the language localization for Spanish (`es`)
- Upgraded `countup.js` from version `2.8.0` to `2.8.2` - Upgraded `countup.js` from version `2.8.0` to `2.8.2`
- Upgraded `nestjs` from version `10.4.15` to `11.0.12` - Upgraded `nestjs` from version `10.4.15` to `11.0.12`
- Upgraded `yahoo-finance2` from version `2.11.3` to `3.3.1` - Upgraded `prisma` from version `6.7.0` to `6.8.2`
- Upgraded `twitter-api-v2` from version `1.14.2` to `1.23.0`
- Upgraded `yahoo-finance2` from version `2.11.3` to `3.3.3`
### Fixed ### Fixed
- Displayed the button to fetch the current market price only if the activity is not in a custom currency
- Fixed an issue in the watchlist endpoint (`POST`) related to the `HasPermissionGuard` - Fixed an issue in the watchlist endpoint (`POST`) related to the `HasPermissionGuard`
- Improved the text alignment of the allocations by ETF holding on the allocations page (experimental)
## 2.161.0 - 2025-05-06 ## 2.161.0 - 2025-05-06
@ -74,7 +107,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the language localization for Français (`fr`) - Improved the language localization for French (`fr`)
- Upgraded `bootstrap` from version `4.6.0` to `4.6.2` - Upgraded `bootstrap` from version `4.6.0` to `4.6.2`
### Fixed ### Fixed
@ -115,7 +148,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the error message of the currency code validation - Improved the error message of the currency code validation
- Tightened the currency code validation by requiring uppercase letters - Tightened the currency code validation by requiring uppercase letters
- Respected the watcher count for the delete asset profiles checkbox in the historical market data table of the admin control panel - Respected the watcher count for the delete asset profiles checkbox in the historical market data table of the admin control panel
- Improved the language localization for Français (`fr`) - Improved the language localization for French (`fr`)
- Upgraded `ngx-skeleton-loader` from version `10.0.0` to `11.0.0` - Upgraded `ngx-skeleton-loader` from version `10.0.0` to `11.0.0`
- Upgraded `Nx` from version `20.8.0` to `20.8.1` - Upgraded `Nx` from version `20.8.0` to `20.8.1`
@ -215,7 +248,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the check for duplicates in the preview step of the activities import (allow different comments) - Improved the check for duplicates in the preview step of the activities import (allow different comments)
- Improved the language localization for Français (`fr`) - Improved the language localization for French (`fr`)
- Improved the language localization for German (`de`) - Improved the language localization for German (`de`)
- Improved the language localization for Polish (`pl`) - Improved the language localization for Polish (`pl`)
- Upgraded `ng-extract-i18n-merge` from version `2.14.1` to `2.14.3` - Upgraded `ng-extract-i18n-merge` from version `2.14.1` to `2.14.3`
@ -3670,7 +3703,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added the language localization for Français (`fr`) - Added the language localization for French (`fr`)
- Extended the landing page by a global heat map of subscribers - Extended the landing page by a global heat map of subscribers
- Added support for the thousand separator in the global heat map component - Added support for the thousand separator in the global heat map component
@ -3699,7 +3732,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added support for the dividend timeline grouped by year - Added support for the dividend timeline grouped by year
- Added support for the investment timeline grouped by year - Added support for the investment timeline grouped by year
- Set up the language localization for Français (`fr`) - Set up the language localization for French (`fr`)
### Changed ### Changed
@ -3808,7 +3841,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the value redaction interceptor (including `comment`) - Improved the value redaction interceptor (including `comment`)
- Improved the language localization for Español (`es`) - Improved the language localization for Spanish (`es`)
- Upgraded `cheerio` from version `1.0.0-rc.6` to `1.0.0-rc.12` - Upgraded `cheerio` from version `1.0.0-rc.6` to `1.0.0-rc.12`
- Upgraded `prisma` from version `4.6.1` to `4.7.1` - Upgraded `prisma` from version `4.6.1` to `4.7.1`
@ -4037,7 +4070,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the usage of the value component in the admin control panel - Improved the usage of the value component in the admin control panel
- Improved the language localization for Español (`es`) - Improved the language localization for Spanish (`es`)
### Fixed ### Fixed
@ -4059,7 +4092,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Set up the language localization for Español (`es`) - Set up the language localization for Spanish (`es`)
- Added support for sectors in mutual funds - Added support for sectors in mutual funds
## 1.198.0 - 25.09.2022 ## 1.198.0 - 25.09.2022

2
DEVELOPMENT.md

@ -5,7 +5,7 @@
### Prerequisites ### Prerequisites
- [Docker](https://www.docker.com/products/docker-desktop) - [Docker](https://www.docker.com/products/docker-desktop)
- [Node.js](https://nodejs.org/en/download) (version 20+) - [Node.js](https://nodejs.org/en/download) (version 22+)
- Create a local copy of this Git repository (clone) - Create a local copy of this Git repository (clone)
- Copy the file `.env.dev` to `.env` and populate it with your data (`cp .env.dev .env`) - Copy the file `.env.dev` to `.env` and populate it with your data (`cp .env.dev .env`)

4
Dockerfile

@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM node:20-slim AS builder FROM --platform=$BUILDPLATFORM node:22-slim AS builder
# Build application and add additional files # Build application and add additional files
WORKDIR /ghostfolio WORKDIR /ghostfolio
@ -50,7 +50,7 @@ COPY package.json /ghostfolio/dist/apps/api
RUN npm run database:generate-typings RUN npm run database:generate-typings
# Image to run, copy everything needed from builder # Image to run, copy everything needed from builder
FROM node:20-slim FROM node:22-slim
LABEL org.opencontainers.image.source="https://github.com/ghostfolio/ghostfolio" LABEL org.opencontainers.image.source="https://github.com/ghostfolio/ghostfolio"
ENV NODE_ENV=production ENV NODE_ENV=production

70
apps/api/src/app/admin/admin.controller.ts

@ -3,7 +3,6 @@ import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service'; import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto'; import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { import {
@ -16,7 +15,6 @@ import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { import {
AdminData, AdminData,
AdminMarketData, AdminMarketData,
AdminMarketDataDetails,
AdminUsers, AdminUsers,
EnhancedSymbolProfile EnhancedSymbolProfile
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -50,8 +48,6 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AdminService } from './admin.service'; import { AdminService } from './admin.service';
import { UpdateAssetProfileDto } from './update-asset-profile.dto'; import { UpdateAssetProfileDto } from './update-asset-profile.dto';
import { UpdateBulkMarketDataDto } from './update-bulk-market-data.dto';
import { UpdateMarketDataDto } from './update-market-data.dto';
@Controller('admin') @Controller('admin')
export class AdminController { export class AdminController {
@ -60,7 +56,6 @@ export class AdminController {
private readonly apiService: ApiService, private readonly apiService: ApiService,
private readonly dataGatheringService: DataGatheringService, private readonly dataGatheringService: DataGatheringService,
private readonly manualService: ManualService, private readonly manualService: ManualService,
private readonly marketDataService: MarketDataService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser
) {} ) {}
@ -214,19 +209,6 @@ export class AdminController {
}); });
} }
/**
* @deprecated
*/
@Get('market-data/:dataSource/:symbol')
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getMarketDataBySymbol(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<AdminMarketDataDetails> {
return this.adminService.getMarketDataBySymbol({ dataSource, symbol });
}
@HasPermission(permissions.accessAdminControl) @HasPermission(permissions.accessAdminControl)
@Post('market-data/:dataSource/:symbol/test') @Post('market-data/:dataSource/:symbol/test')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@ -253,58 +235,6 @@ export class AdminController {
} }
} }
/**
* @deprecated
*/
@HasPermission(permissions.accessAdminControl)
@Post('market-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async updateMarketData(
@Body() data: UpdateBulkMarketDataDto,
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
) {
const dataBulkUpdate: Prisma.MarketDataUpdateInput[] = data.marketData.map(
({ date, marketPrice }) => ({
dataSource,
marketPrice,
symbol,
date: parseISO(date),
state: 'CLOSE'
})
);
return this.marketDataService.updateMany({
data: dataBulkUpdate
});
}
/**
* @deprecated
*/
@HasPermission(permissions.accessAdminControl)
@Put('market-data/:dataSource/:symbol/:dateString')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async update(
@Param('dataSource') dataSource: DataSource,
@Param('dateString') dateString: string,
@Param('symbol') symbol: string,
@Body() data: UpdateMarketDataDto
) {
const date = parseISO(dateString);
return this.marketDataService.updateMarketData({
data: { marketPrice: data.marketPrice, state: 'CLOSE' },
where: {
dataSource_date_symbol: {
dataSource,
date,
symbol
}
}
});
}
@HasPermission(permissions.accessAdminControl) @HasPermission(permissions.accessAdminControl)
@Post('profile-data/:dataSource/:symbol') @Post('profile-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)

5
apps/api/src/app/admin/admin.service.ts

@ -135,7 +135,10 @@ export class AdminService {
} }
public async get({ user }: { user: UserWithSettings }): Promise<AdminData> { public async get({ user }: { user: UserWithSettings }): Promise<AdminData> {
const dataSources = await this.dataProviderService.getDataSources({ user }); const dataSources = await this.dataProviderService.getDataSources({
user,
includeGhostfolio: true
});
const [settings, transactionCount, userCount] = await Promise.all([ const [settings, transactionCount, userCount] = await Promise.all([
this.propertyService.get(), this.propertyService.get(),

2
apps/api/src/app/app.module.ts

@ -101,7 +101,7 @@ import { UserModule } from './user/user.module';
RedisCacheModule, RedisCacheModule,
ScheduleModule.forRoot(), ScheduleModule.forRoot(),
ServeStaticModule.forRoot({ ServeStaticModule.forRoot({
exclude: ['/api*', '/sitemap.xml'], exclude: ['/api/*wildcard', '/sitemap.xml'],
rootPath: join(__dirname, '..', 'client'), rootPath: join(__dirname, '..', 'client'),
serveStaticOptions: { serveStaticOptions: {
setHeaders: (res) => { setHeaders: (res) => {

173
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts

@ -74,48 +74,6 @@ export class GhostfolioController {
} }
} }
/**
* @deprecated
*/
@Get('dividends/:symbol')
@HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getDividendsV1(
@Param('symbol') symbol: string,
@Query() query: GetDividendsDto
): Promise<DividendsResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
try {
const dividends = await this.ghostfolioService.getDividends({
symbol,
from: parseDate(query.from),
granularity: query.granularity,
to: parseDate(query.to)
});
await this.ghostfolioService.incrementDailyRequests({
userId: this.request.user.id
});
return dividends;
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR
);
}
}
@Get('dividends/:symbol') @Get('dividends/:symbol')
@HasPermission(permissions.enableDataProviderGhostfolio) @HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('api-key'), HasPermissionGuard) @UseGuards(AuthGuard('api-key'), HasPermissionGuard)
@ -156,48 +114,6 @@ export class GhostfolioController {
} }
} }
/**
* @deprecated
*/
@Get('historical/:symbol')
@HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getHistoricalV1(
@Param('symbol') symbol: string,
@Query() query: GetHistoricalDto
): Promise<HistoricalResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
try {
const historicalData = await this.ghostfolioService.getHistorical({
symbol,
from: parseDate(query.from),
granularity: query.granularity,
to: parseDate(query.to)
});
await this.ghostfolioService.incrementDailyRequests({
userId: this.request.user.id
});
return historicalData;
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR
);
}
}
@Get('historical/:symbol') @Get('historical/:symbol')
@HasPermission(permissions.enableDataProviderGhostfolio) @HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('api-key'), HasPermissionGuard) @UseGuards(AuthGuard('api-key'), HasPermissionGuard)
@ -238,47 +154,6 @@ export class GhostfolioController {
} }
} }
/**
* @deprecated
*/
@Get('lookup')
@HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async lookupSymbolV1(
@Query('includeIndices') includeIndicesParam = 'false',
@Query('query') query = ''
): Promise<LookupResponse> {
const includeIndices = includeIndicesParam === 'true';
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
try {
const result = await this.ghostfolioService.lookup({
includeIndices,
query: query.toLowerCase()
});
await this.ghostfolioService.incrementDailyRequests({
userId: this.request.user.id
});
return result;
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR
);
}
}
@Get('lookup') @Get('lookup')
@HasPermission(permissions.enableDataProviderGhostfolio) @HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('api-key'), HasPermissionGuard) @UseGuards(AuthGuard('api-key'), HasPermissionGuard)
@ -320,44 +195,6 @@ export class GhostfolioController {
} }
} }
/**
* @deprecated
*/
@Get('quotes')
@HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getQuotesV1(
@Query() query: GetQuotesDto
): Promise<QuotesResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
try {
const quotes = await this.ghostfolioService.getQuotes({
symbols: query.symbols
});
await this.ghostfolioService.incrementDailyRequests({
userId: this.request.user.id
});
return quotes;
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR
);
}
}
@Get('quotes') @Get('quotes')
@HasPermission(permissions.enableDataProviderGhostfolio) @HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('api-key'), HasPermissionGuard) @UseGuards(AuthGuard('api-key'), HasPermissionGuard)
@ -394,16 +231,6 @@ export class GhostfolioController {
} }
} }
/**
* @deprecated
*/
@Get('status')
@HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getStatusV1(): Promise<DataProviderGhostfolioStatusResponse> {
return this.ghostfolioService.getStatus({ user: this.request.user });
}
@Get('status') @Get('status')
@HasPermission(permissions.enableDataProviderGhostfolio) @HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('api-key'), HasPermissionGuard) @UseGuards(AuthGuard('api-key'), HasPermissionGuard)

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

@ -1249,7 +1249,7 @@ export class PortfolioService {
const rules: PortfolioReportResponse['rules'] = { const rules: PortfolioReportResponse['rules'] = {
accountClusterRisk: accountClusterRisk:
summary.ordersCount > 0 summary.activityCount > 0
? await this.rulesService.evaluate( ? await this.rulesService.evaluate(
[ [
new AccountClusterRiskCurrentInvestment( new AccountClusterRiskCurrentInvestment(
@ -1265,7 +1265,7 @@ export class PortfolioService {
) )
: undefined, : undefined,
assetClassClusterRisk: assetClassClusterRisk:
summary.ordersCount > 0 summary.activityCount > 0
? await this.rulesService.evaluate( ? await this.rulesService.evaluate(
[ [
new AssetClassClusterRiskEquity( new AssetClassClusterRiskEquity(
@ -1281,7 +1281,7 @@ export class PortfolioService {
) )
: undefined, : undefined,
currencyClusterRisk: currencyClusterRisk:
summary.ordersCount > 0 summary.activityCount > 0
? await this.rulesService.evaluate( ? await this.rulesService.evaluate(
[ [
new CurrencyClusterRiskBaseCurrencyCurrentInvestment( new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
@ -1297,7 +1297,7 @@ export class PortfolioService {
) )
: undefined, : undefined,
economicMarketClusterRisk: economicMarketClusterRisk:
summary.ordersCount > 0 summary.activityCount > 0
? await this.rulesService.evaluate( ? await this.rulesService.evaluate(
[ [
new EconomicMarketClusterRiskDevelopedMarkets( new EconomicMarketClusterRiskDevelopedMarkets(
@ -1338,7 +1338,7 @@ export class PortfolioService {
userSettings userSettings
), ),
regionalMarketClusterRisk: regionalMarketClusterRisk:
summary.ordersCount > 0 summary.activityCount > 0
? await this.rulesService.evaluate( ? await this.rulesService.evaluate(
[ [
new RegionalMarketClusterRiskAsiaPacific( new RegionalMarketClusterRiskAsiaPacific(
@ -1981,6 +1981,9 @@ export class PortfolioService {
netPerformanceWithCurrencyEffect, netPerformanceWithCurrencyEffect,
totalBuy, totalBuy,
totalSell, totalSell,
activityCount: activities.filter(({ type }) => {
return ['BUY', 'SELL'].includes(type);
}).length,
committedFunds: committedFunds.toNumber(), committedFunds: committedFunds.toNumber(),
currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(), currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(),
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
@ -2008,9 +2011,6 @@ export class PortfolioService {
interest: interest.toNumber(), interest: interest.toNumber(),
items: valuables.toNumber(), items: valuables.toNumber(),
liabilities: liabilities.toNumber(), liabilities: liabilities.toNumber(),
ordersCount: activities.filter(({ type }) => {
return ['BUY', 'SELL'].includes(type);
}).length,
totalInvestment: totalInvestment.toNumber(), totalInvestment: totalInvestment.toNumber(),
totalValueInBaseCurrency: netWorth totalValueInBaseCurrency: netWorth
}; };

4
apps/api/src/app/redis-cache/redis-cache.service.ts

@ -80,7 +80,7 @@ export class RedisCacheService {
public async isHealthy() { public async isHealthy() {
try { try {
const isHealthy = await Promise.race([ await Promise.race([
this.getKeys(), this.getKeys(),
new Promise((_, reject) => new Promise((_, reject) =>
setTimeout( setTimeout(
@ -90,7 +90,7 @@ export class RedisCacheService {
) )
]); ]);
return isHealthy === 'PONG'; return true;
} catch (error) { } catch (error) {
return false; return false;
} }

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

@ -317,7 +317,7 @@
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/fr/a-propos/changelog</loc> <loc>https://ghostfol.io/fr/a-propos/journal-des-modifications</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
@ -383,7 +383,7 @@
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/it/informazioni-su/changelog</loc> <loc>https://ghostfol.io/it/informazioni-su/registro-delle-modifiche</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>

4
apps/api/src/helper/object.helper.spec.ts

@ -1515,6 +1515,7 @@ describe('redactAttributes', () => {
} }
}, },
summary: { summary: {
activityCount: 29,
annualizedPerformancePercent: 0.16690880197786, annualizedPerformancePercent: 0.16690880197786,
annualizedPerformancePercentWithCurrencyEffect: 0.1694019484552876, annualizedPerformancePercentWithCurrencyEffect: 0.1694019484552876,
cash: null, cash: null,
@ -1538,7 +1539,6 @@ describe('redactAttributes', () => {
interest: null, interest: null,
items: null, items: null,
liabilities: null, liabilities: null,
ordersCount: 29,
totalInvestment: null, totalInvestment: null,
totalValueInBaseCurrency: null, totalValueInBaseCurrency: null,
currentNetWorth: null currentNetWorth: null
@ -3018,6 +3018,7 @@ describe('redactAttributes', () => {
} }
}, },
summary: { summary: {
activityCount: 29,
annualizedPerformancePercent: 0.16690880197786, annualizedPerformancePercent: 0.16690880197786,
annualizedPerformancePercentWithCurrencyEffect: 0.1694019484552876, annualizedPerformancePercentWithCurrencyEffect: 0.1694019484552876,
cash: null, cash: null,
@ -3041,7 +3042,6 @@ describe('redactAttributes', () => {
interest: null, interest: null,
items: null, items: null,
liabilities: null, liabilities: null,
ordersCount: 29,
totalInvestment: null, totalInvestment: null,
totalValueInBaseCurrency: null, totalValueInBaseCurrency: null,
currentNetWorth: null currentNetWorth: null

11
apps/api/src/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor.ts

@ -43,7 +43,16 @@ export class TransformDataSourceInRequestInterceptor<T>
const dataSourceValue = request[type]?.dataSource; const dataSourceValue = request[type]?.dataSource;
if (dataSourceValue && !DataSource[dataSourceValue]) { if (dataSourceValue && !DataSource[dataSourceValue]) {
request[type].dataSource = decodeDataSource(dataSourceValue); // In Express 5, request.query is read-only, so request[type].dataSource cannot be directly modified
Object.defineProperty(request, type, {
configurable: true,
enumerable: true,
value: {
...request[type],
dataSource: decodeDataSource(dataSourceValue)
},
writable: true
});
} }
} }
} }

21
apps/api/src/services/data-provider/data-provider.service.ts

@ -163,8 +163,10 @@ export class DataProviderService {
} }
public async getDataSources({ public async getDataSources({
includeGhostfolio = false,
user user
}: { }: {
includeGhostfolio?: boolean;
user: UserWithSettings; user: UserWithSettings;
}): Promise<DataSource[]> { }): Promise<DataSource[]> {
let dataSourcesKey: 'DATA_SOURCES' | 'DATA_SOURCES_LEGACY' = 'DATA_SOURCES'; let dataSourcesKey: 'DATA_SOURCES' | 'DATA_SOURCES_LEGACY' = 'DATA_SOURCES';
@ -187,7 +189,7 @@ export class DataProviderService {
PROPERTY_API_KEY_GHOSTFOLIO PROPERTY_API_KEY_GHOSTFOLIO
)) as string; )) as string;
if (ghostfolioApiKey || hasRole(user, 'ADMIN')) { if (includeGhostfolio || ghostfolioApiKey) {
dataSources.push('GHOSTFOLIO'); dataSources.push('GHOSTFOLIO');
} }
@ -663,9 +665,6 @@ export class DataProviderService {
// Only allow symbols with supported currency // Only allow symbols with supported currency
return currency ? true : false; return currency ? true : false;
}) })
.sort(({ name: name1 }, { name: name2 }) => {
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
})
.map((lookupItem) => { .map((lookupItem) => {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
if (user.subscription.type === 'Premium') { if (user.subscription.type === 'Premium') {
@ -679,7 +678,21 @@ export class DataProviderService {
lookupItem.dataProviderInfo.isPremium = false; lookupItem.dataProviderInfo.isPremium = false;
} }
if (
lookupItem.assetSubClass === 'CRYPTOCURRENCY' &&
user?.Settings?.settings.isExperimentalFeatures
) {
// Remove DEFAULT_CURRENCY at the end of cryptocurrency names
lookupItem.name = lookupItem.name.replace(
new RegExp(` ${DEFAULT_CURRENCY}$`),
''
);
}
return lookupItem; return lookupItem;
})
.sort(({ name: name1 }, { name: name2 }) => {
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
}); });
return { return {

17
apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts

@ -30,8 +30,11 @@ import {
HistoricalDividendsResult, HistoricalDividendsResult,
HistoricalHistoryResult HistoricalHistoryResult
} from 'yahoo-finance2/esm/src/modules/historical'; } from 'yahoo-finance2/esm/src/modules/historical';
import { Quote } from 'yahoo-finance2/esm/src/modules/quote'; import {
import { SearchQuoteNonYahoo } from 'yahoo-finance2/script/src/modules/search'; Quote,
QuoteResponseArray
} from 'yahoo-finance2/esm/src/modules/quote';
import { SearchQuoteNonYahoo } from 'yahoo-finance2/esm/src/modules/search';
@Injectable() @Injectable()
export class YahooFinanceService implements DataProviderInterface { export class YahooFinanceService implements DataProviderInterface {
@ -281,11 +284,19 @@ export class YahooFinanceService implements DataProviderInterface {
return true; return true;
}); });
const marketData = await this.yahooFinance.quote( let marketData: QuoteResponseArray = [];
try {
marketData = await this.yahooFinance.quote(
quotes.map(({ symbol }) => { quotes.map(({ symbol }) => {
return symbol; return symbol;
}) })
); );
} catch (error) {
if (error?.result?.length > 0) {
marketData = error.result;
}
}
for (const marketDataItem of marketData) { for (const marketDataItem of marketData) {
const quote = quotes.find((currentQuote) => { const quote = quotes.find((currentQuote) => {

44
apps/client/src/app/app-routing.module.ts

@ -1,6 +1,6 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { paths } from '@ghostfolio/client/core/paths';
import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strategy'; import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strategy';
import { paths } from '@ghostfolio/common/paths';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes, TitleStrategy } from '@angular/router'; import { RouterModule, Routes, TitleStrategy } from '@angular/router';
@ -14,21 +14,21 @@ const routes: Routes = [
import('./pages/about/about-page.module').then((m) => m.AboutPageModule) import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
}, },
{ {
path: 'account', path: paths.account,
loadChildren: () => loadChildren: () =>
import('./pages/user-account/user-account-page.module').then( import('./pages/user-account/user-account-page.module').then(
(m) => m.UserAccountPageModule (m) => m.UserAccountPageModule
) )
}, },
{ {
path: 'accounts', path: paths.accounts,
loadChildren: () => loadChildren: () =>
import('./pages/accounts/accounts-page.module').then( import('./pages/accounts/accounts-page.module').then(
(m) => m.AccountsPageModule (m) => m.AccountsPageModule
) )
}, },
{ {
path: 'admin', path: paths.adminControl,
loadChildren: () => loadChildren: () =>
import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule) import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule)
}, },
@ -38,16 +38,16 @@ const routes: Routes = [
import('./pages/api/api-page.component').then( import('./pages/api/api-page.component').then(
(c) => c.GfApiPageComponent (c) => c.GfApiPageComponent
), ),
path: 'api', path: paths.api,
title: 'Ghostfolio API' title: 'Ghostfolio API'
}, },
{ {
path: 'auth', path: paths.auth,
loadChildren: () => loadChildren: () =>
import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule) import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule)
}, },
{ {
path: 'blog', path: paths.blog,
loadChildren: () => loadChildren: () =>
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule) import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
}, },
@ -57,7 +57,7 @@ const routes: Routes = [
import('./pages/demo/demo-page.component').then( import('./pages/demo/demo-page.component').then(
(c) => c.GfDemoPageComponent (c) => c.GfDemoPageComponent
), ),
path: 'demo' path: paths.demo
}, },
{ {
path: paths.faq, path: paths.faq,
@ -74,7 +74,7 @@ const routes: Routes = [
title: $localize`Features` title: $localize`Features`
}, },
{ {
path: 'home', path: paths.home,
loadChildren: () => loadChildren: () =>
import('./pages/home/home-page.module').then((m) => m.HomePageModule) import('./pages/home/home-page.module').then((m) => m.HomePageModule)
}, },
@ -84,7 +84,7 @@ const routes: Routes = [
import('./pages/i18n/i18n-page.component').then( import('./pages/i18n/i18n-page.component').then(
(c) => c.GfI18nPageComponent (c) => c.GfI18nPageComponent
), ),
path: 'i18n', path: paths.i18n,
title: $localize`Internationalization` title: $localize`Internationalization`
}, },
{ {
@ -95,19 +95,12 @@ const routes: Routes = [
) )
}, },
{ {
path: 'open', path: paths.openStartup,
loadChildren: () => loadChildren: () =>
import('./pages/open/open-page.module').then((m) => m.OpenPageModule) import('./pages/open/open-page.module').then((m) => m.OpenPageModule)
}, },
{ {
path: 'p', path: paths.portfolio,
loadChildren: () =>
import('./pages/public/public-page.module').then(
(m) => m.PublicPageModule
)
},
{
path: 'portfolio',
loadChildren: () => loadChildren: () =>
import('./pages/portfolio/portfolio-page.module').then( import('./pages/portfolio/portfolio-page.module').then(
(m) => m.PortfolioPageModule (m) => m.PortfolioPageModule
@ -120,6 +113,13 @@ const routes: Routes = [
(m) => m.PricingPageModule (m) => m.PricingPageModule
) )
}, },
{
path: paths.public,
loadChildren: () =>
import('./pages/public/public-page.module').then(
(m) => m.PublicPageModule
)
},
{ {
path: paths.register, path: paths.register,
loadChildren: () => loadChildren: () =>
@ -135,7 +135,7 @@ const routes: Routes = [
) )
}, },
{ {
path: 'start', path: paths.start,
loadChildren: () => loadChildren: () =>
import('./pages/landing/landing-page.module').then( import('./pages/landing/landing-page.module').then(
(m) => m.LandingPageModule (m) => m.LandingPageModule
@ -146,11 +146,11 @@ const routes: Routes = [
import('./pages/webauthn/webauthn-page.component').then( import('./pages/webauthn/webauthn-page.component').then(
(c) => c.GfWebauthnPageComponent (c) => c.GfWebauthnPageComponent
), ),
path: 'webauthn', path: paths.webauthn,
title: $localize`Sign in` title: $localize`Sign in`
}, },
{ {
path: 'zen', path: paths.zen,
loadChildren: () => loadChildren: () =>
import('./pages/zen/zen-page.module').then((m) => m.ZenPageModule) import('./pages/zen/zen-page.module').then((m) => m.ZenPageModule)
}, },

4
apps/client/src/app/app.component.html

@ -70,7 +70,7 @@
<li><a i18n [routerLink]="routerLinkAbout">About</a></li> <li><a i18n [routerLink]="routerLinkAbout">About</a></li>
@if (hasPermissionForSubscription) { @if (hasPermissionForSubscription) {
<li> <li>
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
} }
<li> <li>
@ -91,7 +91,7 @@
} }
@if (hasPermissionForStatistics) { @if (hasPermissionForStatistics) {
<li> <li>
<a [routerLink]="['/open']">Open Startup</a> <a [routerLink]="routerLinkOpenStartup">Open Startup</a>
</li> </li>
} }
@if (hasPermissionForSubscription) { @if (hasPermissionForSubscription) {

35
apps/client/src/app/app.component.ts

@ -2,6 +2,7 @@ import { GfHoldingDetailDialogComponent } from '@ghostfolio/client/components/ho
import { HoldingDetailDialogParams } from '@ghostfolio/client/components/holding-detail-dialog/interfaces/interfaces'; import { HoldingDetailDialogParams } from '@ghostfolio/client/components/holding-detail-dialog/interfaces/interfaces';
import { getCssVariable } from '@ghostfolio/common/helper'; import { getCssVariable } from '@ghostfolio/common/helper';
import { InfoItem, User } from '@ghostfolio/common/interfaces'; import { InfoItem, User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ColorScheme } from '@ghostfolio/common/types'; import { ColorScheme } from '@ghostfolio/common/types';
@ -62,29 +63,25 @@ export class AppComponent implements OnDestroy, OnInit {
public hasTabs = false; public hasTabs = false;
public info: InfoItem; public info: InfoItem;
public pageTitle: string; public pageTitle: string;
public routerLinkAbout = ['/' + $localize`:snake-case:about`]; public routerLinkAbout = ['/' + paths.about];
public routerLinkAboutChangelog = [ public routerLinkAboutChangelog = ['/' + paths.about, paths.changelog];
'/' + $localize`:snake-case:about`, public routerLinkAboutLicense = ['/' + paths.about, paths.license];
'changelog'
];
public routerLinkAboutLicense = [
'/' + $localize`:snake-case:about`,
$localize`:snake-case:license`
];
public routerLinkAboutPrivacyPolicy = [ public routerLinkAboutPrivacyPolicy = [
'/' + $localize`:snake-case:about`, '/' + paths.about,
$localize`:snake-case:privacy-policy` paths.privacyPolicy
]; ];
public routerLinkAboutTermsOfService = [ public routerLinkAboutTermsOfService = [
'/' + $localize`:snake-case:about`, '/' + paths.about,
$localize`:snake-case:terms-of-service` paths.termsOfService
]; ];
public routerLinkFaq = ['/' + $localize`:snake-case:faq`]; public routerLinkBlog = ['/' + paths.blog];
public routerLinkFeatures = ['/' + $localize`:snake-case:features`]; public routerLinkFaq = ['/' + paths.faq];
public routerLinkMarkets = ['/' + $localize`:snake-case:markets`]; public routerLinkFeatures = ['/' + paths.features];
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; public routerLinkMarkets = ['/' + paths.markets];
public routerLinkRegister = ['/' + $localize`:snake-case:register`]; public routerLinkOpenStartup = ['/' + paths.openStartup];
public routerLinkResources = ['/' + $localize`:snake-case:resources`]; public routerLinkPricing = ['/' + paths.pricing];
public routerLinkRegister = ['/' + paths.register];
public routerLinkResources = ['/' + paths.resources];
public showFooter = false; public showFooter = false;
public user: User; public user: User;

3
apps/client/src/app/components/access-table/access-table.component.ts

@ -2,6 +2,7 @@ import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/con
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { Access, User } from '@ghostfolio/common/interfaces'; import { Access, User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { Clipboard } from '@angular/cdk/clipboard'; import { Clipboard } from '@angular/cdk/clipboard';
import { import {
@ -55,7 +56,7 @@ export class AccessTableComponent implements OnChanges {
public getPublicUrl(aId: string): string { public getPublicUrl(aId: string): string {
const languageCode = this.user?.settings?.language ?? DEFAULT_LANGUAGE_CODE; const languageCode = this.user?.settings?.language ?? DEFAULT_LANGUAGE_CODE;
return `${this.baseUrl}/${languageCode}/p/${aId}`; return `${this.baseUrl}/${languageCode}/${paths.public}/${aId}`;
} }
public onCopyUrlToClipboard(aId: string): void { public onCopyUrlToClipboard(aId: string): void {

5
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts

@ -9,6 +9,7 @@ import {
PortfolioPosition, PortfolioPosition,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
@ -92,7 +93,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
} }
public onCloneActivity(aActivity: Activity) { public onCloneActivity(aActivity: Activity) {
this.router.navigate(['/portfolio', 'activities'], { this.router.navigate(['/' + paths.portfolio, paths.activities], {
queryParams: { activityId: aActivity.id, createDialog: true } queryParams: { activityId: aActivity.id, createDialog: true }
}); });
@ -151,7 +152,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
} }
public onUpdateActivity(aActivity: Activity) { public onUpdateActivity(aActivity: Activity) {
this.router.navigate(['/portfolio', 'activities'], { this.router.navigate(['/' + paths.portfolio, paths.activities], {
queryParams: { activityId: aActivity.id, editDialog: true } queryParams: { activityId: aActivity.id, editDialog: true }
}); });

17
apps/client/src/app/components/admin-settings/admin-settings.component.ts

@ -13,6 +13,7 @@ import {
DataProviderInfo, DataProviderInfo,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
@ -75,9 +76,7 @@ export class AdminSettingsComponent implements OnDestroy, OnInit {
const languageCode = const languageCode =
this.user?.settings?.language ?? DEFAULT_LANGUAGE_CODE; this.user?.settings?.language ?? DEFAULT_LANGUAGE_CODE;
this.pricingUrl = this.pricingUrl = `https://ghostfol.io/${languageCode}/${paths.pricing}`;
`https://ghostfol.io/${languageCode}/` +
$localize`:snake-case:pricing`;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
@ -146,10 +145,13 @@ export class AdminSettingsComponent implements OnDestroy, OnInit {
this.dataSource = new MatTableDataSource(filteredProviders); this.dataSource = new MatTableDataSource(filteredProviders);
const ghostfolioApiKey = settings[
PROPERTY_API_KEY_GHOSTFOLIO
] as string;
if (ghostfolioApiKey) {
this.adminService this.adminService
.fetchGhostfolioDataProviderStatus( .fetchGhostfolioDataProviderStatus(ghostfolioApiKey)
settings[PROPERTY_API_KEY_GHOSTFOLIO] as string
)
.pipe( .pipe(
catchError(() => { catchError(() => {
this.isGhostfolioApiKeyValid = false; this.isGhostfolioApiKeyValid = false;
@ -169,6 +171,9 @@ export class AdminSettingsComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} else {
this.isGhostfolioApiKeyValid = false;
}
this.isLoading = false; this.isLoading = false;

2
apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html

@ -29,7 +29,7 @@
}}</mat-option> }}</mat-option>
} }
@if (hasPermissionToAccessAdminControl) { @if (hasPermissionToAccessAdminControl) {
<mat-option [routerLink]="['/admin', 'market-data']"> <mat-option [routerLink]="routerLinkAdminControlMarketData">
<div class="align-items-center d-flex"> <div class="align-items-center d-flex">
<ion-icon class="mr-2 text-muted" name="arrow-forward-outline" /> <ion-icon class="mr-2 text-muted" name="arrow-forward-outline" />
<span i18n>Manage Benchmarks</span> <span i18n>Manage Benchmarks</span>

5
apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts

@ -12,6 +12,7 @@ import {
parseDate parseDate
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { LineChartItem, User } from '@ghostfolio/common/interfaces'; import { LineChartItem, User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ColorScheme } from '@ghostfolio/common/types'; import { ColorScheme } from '@ghostfolio/common/types';
@ -63,6 +64,10 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
public chart: Chart<'line'>; public chart: Chart<'line'>;
public hasPermissionToAccessAdminControl: boolean; public hasPermissionToAccessAdminControl: boolean;
public routerLinkAdminControlMarketData = [
'/' + paths.adminControl,
paths.marketData
];
public constructor() { public constructor() {
Chart.register( Chart.register(

42
apps/client/src/app/components/header/header.component.html

@ -20,9 +20,9 @@
mat-flat-button mat-flat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': 'font-weight-bold':
currentRoute === 'home' || currentRoute === 'zen', currentRoute === paths.home || currentRoute === paths.zen,
'text-decoration-underline': 'text-decoration-underline':
currentRoute === 'home' || currentRoute === 'zen' currentRoute === paths.home || currentRoute === paths.zen
}" }"
[routerLink]="['/']" [routerLink]="['/']"
>Overview</a >Overview</a
@ -34,10 +34,10 @@
i18n i18n
mat-flat-button mat-flat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === 'portfolio', 'font-weight-bold': currentRoute === paths.portfolio,
'text-decoration-underline': currentRoute === 'portfolio' 'text-decoration-underline': currentRoute === paths.portfolio
}" }"
[routerLink]="['/portfolio']" [routerLink]="routerLinkPortfolio"
>Portfolio</a >Portfolio</a
> >
</li> </li>
@ -47,10 +47,10 @@
i18n i18n
mat-flat-button mat-flat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === 'accounts', 'font-weight-bold': currentRoute === paths.accounts,
'text-decoration-underline': currentRoute === 'accounts' 'text-decoration-underline': currentRoute === paths.accounts
}" }"
[routerLink]="['/accounts']" [routerLink]="routerLinkAccounts"
>Accounts</a >Accounts</a
> >
</li> </li>
@ -61,10 +61,10 @@
i18n i18n
mat-flat-button mat-flat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === 'admin', 'font-weight-bold': currentRoute === paths.adminControl,
'text-decoration-underline': currentRoute === 'admin' 'text-decoration-underline': currentRoute === paths.adminControl
}" }"
[routerLink]="['/admin']" [routerLink]="routerLinkAdminControl"
>Admin Control</a >Admin Control</a
> >
</li> </li>
@ -235,7 +235,7 @@
mat-menu-item mat-menu-item
[ngClass]="{ [ngClass]="{
'font-weight-bold': 'font-weight-bold':
currentRoute === 'home' || currentRoute === 'zen' currentRoute === paths.home || currentRoute === paths.zen
}" }"
[routerLink]="['/']" [routerLink]="['/']"
>Overview</a >Overview</a
@ -245,24 +245,24 @@
i18n i18n
mat-menu-item mat-menu-item
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === 'portfolio' 'font-weight-bold': currentRoute === paths.portfolio
}" }"
[routerLink]="['/portfolio']" [routerLink]="routerLinkPortfolio"
>Portfolio</a >Portfolio</a
> >
<a <a
class="d-flex d-sm-none" class="d-flex d-sm-none"
i18n i18n
mat-menu-item mat-menu-item
[ngClass]="{ 'font-weight-bold': currentRoute === 'accounts' }" [ngClass]="{ 'font-weight-bold': currentRoute === paths.accounts }"
[routerLink]="['/accounts']" [routerLink]="routerLinkAccounts"
>Accounts</a >Accounts</a
> >
<a <a
i18n i18n
mat-menu-item mat-menu-item
[ngClass]="{ 'font-weight-bold': currentRoute === 'account' }" [ngClass]="{ 'font-weight-bold': currentRoute === paths.account }"
[routerLink]="['/account']" [routerLink]="routerLinkAccount"
>My Ghostfolio</a >My Ghostfolio</a
> >
@if (hasPermissionToAccessAdminControl) { @if (hasPermissionToAccessAdminControl) {
@ -270,8 +270,10 @@
class="d-flex d-sm-none" class="d-flex d-sm-none"
i18n i18n
mat-menu-item mat-menu-item
[ngClass]="{ 'font-weight-bold': currentRoute === 'admin' }" [ngClass]="{
[routerLink]="['/admin']" 'font-weight-bold': currentRoute === paths.adminControl
}"
[routerLink]="routerLinkAdminControl"
>Admin Control</a >Admin Control</a
> >
} }

28
apps/client/src/app/components/header/header.component.ts

@ -11,6 +11,7 @@ import {
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Filter, InfoItem, User } from '@ghostfolio/common/interfaces'; import { Filter, InfoItem, User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
import { GfAssistantComponent } from '@ghostfolio/ui/assistant/assistant.component'; import { GfAssistantComponent } from '@ghostfolio/ui/assistant/assistant.component';
@ -79,17 +80,22 @@ export class HeaderComponent implements OnChanges {
public hasPermissionToCreateUser: boolean; public hasPermissionToCreateUser: boolean;
public impersonationId: string; public impersonationId: string;
public isMenuOpen: boolean; public isMenuOpen: boolean;
public routeAbout = $localize`:snake-case:about`; public paths = paths;
public routeFeatures = $localize`:snake-case:features`; public routeAbout = paths.about;
public routeMarkets = $localize`:snake-case:markets`; public routeFeatures = paths.features;
public routePricing = $localize`:snake-case:pricing`; public routeMarkets = paths.markets;
public routeResources = $localize`:snake-case:resources`; public routePricing = paths.pricing;
public routerLinkAbout = ['/' + $localize`:snake-case:about`]; public routeResources = paths.resources;
public routerLinkFeatures = ['/' + $localize`:snake-case:features`]; public routerLinkAbout = ['/' + paths.about];
public routerLinkMarkets = ['/' + $localize`:snake-case:markets`]; public routerLinkAccount = ['/' + paths.account];
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; public routerLinkAccounts = ['/' + paths.accounts];
public routerLinkRegister = ['/' + $localize`:snake-case:register`]; public routerLinkAdminControl = ['/' + paths.adminControl];
public routerLinkResources = ['/' + $localize`:snake-case:resources`]; public routerLinkFeatures = ['/' + paths.features];
public routerLinkMarkets = ['/' + paths.markets];
public routerLinkPortfolio = ['/' + paths.portfolio];
public routerLinkPricing = ['/' + paths.pricing];
public routerLinkRegister = ['/' + paths.register];
public routerLinkResources = ['/' + paths.resources];
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();

5
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts

@ -13,6 +13,7 @@ import {
LineChartItem, LineChartItem,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-credits'; import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-credits';
@ -468,7 +469,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
} }
public onCloneActivity(aActivity: Activity) { public onCloneActivity(aActivity: Activity) {
this.router.navigate(['/portfolio', 'activities'], { this.router.navigate(['/' + paths.portfolio, paths.activities], {
queryParams: { activityId: aActivity.id, createDialog: true } queryParams: { activityId: aActivity.id, createDialog: true }
}); });
@ -510,7 +511,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
} }
public onUpdateActivity(aActivity: Activity) { public onUpdateActivity(aActivity: Activity) {
this.router.navigate(['/portfolio', 'activities'], { this.router.navigate(['/' + paths.portfolio, paths.activities], {
queryParams: { activityId: aActivity.id, editDialog: true } queryParams: { activityId: aActivity.id, editDialog: true }
}); });

5
apps/client/src/app/components/home-holdings/home-holdings.component.ts

@ -7,6 +7,7 @@ import {
ToggleOption, ToggleOption,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { HoldingType, HoldingsViewMode } from '@ghostfolio/common/types'; import { HoldingType, HoldingsViewMode } from '@ghostfolio/common/types';
@ -36,6 +37,10 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
{ label: $localize`Active`, value: 'ACTIVE' }, { label: $localize`Active`, value: 'ACTIVE' },
{ label: $localize`Closed`, value: 'CLOSED' } { label: $localize`Closed`, value: 'CLOSED' }
]; ];
public routerLinkPortfolioActivities = [
'/' + paths.portfolio,
paths.activities
];
public user: User; public user: User;
public viewModeFormControl = new FormControl<HoldingsViewMode>( public viewModeFormControl = new FormControl<HoldingsViewMode>(
HomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE HomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE

2
apps/client/src/app/components/home-holdings/home-holdings.html

@ -58,7 +58,7 @@
class="mt-3" class="mt-3"
i18n i18n
mat-stroked-button mat-stroked-button
[routerLink]="['/portfolio', 'activities']" [routerLink]="routerLinkPortfolioActivities"
>Manage Activities</a >Manage Activities</a
> >
</div> </div>

8
apps/client/src/app/components/home-market/home-market.html

@ -36,6 +36,14 @@
[locale]="user?.settings?.locale || undefined" [locale]="user?.settings?.locale || undefined"
[user]="user" [user]="user"
/> />
@if (benchmarks?.length > 0) {
<div class="mt-3 text-center">
<small class="text-muted" i18n>
Calculations are based on delayed market data and may not be
displayed in real-time.</small
>
</div>
}
</div> </div>
</div> </div>
</div> </div>

7
apps/client/src/app/components/home-overview/home-overview.component.ts

@ -10,6 +10,7 @@ import {
PortfolioPerformance, PortfolioPerformance,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
@ -36,6 +37,12 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
public isLoadingPerformance = true; public isLoadingPerformance = true;
public performance: PortfolioPerformance; public performance: PortfolioPerformance;
public precision = 2; public precision = 2;
public routerLinkAccounts = ['/' + paths.accounts];
public routerLinkPortfolio = ['/' + paths.portfolio];
public routerLinkPortfolioActivities = [
'/' + paths.portfolio,
paths.activities
];
public showDetails = false; public showDetails = false;
public unit: string; public unit: string;
public user: User; public user: User;

14
apps/client/src/app/components/home-overview/home-overview.html

@ -11,7 +11,7 @@
class="mb-2" class="mb-2"
[ngClass]="{ 'text-muted': user?.accounts?.length > 1 }" [ngClass]="{ 'text-muted': user?.accounts?.length > 1 }"
> >
<a class="d-block" [routerLink]="['/accounts']" <a class="d-block" [routerLink]="routerLinkAccounts"
><span i18n>Setup your accounts</span><br /> ><span i18n>Setup your accounts</span><br />
<span class="font-weight-normal" i18n <span class="font-weight-normal" i18n
>Get a comprehensive financial overview by adding your bank and >Get a comprehensive financial overview by adding your bank and
@ -20,7 +20,7 @@
> >
</li> </li>
<li class="mb-2"> <li class="mb-2">
<a class="d-block" [routerLink]="['/portfolio', 'activities']"> <a class="d-block" [routerLink]="routerLinkPortfolioActivities">
<span i18n>Capture your activities</span><br /> <span i18n>Capture your activities</span><br />
<span class="font-weight-normal" i18n <span class="font-weight-normal" i18n
>Record your investment activities to keep your portfolio up to >Record your investment activities to keep your portfolio up to
@ -29,7 +29,7 @@
> >
</li> </li>
<li class="mb-2"> <li class="mb-2">
<a class="d-block" [routerLink]="['/portfolio']"> <a class="d-block" [routerLink]="routerLinkPortfolio">
<span i18n>Monitor and analyze your portfolio</span><br /> <span i18n>Monitor and analyze your portfolio</span><br />
<span class="font-weight-normal" i18n <span class="font-weight-normal" i18n
>Track your progress in real-time with comprehensive analysis >Track your progress in real-time with comprehensive analysis
@ -40,14 +40,18 @@
</ol> </ol>
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
@if (user?.accounts?.length === 1) { @if (user?.accounts?.length === 1) {
<a color="primary" mat-flat-button [routerLink]="['/accounts']"> <a
color="primary"
mat-flat-button
[routerLink]="routerLinkAccounts"
>
<ng-container i18n>Setup accounts</ng-container> <ng-container i18n>Setup accounts</ng-container>
</a> </a>
} @else if (user?.accounts?.length > 1) { } @else if (user?.accounts?.length > 1) {
<a <a
color="primary" color="primary"
mat-flat-button mat-flat-button
[routerLink]="['/portfolio', 'activities']" [routerLink]="routerLinkPortfolioActivities"
> >
<ng-container i18n>Add activity</ng-container> <ng-container i18n>Add activity</ng-container>
</a> </a>

6
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -7,11 +7,11 @@
</div> </div>
<div <div
class="flex-nowrap px-3 py-1 row" class="flex-nowrap px-3 py-1 row"
[hidden]="summary?.ordersCount === null" [hidden]="summary?.activityCount === null"
> >
<div class="d-flex flex-grow-1 ml-3 text-truncate"> <div class="d-flex flex-grow-1 ml-3 text-truncate">
{{ summary?.ordersCount }} {{ summary?.activityCount }}
<ng-container i18n>{summary?.ordersCount, plural, <ng-container i18n>{summary?.activityCount, plural,
=1 {activity} =1 {activity}
other {activities} other {activities}
}</ng-container> }</ng-container>

4
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
@ -26,7 +28,7 @@ export class SubscriptionInterstitialDialog implements OnInit {
public remainingSkipButtonDelay = public remainingSkipButtonDelay =
SubscriptionInterstitialDialog.SKIP_BUTTON_DELAY_IN_SECONDS; SubscriptionInterstitialDialog.SKIP_BUTTON_DELAY_IN_SECONDS;
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; public routerLinkPricing = ['/' + paths.pricing];
public variantIndex: number; public variantIndex: number;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();

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

@ -4,6 +4,7 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { getDateFormatString } from '@ghostfolio/common/helper'; import { getDateFormatString } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { import {
@ -36,7 +37,7 @@ export class UserAccountMembershipComponent implements OnDestroy {
public hasPermissionToUpdateUserSettings: boolean; public hasPermissionToUpdateUserSettings: boolean;
public price: number; public price: number;
public priceId: string; public priceId: string;
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; public routerLinkPricing = ['/' + paths.pricing];
public trySubscriptionMail = 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'; '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;

37
apps/client/src/app/core/auth.guard.ts

@ -1,6 +1,7 @@
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service'; import { 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 { paths } from '@ghostfolio/common/paths';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { import {
@ -11,20 +12,18 @@ import {
import { EMPTY } from 'rxjs'; import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { paths } from './paths';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AuthGuard { export class AuthGuard {
private static PUBLIC_PAGE_ROUTES = [ private static PUBLIC_PAGE_ROUTES = [
`/${paths.about}`, `/${paths.about}`,
'/blog', `/${paths.blog}`,
'/demo', `/${paths.demo}`,
`/${paths.faq}`, `/${paths.faq}`,
`/${paths.features}`, `/${paths.features}`,
`/${paths.markets}`, `/${paths.markets}`,
'/open', `/${paths.openStartup}`,
'/p',
`/${paths.pricing}`, `/${paths.pricing}`,
`/${paths.public}`,
`/${paths.register}`, `/${paths.register}`,
`/${paths.resources}` `/${paths.resources}`
]; ];
@ -49,21 +48,21 @@ export class AuthGuard {
.pipe( .pipe(
catchError(() => { catchError(() => {
if (utmSource === 'ios') { if (utmSource === 'ios') {
this.router.navigate(['/demo']); this.router.navigate(['/' + paths.demo]);
resolve(false); resolve(false);
} else if (utmSource === 'trusted-web-activity') { } else if (utmSource === 'trusted-web-activity') {
this.router.navigate(['/' + $localize`register`]); this.router.navigate(['/' + paths.register]);
resolve(false); resolve(false);
} else if ( } else if (
AuthGuard.PUBLIC_PAGE_ROUTES.filter((publicPageRoute) => { AuthGuard.PUBLIC_PAGE_ROUTES.some((publicPageRoute) => {
const [, url] = state.url.split('/'); const [, url] = decodeURIComponent(state.url).split('/');
return `/${url}` === publicPageRoute; return `/${url}` === publicPageRoute;
})?.length > 0 })
) { ) {
resolve(true); resolve(true);
return EMPTY; return EMPTY;
} else if (state.url !== '/start') { } else if (state.url !== '/start') {
this.router.navigate(['/start']); this.router.navigate(['/' + paths.start]);
resolve(false); resolve(false);
return EMPTY; return EMPTY;
} }
@ -89,26 +88,26 @@ export class AuthGuard {
resolve(true); resolve(true);
return; return;
} else if ( } else if (
state.url.startsWith('/home') && state.url.startsWith(`/${paths.home}`) &&
user.settings.viewMode === 'ZEN' user.settings.viewMode === 'ZEN'
) { ) {
this.router.navigate(['/zen']); this.router.navigate(['/' + paths.zen]);
resolve(false); resolve(false);
return; return;
} else if (state.url.startsWith('/start')) { } else if (state.url.startsWith(`/${paths.start}`)) {
if (user.settings.viewMode === 'ZEN') { if (user.settings.viewMode === 'ZEN') {
this.router.navigate(['/zen']); this.router.navigate(['/' + paths.zen]);
} else { } else {
this.router.navigate(['/home']); this.router.navigate(['/' + paths.home]);
} }
resolve(false); resolve(false);
return; return;
} else if ( } else if (
state.url.startsWith('/zen') && state.url.startsWith(`/${paths.zen}`) &&
user.settings.viewMode === 'DEFAULT' user.settings.viewMode === 'DEFAULT'
) { ) {
this.router.navigate(['/home']); this.router.navigate(['/' + paths.home]);
resolve(false); resolve(false);
return; return;
} }

5
apps/client/src/app/core/http-response.interceptor.ts

@ -2,6 +2,7 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { InfoItem } from '@ghostfolio/common/interfaces'; import { InfoItem } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { import {
HTTP_INTERCEPTORS, HTTP_INTERCEPTORS,
@ -74,7 +75,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
}); });
this.snackBarRef.onAction().subscribe(() => { this.snackBarRef.onAction().subscribe(() => {
this.router.navigate(['/' + $localize`pricing`]); this.router.navigate(['/' + paths.pricing]);
}); });
} }
} else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) { } else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) {
@ -110,7 +111,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
} else if (error.status === StatusCodes.UNAUTHORIZED) { } else if (error.status === StatusCodes.UNAUTHORIZED) {
if (!error.url.includes('/data-providers/ghostfolio/status')) { if (!error.url.includes('/data-providers/ghostfolio/status')) {
if (this.webAuthnService.isEnabled()) { if (this.webAuthnService.isEnabled()) {
this.router.navigate(['/webauthn']); this.router.navigate(['/' + paths.webauthn]);
} else { } else {
this.tokenStorageService.signOut(); this.tokenStorageService.signOut();
} }

12
apps/client/src/app/core/paths.ts

@ -1,12 +0,0 @@
export const paths = {
about: $localize`about`,
faq: $localize`faq`,
features: $localize`features`,
license: $localize`license`,
markets: $localize`markets`,
pricing: $localize`pricing`,
privacyPolicy: $localize`privacy-policy`,
register: $localize`register`,
resources: $localize`resources`,
termsOfService: $localize`terms-of-service`
};

6
apps/client/src/app/pages/about/about-page-routing.module.ts

@ -1,5 +1,5 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { paths } from '@ghostfolio/client/core/paths'; import { paths } from '@ghostfolio/common/paths';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
@ -18,7 +18,7 @@ const routes: Routes = [
) )
}, },
{ {
path: 'changelog', path: paths.changelog,
loadChildren: () => loadChildren: () =>
import('./changelog/changelog-page.module').then( import('./changelog/changelog-page.module').then(
(m) => m.ChangelogPageModule (m) => m.ChangelogPageModule
@ -32,7 +32,7 @@ const routes: Routes = [
) )
}, },
{ {
path: 'oss-friends', path: paths.ossFriends,
loadChildren: () => loadChildren: () =>
import('./oss-friends/oss-friends-page.module').then( import('./oss-friends/oss-friends-page.module').then(
(m) => m.OpenSourceSoftwareFriendsPageModule (m) => m.OpenSourceSoftwareFriendsPageModule

13
apps/client/src/app/pages/about/about-page.component.ts

@ -1,6 +1,7 @@
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { TabConfiguration, User } from '@ghostfolio/common/interfaces'; import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
@ -43,17 +44,17 @@ export class AboutPageComponent implements OnDestroy, OnInit {
{ {
iconName: 'information-circle-outline', iconName: 'information-circle-outline',
label: $localize`About`, label: $localize`About`,
path: ['/' + $localize`about`] path: ['/' + paths.about]
}, },
{ {
iconName: 'sparkles-outline', iconName: 'sparkles-outline',
label: $localize`Changelog`, label: $localize`Changelog`,
path: ['/' + $localize`about`, 'changelog'] path: ['/' + paths.about, paths.changelog]
}, },
{ {
iconName: 'ribbon-outline', iconName: 'ribbon-outline',
label: $localize`License`, label: $localize`License`,
path: ['/' + $localize`about`, $localize`license`], path: ['/' + paths.about, paths.license],
showCondition: !this.hasPermissionForSubscription showCondition: !this.hasPermissionForSubscription
} }
]; ];
@ -62,14 +63,14 @@ export class AboutPageComponent implements OnDestroy, OnInit {
this.tabs.push({ this.tabs.push({
iconName: 'shield-checkmark-outline', iconName: 'shield-checkmark-outline',
label: $localize`Privacy Policy`, label: $localize`Privacy Policy`,
path: ['/' + $localize`about`, $localize`privacy-policy`], path: ['/' + paths.about, paths.privacyPolicy],
showCondition: this.hasPermissionForSubscription showCondition: this.hasPermissionForSubscription
}); });
this.tabs.push({ this.tabs.push({
iconName: 'document-text-outline', iconName: 'document-text-outline',
label: $localize`Terms of Service`, label: $localize`Terms of Service`,
path: ['/' + $localize`about`, $localize`terms-of-service`], path: ['/' + paths.about, paths.termsOfService],
showCondition: this.hasPermissionForSubscription showCondition: this.hasPermissionForSubscription
}); });
@ -81,7 +82,7 @@ export class AboutPageComponent implements OnDestroy, OnInit {
this.tabs.push({ this.tabs.push({
iconName: 'happy-outline', iconName: 'happy-outline',
label: 'OSS Friends', label: 'OSS Friends',
path: ['/' + $localize`about`, 'oss-friends'] path: ['/' + paths.about, paths.ossFriends]
}); });
}); });
} }

7
apps/client/src/app/pages/about/overview/about-overview-page.component.ts

@ -1,6 +1,7 @@
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
@ -17,8 +18,10 @@ export class AboutOverviewPageComponent implements OnDestroy, OnInit {
public hasPermissionForStatistics: boolean; public hasPermissionForStatistics: boolean;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
public isLoggedIn: boolean; public isLoggedIn: boolean;
public routerLinkFaq = ['/' + $localize`:snake-case:faq`]; public routerLinkBlog = ['/' + paths.blog];
public routerLinkFeatures = ['/' + $localize`:snake-case:features`]; public routerLinkFaq = ['/' + paths.faq];
public routerLinkFeatures = ['/' + paths.features];
public routerLinkOpenStartup = ['/' + paths.openStartup];
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();

6
apps/client/src/app/pages/about/overview/about-overview-page.html

@ -23,7 +23,9 @@
> >
@if (hasPermissionForStatistics) { @if (hasPermissionForStatistics) {
and we share aggregated and we share aggregated
<a title="Open Startup" [routerLink]="['/open']">key metrics</a> <a title="Open Startup" [routerLink]="routerLinkOpenStartup"
>key metrics</a
>
of the platform’s performance of the platform’s performance
} }
. The project has been initiated by . The project has been initiated by
@ -160,7 +162,7 @@
class="py-4 w-100" class="py-4 w-100"
color="primary" color="primary"
mat-flat-button mat-flat-button
[routerLink]="['/blog']" [routerLink]="routerLinkBlog"
>Blog</a >Blog</a
> >
</div> </div>

9
apps/client/src/app/pages/admin/admin-page-routing.module.ts

@ -4,6 +4,7 @@ import { AdminOverviewComponent } from '@ghostfolio/client/components/admin-over
import { AdminSettingsComponent } from '@ghostfolio/client/components/admin-settings/admin-settings.component'; import { AdminSettingsComponent } from '@ghostfolio/client/components/admin-settings/admin-settings.component';
import { AdminUsersComponent } from '@ghostfolio/client/components/admin-users/admin-users.component'; import { AdminUsersComponent } from '@ghostfolio/client/components/admin-users/admin-users.component';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { paths } from '@ghostfolio/common/paths';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
@ -20,22 +21,22 @@ const routes: Routes = [
title: $localize`Admin Control` title: $localize`Admin Control`
}, },
{ {
path: 'jobs', path: paths.jobs,
component: AdminJobsComponent, component: AdminJobsComponent,
title: $localize`Job Queue` title: $localize`Job Queue`
}, },
{ {
path: 'market-data', path: paths.marketData,
component: AdminMarketDataComponent, component: AdminMarketDataComponent,
title: $localize`Market Data` title: $localize`Market Data`
}, },
{ {
path: 'settings', path: paths.settings,
component: AdminSettingsComponent, component: AdminSettingsComponent,
title: $localize`Settings` title: $localize`Settings`
}, },
{ {
path: 'users', path: paths.users,
component: AdminUsersComponent, component: AdminUsersComponent,
title: $localize`Users` title: $localize`Users`
} }

11
apps/client/src/app/pages/admin/admin-page.component.ts

@ -1,4 +1,5 @@
import { TabConfiguration } from '@ghostfolio/common/interfaces'; import { TabConfiguration } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
@ -26,27 +27,27 @@ export class AdminPageComponent implements OnDestroy, OnInit {
{ {
iconName: 'reader-outline', iconName: 'reader-outline',
label: $localize`Overview`, label: $localize`Overview`,
path: ['/admin'] path: ['/' + paths.adminControl]
}, },
{ {
iconName: 'settings-outline', iconName: 'settings-outline',
label: $localize`Settings`, label: $localize`Settings`,
path: ['/admin', 'settings'] path: ['/' + paths.adminControl, paths.settings]
}, },
{ {
iconName: 'server-outline', iconName: 'server-outline',
label: $localize`Market Data`, label: $localize`Market Data`,
path: ['/admin', 'market-data'] path: ['/' + paths.adminControl, paths.marketData]
}, },
{ {
iconName: 'flash-outline', iconName: 'flash-outline',
label: $localize`Job Queue`, label: $localize`Job Queue`,
path: ['/admin', 'jobs'] path: ['/' + paths.adminControl, paths.jobs]
}, },
{ {
iconName: 'people-outline', iconName: 'people-outline',
label: $localize`Users`, label: $localize`Users`,
path: ['/admin', 'users'] path: ['/' + paths.adminControl, paths.users]
} }
]; ];
} }

7
apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -9,6 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './hallo-ghostfolio-page.html' templateUrl: './hallo-ghostfolio-page.html'
}) })
export class HalloGhostfolioPageComponent { export class HalloGhostfolioPageComponent {
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; public routerLinkBlog = ['/' + paths.blog];
public routerLinkResources = ['/' + $localize`:snake-case:resources`]; public routerLinkPricing = ['/' + paths.pricing];
public routerLinkResources = ['/' + paths.resources];
} }

2
apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.html

@ -201,7 +201,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

7
apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -9,6 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './hello-ghostfolio-page.html' templateUrl: './hello-ghostfolio-page.html'
}) })
export class HelloGhostfolioPageComponent { export class HelloGhostfolioPageComponent {
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; public routerLinkBlog = ['/' + paths.blog];
public routerLinkResources = ['/' + $localize`:snake-case:resources`]; public routerLinkPricing = ['/' + paths.pricing];
public routerLinkResources = ['/' + paths.resources];
} }

2
apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.html

@ -181,7 +181,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

5
apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -9,5 +11,6 @@ import { RouterModule } from '@angular/router';
templateUrl: './first-months-in-open-source-page.html' templateUrl: './first-months-in-open-source-page.html'
}) })
export class FirstMonthsInOpenSourcePageComponent { export class FirstMonthsInOpenSourcePageComponent {
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; public routerLinkBlog = ['/' + paths.blog];
public routerLinkPricing = ['/' + paths.pricing];
} }

2
apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.html

@ -181,7 +181,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

6
apps/client/src/app/pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -8,4 +10,6 @@ import { RouterModule } from '@angular/router';
selector: 'gf-ghostfolio-meets-internet-identity-page', selector: 'gf-ghostfolio-meets-internet-identity-page',
templateUrl: './ghostfolio-meets-internet-identity-page.html' templateUrl: './ghostfolio-meets-internet-identity-page.html'
}) })
export class GhostfolioMeetsInternetIdentityPageComponent {} export class GhostfolioMeetsInternetIdentityPageComponent {
public routerLinkBlog = ['/' + paths.blog];
}

2
apps/client/src/app/pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.html

@ -181,7 +181,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

5
apps/client/src/app/pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -9,5 +11,6 @@ import { RouterModule } from '@angular/router';
templateUrl: './how-do-i-get-my-finances-in-order-page.html' templateUrl: './how-do-i-get-my-finances-in-order-page.html'
}) })
export class HowDoIGetMyFinancesInOrderPageComponent { export class HowDoIGetMyFinancesInOrderPageComponent {
public routerLinkResources = ['/' + $localize`:snake-case:resources`]; public routerLinkBlog = ['/' + paths.blog];
public routerLinkResources = ['/' + paths.resources];
} }

2
apps/client/src/app/pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.html

@ -206,7 +206,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

7
apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -9,6 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './500-stars-on-github-page.html' templateUrl: './500-stars-on-github-page.html'
}) })
export class FiveHundredStarsOnGitHubPageComponent { export class FiveHundredStarsOnGitHubPageComponent {
public routerLinkMarkets = ['/' + $localize`:snake-case:markets`]; public routerLinkBlog = ['/' + paths.blog];
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; public routerLinkMarkets = ['/' + paths.markets];
public routerLinkPricing = ['/' + paths.pricing];
} }

2
apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.html

@ -193,7 +193,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

6
apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -8,4 +10,6 @@ import { RouterModule } from '@angular/router';
selector: 'gf-hacktoberfest-2022-page', selector: 'gf-hacktoberfest-2022-page',
templateUrl: './hacktoberfest-2022-page.html' templateUrl: './hacktoberfest-2022-page.html'
}) })
export class Hacktoberfest2022PageComponent {} export class Hacktoberfest2022PageComponent {
public routerLinkBlog = ['/' + paths.blog];
}

2
apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.html

@ -178,7 +178,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

6
apps/client/src/app/pages/blog/2022/11/black-friday-2022/black-friday-2022-page.component.ts

@ -1,3 +1,4 @@
import { paths } from '@ghostfolio/common/paths';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@ -11,6 +12,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './black-friday-2022-page.html' templateUrl: './black-friday-2022-page.html'
}) })
export class BlackFriday2022PageComponent { export class BlackFriday2022PageComponent {
public routerLinkFeatures = ['/' + $localize`:snake-case:features`]; public routerLinkBlog = ['/' + paths.blog];
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; public routerLinkFeatures = ['/' + paths.features];
public routerLinkPricing = ['/' + paths.pricing];
} }

2
apps/client/src/app/pages/blog/2022/11/black-friday-2022/black-friday-2022-page.html

@ -138,7 +138,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

6
apps/client/src/app/pages/blog/2022/12/the-importance-of-tracking-your-personal-finances/the-importance-of-tracking-your-personal-finances-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -8,4 +10,6 @@ import { RouterModule } from '@angular/router';
selector: 'gf-the-importance-of-tracking-your-personal-finances-page', selector: 'gf-the-importance-of-tracking-your-personal-finances-page',
templateUrl: './the-importance-of-tracking-your-personal-finances-page.html' templateUrl: './the-importance-of-tracking-your-personal-finances-page.html'
}) })
export class TheImportanceOfTrackingYourPersonalFinancesPageComponent {} export class TheImportanceOfTrackingYourPersonalFinancesPageComponent {
public routerLinkBlog = ['/' + paths.blog];
}

2
apps/client/src/app/pages/blog/2022/12/the-importance-of-tracking-your-personal-finances/the-importance-of-tracking-your-personal-finances-page.html

@ -165,7 +165,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

6
apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -8,4 +10,6 @@ import { RouterModule } from '@angular/router';
selector: 'gf-ghostfolio-auf-sackgeld-vorgestellt-page', selector: 'gf-ghostfolio-auf-sackgeld-vorgestellt-page',
templateUrl: './ghostfolio-auf-sackgeld-vorgestellt-page.html' templateUrl: './ghostfolio-auf-sackgeld-vorgestellt-page.html'
}) })
export class GhostfolioAufSackgeldVorgestelltPageComponent {} export class GhostfolioAufSackgeldVorgestelltPageComponent {
public routerLinkBlog = ['/' + paths.blog];
}

2
apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.html

@ -175,7 +175,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

6
apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -8,4 +10,6 @@ import { RouterModule } from '@angular/router';
selector: 'gf-ghostfolio-meets-umbrel-page', selector: 'gf-ghostfolio-meets-umbrel-page',
templateUrl: './ghostfolio-meets-umbrel-page.html' templateUrl: './ghostfolio-meets-umbrel-page.html'
}) })
export class GhostfolioMeetsUmbrelPageComponent {} export class GhostfolioMeetsUmbrelPageComponent {
public routerLinkBlog = ['/' + paths.blog];
}

2
apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.html

@ -199,7 +199,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

7
apps/client/src/app/pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -9,6 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './1000-stars-on-github-page.html' templateUrl: './1000-stars-on-github-page.html'
}) })
export class ThousandStarsOnGitHubPageComponent { export class ThousandStarsOnGitHubPageComponent {
public routerLinkFeatures = ['/' + $localize`:snake-case:features`]; public routerLinkBlog = ['/' + paths.blog];
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; public routerLinkFeatures = ['/' + paths.features];
public routerLinkPricing = ['/' + paths.pricing];
} }

2
apps/client/src/app/pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.html

@ -250,7 +250,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

7
apps/client/src/app/pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -9,6 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './unlock-your-financial-potential-with-ghostfolio-page.html' templateUrl: './unlock-your-financial-potential-with-ghostfolio-page.html'
}) })
export class UnlockYourFinancialPotentialWithGhostfolioPageComponent { export class UnlockYourFinancialPotentialWithGhostfolioPageComponent {
public routerLinkFeatures = ['/' + $localize`:snake-case:features`]; public routerLinkBlog = ['/' + paths.blog];
public routerLinkResources = ['/' + $localize`:snake-case:resources`]; public routerLinkFeatures = ['/' + paths.features];
public routerLinkResources = ['/' + paths.resources];
} }

2
apps/client/src/app/pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.html

@ -230,7 +230,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

5
apps/client/src/app/pages/blog/2023/07/exploring-the-path-to-fire/exploring-the-path-to-fire-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -9,5 +11,6 @@ import { RouterModule } from '@angular/router';
templateUrl: './exploring-the-path-to-fire-page.html' templateUrl: './exploring-the-path-to-fire-page.html'
}) })
export class ExploringThePathToFirePageComponent { export class ExploringThePathToFirePageComponent {
public routerLinkFeatures = ['/' + $localize`:snake-case:features`]; public routerLinkBlog = ['/' + paths.blog];
public routerLinkFeatures = ['/' + paths.features];
} }

2
apps/client/src/app/pages/blog/2023/07/exploring-the-path-to-fire/exploring-the-path-to-fire-page.html

@ -240,7 +240,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

8
apps/client/src/app/pages/blog/2023/08/ghostfolio-joins-oss-friends/ghostfolio-joins-oss-friends-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -9,8 +11,6 @@ import { RouterModule } from '@angular/router';
templateUrl: './ghostfolio-joins-oss-friends-page.html' templateUrl: './ghostfolio-joins-oss-friends-page.html'
}) })
export class GhostfolioJoinsOssFriendsPageComponent { export class GhostfolioJoinsOssFriendsPageComponent {
public routerLinkAboutOssFriends = [ public routerLinkAboutOssFriends = ['/' + paths.about, paths.ossFriends];
'/' + $localize`:snake-case:about`, public routerLinkBlog = ['/' + paths.blog];
'oss-friends'
];
} }

2
apps/client/src/app/pages/blog/2023/08/ghostfolio-joins-oss-friends/ghostfolio-joins-oss-friends-page.html

@ -151,7 +151,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

14
apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -9,11 +11,9 @@ import { RouterModule } from '@angular/router';
templateUrl: './ghostfolio-2-page.html' templateUrl: './ghostfolio-2-page.html'
}) })
export class Ghostfolio2PageComponent { export class Ghostfolio2PageComponent {
public routerLinkAbout = ['/' + $localize`:snake-case:about`]; public routerLinkAbout = ['/' + paths.about];
public routerLinkAboutChangelog = [ public routerLinkAboutChangelog = ['/' + paths.about, paths.changelog];
'/' + $localize`:snake-case:about`, public routerLinkBlog = ['/' + paths.blog];
'changelog' public routerLinkFeatures = ['/' + paths.features];
]; public routerLinkMarkets = ['/' + paths.markets];
public routerLinkFeatures = ['/' + $localize`:snake-case:features`];
public routerLinkMarkets = ['/' + $localize`:snake-case:markets`];
} }

2
apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html

@ -270,7 +270,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

6
apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -9,5 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './hacktoberfest-2023-page.html' templateUrl: './hacktoberfest-2023-page.html'
}) })
export class Hacktoberfest2023PageComponent { export class Hacktoberfest2023PageComponent {
public routerLinkAbout = ['/' + $localize`:snake-case:about`]; public routerLinkAbout = ['/' + paths.about];
public routerLinkBlog = ['/' + paths.blog];
public routerLinkOpenStartup = ['/' + paths.openStartup];
} }

12
apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.html

@ -48,10 +48,12 @@
</p> </p>
<p> <p>
The software is used daily by a thriving global community. With over The software is used daily by a thriving global community. With over
<a [routerLink]="['/open']">2’600 stars on GitHub</a> and <a [routerLink]="routerLinkOpenStartup">2’600 stars on GitHub</a>
<a [routerLink]="['/open']">300’000+ pulls on Docker Hub</a>, and
Ghostfolio has gained widespread recognition for its user-friendly <a [routerLink]="routerLinkOpenStartup"
experience and simplicity. >300’000+ pulls on Docker Hub</a
>, Ghostfolio has gained widespread recognition for its
user-friendly experience and simplicity.
</p> </p>
</section> </section>
<section class="mb-4"> <section class="mb-4">
@ -178,7 +180,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

6
apps/client/src/app/pages/blog/2023/11/black-week-2023/black-week-2023-page.component.ts

@ -1,3 +1,4 @@
import { paths } from '@ghostfolio/common/paths';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@ -11,6 +12,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './black-week-2023-page.html' templateUrl: './black-week-2023-page.html'
}) })
export class BlackWeek2023PageComponent { export class BlackWeek2023PageComponent {
public routerLinkFeatures = ['/' + $localize`:snake-case:features`]; public routerLinkBlog = ['/' + paths.blog];
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; public routerLinkFeatures = ['/' + paths.features];
public routerLinkPricing = ['/' + paths.pricing];
} }

2
apps/client/src/app/pages/blog/2023/11/black-week-2023/black-week-2023-page.html

@ -145,7 +145,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

7
apps/client/src/app/pages/blog/2023/11/hacktoberfest-2023-debriefing/hacktoberfest-2023-debriefing-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -9,6 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './hacktoberfest-2023-debriefing-page.html' templateUrl: './hacktoberfest-2023-debriefing-page.html'
}) })
export class Hacktoberfest2023DebriefingPageComponent { export class Hacktoberfest2023DebriefingPageComponent {
public routerLinkAbout = ['/' + $localize`:snake-case:about`]; public routerLinkAbout = ['/' + paths.about];
public routerLinkFeatures = ['/' + $localize`:snake-case:features`]; public routerLinkBlog = ['/' + paths.blog];
public routerLinkFeatures = ['/' + paths.features];
} }

2
apps/client/src/app/pages/blog/2023/11/hacktoberfest-2023-debriefing/hacktoberfest-2023-debriefing-page.html

@ -267,7 +267,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

6
apps/client/src/app/pages/blog/2024/09/hacktoberfest-2024/hacktoberfest-2024-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -9,5 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './hacktoberfest-2024-page.html' templateUrl: './hacktoberfest-2024-page.html'
}) })
export class Hacktoberfest2024PageComponent { export class Hacktoberfest2024PageComponent {
public routerLinkAbout = ['/' + $localize`:snake-case:about`]; public routerLinkAbout = ['/' + paths.about];
public routerLinkBlog = ['/' + paths.blog];
public routerLinkOpenStartup = ['/' + paths.openStartup];
} }

12
apps/client/src/app/pages/blog/2024/09/hacktoberfest-2024/hacktoberfest-2024-page.html

@ -51,10 +51,12 @@
<p> <p>
The OSS project counting more than 100 contributors is used daily by The OSS project counting more than 100 contributors is used daily by
its growing global community. With over its growing global community. With over
<a [routerLink]="['/open']">4’000 stars on GitHub</a> and <a [routerLink]="routerLinkOpenStartup">4’000 stars on GitHub</a>
<a [routerLink]="['/open']">800’000+ pulls on Docker Hub</a>, and
Ghostfolio has gained widespread recognition for its user-friendly <a [routerLink]="routerLinkOpenStartup"
experience and simplicity. >800’000+ pulls on Docker Hub</a
>, Ghostfolio has gained widespread recognition for its
user-friendly experience and simplicity.
</p> </p>
</section> </section>
<section class="mb-4"> <section class="mb-4">
@ -184,7 +186,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

6
apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.component.ts

@ -1,3 +1,4 @@
import { paths } from '@ghostfolio/common/paths';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@ -11,6 +12,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './black-weeks-2024-page.html' templateUrl: './black-weeks-2024-page.html'
}) })
export class BlackWeeks2024PageComponent { export class BlackWeeks2024PageComponent {
public routerLinkFeatures = ['/' + $localize`:snake-case:features`]; public routerLinkBlog = ['/' + paths.blog];
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`]; public routerLinkFeatures = ['/' + paths.features];
public routerLinkPricing = ['/' + paths.pricing];
} }

2
apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.html

@ -164,7 +164,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="routerLinkBlog">Blog</a>
</li> </li>
<li <li
aria-current="page" aria-current="page"

5
apps/client/src/app/pages/faq/faq-page-routing.module.ts

@ -1,4 +1,5 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { paths } from '@ghostfolio/common/paths';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
@ -17,12 +18,12 @@ const routes: Routes = [
) )
}, },
{ {
path: 'saas', path: paths.saas,
loadChildren: () => loadChildren: () =>
import('./saas/saas-page.module').then((m) => m.SaasPageModule) import('./saas/saas-page.module').then((m) => m.SaasPageModule)
}, },
{ {
path: 'self-hosting', path: paths.selfHosting,
loadChildren: () => loadChildren: () =>
import('./self-hosting/self-hosting-page.module').then( import('./self-hosting/self-hosting-page.module').then(
(m) => m.SelfHostingPageModule (m) => m.SelfHostingPageModule

7
apps/client/src/app/pages/faq/faq-page.component.ts

@ -1,5 +1,6 @@
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { TabConfiguration } from '@ghostfolio/common/interfaces'; import { TabConfiguration } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
@ -35,18 +36,18 @@ export class FaqPageComponent implements OnDestroy, OnInit {
{ {
iconName: 'reader-outline', iconName: 'reader-outline',
label: $localize`General`, label: $localize`General`,
path: ['/' + $localize`faq`] path: ['/' + paths.faq]
}, },
{ {
iconName: 'cloudy-outline', iconName: 'cloudy-outline',
label: $localize`Cloud` + ' (SaaS)', label: $localize`Cloud` + ' (SaaS)',
path: ['/' + $localize`faq`, 'saas'], path: ['/' + paths.faq, paths.saas],
showCondition: this.hasPermissionForSubscription showCondition: this.hasPermissionForSubscription
}, },
{ {
iconName: 'server-outline', iconName: 'server-outline',
label: $localize`Self-Hosting`, label: $localize`Self-Hosting`,
path: ['/' + $localize`faq`, $localize`self-hosting`] path: ['/' + paths.faq, paths.selfHosting]
} }
]; ];
} }

7
apps/client/src/app/pages/faq/overview/faq-overview-page.component.ts

@ -1,5 +1,6 @@
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
@ -12,10 +13,8 @@ import { Subject, takeUntil } from 'rxjs';
standalone: false standalone: false
}) })
export class FaqOverviewPageComponent implements OnDestroy { export class FaqOverviewPageComponent implements OnDestroy {
public pricingUrl = public pricingUrl = `https://ghostfol.io/${document.documentElement.lang}/${paths.pricing}`;
`https://ghostfol.io/${document.documentElement.lang}/` + public routerLinkFeatures = ['/' + paths.features];
$localize`:snake-case:pricing`;
public routerLinkFeatures = ['/' + $localize`:snake-case:features`];
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();

11
apps/client/src/app/pages/faq/saas/saas-page.component.ts

@ -1,5 +1,6 @@
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
@ -12,11 +13,11 @@ import { Subject, takeUntil } from 'rxjs';
standalone: false standalone: false
}) })
export class SaasPageComponent implements OnDestroy { export class SaasPageComponent implements OnDestroy {
public pricingUrl = public pricingUrl = `https://ghostfol.io/${document.documentElement.lang}/${paths.pricing}`;
`https://ghostfol.io/${document.documentElement.lang}/` + public routerLinkAccount = ['/' + paths.account];
$localize`:snake-case:pricing`; public routerLinkAccountMembership = ['/' + paths.account, paths.membership];
public routerLinkMarkets = ['/' + $localize`:snake-case:markets`]; public routerLinkMarkets = ['/' + paths.markets];
public routerLinkRegister = ['/' + $localize`:snake-case:register`]; public routerLinkRegister = ['/' + paths.register];
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();

4
apps/client/src/app/pages/faq/saas/saas-page.html

@ -82,7 +82,7 @@
[enableLink]="false" [enableLink]="false"
/></a> /></a>
by signing up for Ghostfolio and applying for a trial (see by signing up for Ghostfolio and applying for a trial (see
<a [routerLink]="['/account', 'membership']">Membership</a>). It is <a [routerLink]="routerLinkAccountMembership">Membership</a>). It is
easy, free and there is no commitment. You can stop using it at any easy, free and there is no commitment. You can stop using it at any
time.</mat-card-content time.</mat-card-content
> >
@ -158,7 +158,7 @@
</mat-card-header> </mat-card-header>
<mat-card-content <mat-card-content
>It is suggested to regularly back up your data via >It is suggested to regularly back up your data via
<a [routerLink]="['/account']">My Ghostfolio</a> <a [routerLink]="routerLinkAccount">My Ghostfolio</a>
<i>Export Data</i>.</mat-card-content <i>Export Data</i>.</mat-card-content
> >
</mat-card> </mat-card>

6
apps/client/src/app/pages/faq/self-hosting/self-hosting-page.component.ts

@ -1,3 +1,5 @@
import { paths } from '@ghostfolio/common/paths';
import { Component, OnDestroy } from '@angular/core'; import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -9,9 +11,7 @@ import { Subject } from 'rxjs';
standalone: false standalone: false
}) })
export class SelfHostingPageComponent implements OnDestroy { export class SelfHostingPageComponent implements OnDestroy {
public pricingUrl = public pricingUrl = `https://ghostfol.io/${document.documentElement.lang}/${paths.pricing}`;
`https://ghostfol.io/${document.documentElement.lang}/` +
$localize`:snake-case:pricing`;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();

5
apps/client/src/app/pages/features/features-page.component.ts

@ -1,6 +1,7 @@
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { InfoItem, User } from '@ghostfolio/common/interfaces'; import { InfoItem, User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
@ -25,8 +26,8 @@ import { Subject, takeUntil } from 'rxjs';
export class GfFeaturesPageComponent implements OnDestroy { export class GfFeaturesPageComponent implements OnDestroy {
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
public info: InfoItem; public info: InfoItem;
public routerLinkRegister = ['/' + $localize`:snake-case:register`]; public routerLinkRegister = ['/' + paths.register];
public routerLinkResources = ['/' + $localize`:snake-case:resources`]; public routerLinkResources = ['/' + paths.resources];
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();

14
apps/client/src/app/pages/home/home-page-routing.module.ts

@ -4,6 +4,7 @@ import { HomeOverviewComponent } from '@ghostfolio/client/components/home-overvi
import { HomeSummaryComponent } from '@ghostfolio/client/components/home-summary/home-summary.component'; import { HomeSummaryComponent } from '@ghostfolio/client/components/home-summary/home-summary.component';
import { HomeWatchlistComponent } from '@ghostfolio/client/components/home-watchlist/home-watchlist.component'; import { HomeWatchlistComponent } from '@ghostfolio/client/components/home-watchlist/home-watchlist.component';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { paths } from '@ghostfolio/common/paths';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
@ -19,27 +20,22 @@ const routes: Routes = [
component: HomeOverviewComponent component: HomeOverviewComponent
}, },
{ {
path: 'holdings', path: paths.holdings,
component: HomeHoldingsComponent, component: HomeHoldingsComponent,
title: $localize`Holdings` title: $localize`Holdings`
}, },
{ {
path: 'holdings', path: paths.summary,
component: HomeHoldingsComponent,
title: $localize`Holdings`
},
{
path: 'summary',
component: HomeSummaryComponent, component: HomeSummaryComponent,
title: $localize`Summary` title: $localize`Summary`
}, },
{ {
path: 'market', path: paths.market,
component: HomeMarketComponent, component: HomeMarketComponent,
title: $localize`Markets` title: $localize`Markets`
}, },
{ {
path: 'watchlist', path: paths.watchlist,
component: HomeWatchlistComponent, component: HomeWatchlistComponent,
title: $localize`Watchlist` title: $localize`Watchlist`
} }

11
apps/client/src/app/pages/home/home-page.component.ts

@ -1,6 +1,7 @@
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { TabConfiguration, User } from '@ghostfolio/common/interfaces'; import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
@ -36,27 +37,27 @@ export class HomePageComponent implements OnDestroy, OnInit {
{ {
iconName: 'analytics-outline', iconName: 'analytics-outline',
label: $localize`Overview`, label: $localize`Overview`,
path: ['/home'] path: ['/' + paths.home]
}, },
{ {
iconName: 'wallet-outline', iconName: 'wallet-outline',
label: $localize`Holdings`, label: $localize`Holdings`,
path: ['/home', 'holdings'] path: ['/' + paths.home, paths.holdings]
}, },
{ {
iconName: 'reader-outline', iconName: 'reader-outline',
label: $localize`Summary`, label: $localize`Summary`,
path: ['/home', 'summary'] path: ['/' + paths.home, paths.summary]
}, },
{ {
iconName: 'bookmark-outline', iconName: 'bookmark-outline',
label: $localize`Watchlist`, label: $localize`Watchlist`,
path: ['/home', 'watchlist'] path: ['/' + paths.home, paths.watchlist]
}, },
{ {
iconName: 'newspaper-outline', iconName: 'newspaper-outline',
label: $localize`Markets`, label: $localize`Markets`,
path: ['/home', 'market'] path: ['/' + paths.home, paths.market]
} }
]; ];

7
apps/client/src/app/pages/landing/landing-page.component.ts

@ -1,5 +1,6 @@
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { Statistics } from '@ghostfolio/common/interfaces'; import { Statistics } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
@ -24,8 +25,10 @@ export class LandingPageComponent implements OnDestroy, OnInit {
public hasPermissionForStatistics: boolean; public hasPermissionForStatistics: boolean;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
public hasPermissionToCreateUser: boolean; public hasPermissionToCreateUser: boolean;
public routerLinkAbout = ['/' + $localize`:snake-case:about`]; public routerLinkAbout = ['/' + paths.about];
public routerLinkRegister = ['/' + $localize`:snake-case:register`]; public routerLinkDemo = ['/' + paths.demo];
public routerLinkOpenStartup = ['/' + paths.openStartup];
public routerLinkRegister = ['/' + paths.register];
public statistics: Statistics; public statistics: Statistics;
public testimonials = [ public testimonials = [
{ {

12
apps/client/src/app/pages/landing/landing-page.html

@ -46,7 +46,7 @@
@if (hasPermissionToCreateUser) { @if (hasPermissionToCreateUser) {
<div class="mx-3 text-muted" i18n>or</div> <div class="mx-3 text-muted" i18n>or</div>
} }
<a i18n mat-stroked-button [routerLink]="['/demo']">Live Demo</a> <a i18n mat-stroked-button [routerLink]="routerLinkDemo">Live Demo</a>
} }
</div> </div>
</div> </div>
@ -60,7 +60,7 @@
<a <a
class="d-block" class="d-block"
title="Ghostfolio in Numbers: Monthly Active Users (MAU)" title="Ghostfolio in Numbers: Monthly Active Users (MAU)"
[routerLink]="['/open']" [routerLink]="routerLinkOpenStartup"
> >
<gf-value <gf-value
i18n i18n
@ -78,7 +78,7 @@
<a <a
class="d-block" class="d-block"
title="Ghostfolio in Numbers: Stars on GitHub" title="Ghostfolio in Numbers: Stars on GitHub"
[routerLink]="['/open']" [routerLink]="routerLinkOpenStartup"
> >
<gf-value <gf-value
i18n i18n
@ -96,7 +96,7 @@
<a <a
class="d-block" class="d-block"
title="Ghostfolio in Numbers: Pulls on Docker Hub" title="Ghostfolio in Numbers: Pulls on Docker Hub"
[routerLink]="['/open']" [routerLink]="routerLinkOpenStartup"
> >
<gf-value <gf-value
i18n i18n
@ -448,7 +448,9 @@
</a> </a>
@if (hasPermissionForDemo) { @if (hasPermissionForDemo) {
<div class="mx-3 text-muted" i18n>or</div> <div class="mx-3 text-muted" i18n>or</div>
<a i18n mat-stroked-button [routerLink]="['/demo']">Live Demo</a> <a i18n mat-stroked-button [routerLink]="routerLinkDemo"
>Live Demo</a
>
} }
</div> </div>
</div> </div>

12
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -39,6 +39,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
return { id: assetSubClass, label: translate(assetSubClass) }; return { id: assetSubClass, label: translate(assetSubClass) };
}); });
public currencies: string[] = []; public currencies: string[] = [];
public currencyOfAssetProfile: string;
public currentMarketPrice = null; public currentMarketPrice = null;
public defaultDateFormat: string; public defaultDateFormat: string;
public isLoading = false; public isLoading = false;
@ -63,8 +64,10 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
) {} ) {}
public ngOnInit() { public ngOnInit() {
this.mode = this.data.activity.id ? 'update' : 'create'; this.currencyOfAssetProfile = this.data.activity?.SymbolProfile?.currency;
this.locale = this.data.user?.settings?.locale; this.locale = this.data.user?.settings?.locale;
this.mode = this.data.activity?.id ? 'update' : 'create';
this.dateAdapter.setLocale(this.locale); this.dateAdapter.setLocale(this.locale);
const { currencies, platforms } = this.dataService.fetchInfo(); const { currencies, platforms } = this.dataService.fetchInfo();
@ -210,7 +213,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.activityForm.get('type').value this.activityForm.get('type').value
) )
) { ) {
this.updateSymbol(); this.updateAssetProfile();
} }
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
@ -397,7 +400,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.dialogRef.close(activity); this.dialogRef.close(activity);
} else { } else {
(activity as UpdateOrderDto).id = this.data.activity.id; (activity as UpdateOrderDto).id = this.data.activity?.id;
await validateObjectForForm({ await validateObjectForForm({
classDto: UpdateOrderDto, classDto: UpdateOrderDto,
@ -422,7 +425,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private updateSymbol() { private updateAssetProfile() {
this.isLoading = true; this.isLoading = true;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
@ -450,6 +453,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.activityForm.get('dataSource').setValue(dataSource); this.activityForm.get('dataSource').setValue(dataSource);
} }
this.currencyOfAssetProfile = currency;
this.currentMarketPrice = marketPrice; this.currentMarketPrice = marketPrice;
this.isLoading = false; this.isLoading = false;

4
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -230,8 +230,10 @@
</div> </div>
</mat-form-field> </mat-form-field>
@if ( @if (
currencyOfAssetProfile ===
activityForm.get('currencyOfUnitPrice').value &&
currentMarketPrice && currentMarketPrice &&
(data.activity.type === 'BUY' || data.activity.type === 'SELL') && ['BUY', 'SELL'].includes(data.activity.type) &&
isToday(activityForm.get('date')?.value) isToday(activityForm.get('date')?.value)
) { ) {
<button <button

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save