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. 25
      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. 53
      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:
matrix:
node_version:
- 20
- 22
steps:
- name: Checkout code
uses: actions/checkout@v4

2
.github/workflows/docker-image.yml

@ -19,7 +19,7 @@ jobs:
id: meta
uses: docker/metadata-action@v4
with:
images: ghostfolio/ghostfolio
images: ${{ vars.DOCKER_REPOSITORY || 'ghostfolio/ghostfolio' }}
tags: |
type=semver,pattern={{major}}
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/),
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 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`
### 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
- Extended the data providers management of the admin control panel by the asset profile count
- Restricted the permissions of the demo user
- 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 Chinese (`zh`)
- 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 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 `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
- 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`
- Improved the text alignment of the allocations by ETF holding on the allocations page (experimental)
## 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
- 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`
### 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
- 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
- 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 `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
- 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 Polish (`pl`)
- 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 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
- 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 investment timeline grouped by year
- Set up the language localization for Français (`fr`)
- Set up the language localization for French (`fr`)
### Changed
@ -3808,7 +3841,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- 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 `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
- 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
@ -4059,7 +4092,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### 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
## 1.198.0 - 25.09.2022

2
DEVELOPMENT.md

@ -5,7 +5,7 @@
### Prerequisites
- [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)
- 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
WORKDIR /ghostfolio
@ -50,7 +50,7 @@ COPY package.json /ghostfolio/dist/apps/api
RUN npm run database:generate-typings
# 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"
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 { ApiService } from '@ghostfolio/api/services/api/api.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 { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import {
@ -16,7 +15,6 @@ import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import {
AdminData,
AdminMarketData,
AdminMarketDataDetails,
AdminUsers,
EnhancedSymbolProfile
} from '@ghostfolio/common/interfaces';
@ -50,8 +48,6 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AdminService } from './admin.service';
import { UpdateAssetProfileDto } from './update-asset-profile.dto';
import { UpdateBulkMarketDataDto } from './update-bulk-market-data.dto';
import { UpdateMarketDataDto } from './update-market-data.dto';
@Controller('admin')
export class AdminController {
@ -60,7 +56,6 @@ export class AdminController {
private readonly apiService: ApiService,
private readonly dataGatheringService: DataGatheringService,
private readonly manualService: ManualService,
private readonly marketDataService: MarketDataService,
@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)
@Post('market-data/:dataSource/:symbol/test')
@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)
@Post('profile-data/:dataSource/:symbol')
@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> {
const dataSources = await this.dataProviderService.getDataSources({ user });
const dataSources = await this.dataProviderService.getDataSources({
user,
includeGhostfolio: true
});
const [settings, transactionCount, userCount] = await Promise.all([
this.propertyService.get(),

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

@ -101,7 +101,7 @@ import { UserModule } from './user/user.module';
RedisCacheModule,
ScheduleModule.forRoot(),
ServeStaticModule.forRoot({
exclude: ['/api*', '/sitemap.xml'],
exclude: ['/api/*wildcard', '/sitemap.xml'],
rootPath: join(__dirname, '..', 'client'),
serveStaticOptions: {
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')
@HasPermission(permissions.enableDataProviderGhostfolio)
@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')
@HasPermission(permissions.enableDataProviderGhostfolio)
@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')
@HasPermission(permissions.enableDataProviderGhostfolio)
@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')
@HasPermission(permissions.enableDataProviderGhostfolio)
@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')
@HasPermission(permissions.enableDataProviderGhostfolio)
@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'] = {
accountClusterRisk:
summary.ordersCount > 0
summary.activityCount > 0
? await this.rulesService.evaluate(
[
new AccountClusterRiskCurrentInvestment(
@ -1265,7 +1265,7 @@ export class PortfolioService {
)
: undefined,
assetClassClusterRisk:
summary.ordersCount > 0
summary.activityCount > 0
? await this.rulesService.evaluate(
[
new AssetClassClusterRiskEquity(
@ -1281,7 +1281,7 @@ export class PortfolioService {
)
: undefined,
currencyClusterRisk:
summary.ordersCount > 0
summary.activityCount > 0
? await this.rulesService.evaluate(
[
new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
@ -1297,7 +1297,7 @@ export class PortfolioService {
)
: undefined,
economicMarketClusterRisk:
summary.ordersCount > 0
summary.activityCount > 0
? await this.rulesService.evaluate(
[
new EconomicMarketClusterRiskDevelopedMarkets(
@ -1338,7 +1338,7 @@ export class PortfolioService {
userSettings
),
regionalMarketClusterRisk:
summary.ordersCount > 0
summary.activityCount > 0
? await this.rulesService.evaluate(
[
new RegionalMarketClusterRiskAsiaPacific(
@ -1981,6 +1981,9 @@ export class PortfolioService {
netPerformanceWithCurrencyEffect,
totalBuy,
totalSell,
activityCount: activities.filter(({ type }) => {
return ['BUY', 'SELL'].includes(type);
}).length,
committedFunds: committedFunds.toNumber(),
currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(),
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
@ -2008,9 +2011,6 @@ export class PortfolioService {
interest: interest.toNumber(),
items: valuables.toNumber(),
liabilities: liabilities.toNumber(),
ordersCount: activities.filter(({ type }) => {
return ['BUY', 'SELL'].includes(type);
}).length,
totalInvestment: totalInvestment.toNumber(),
totalValueInBaseCurrency: netWorth
};

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

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

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

@ -317,7 +317,7 @@
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</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>
</url>
<url>
@ -383,7 +383,7 @@
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</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>
</url>
<url>

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

@ -1515,6 +1515,7 @@ describe('redactAttributes', () => {
}
},
summary: {
activityCount: 29,
annualizedPerformancePercent: 0.16690880197786,
annualizedPerformancePercentWithCurrencyEffect: 0.1694019484552876,
cash: null,
@ -1538,7 +1539,6 @@ describe('redactAttributes', () => {
interest: null,
items: null,
liabilities: null,
ordersCount: 29,
totalInvestment: null,
totalValueInBaseCurrency: null,
currentNetWorth: null
@ -3018,6 +3018,7 @@ describe('redactAttributes', () => {
}
},
summary: {
activityCount: 29,
annualizedPerformancePercent: 0.16690880197786,
annualizedPerformancePercentWithCurrencyEffect: 0.1694019484552876,
cash: null,
@ -3041,7 +3042,6 @@ describe('redactAttributes', () => {
interest: null,
items: null,
liabilities: null,
ordersCount: 29,
totalInvestment: null,
totalValueInBaseCurrency: 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;
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({
includeGhostfolio = false,
user
}: {
includeGhostfolio?: boolean;
user: UserWithSettings;
}): Promise<DataSource[]> {
let dataSourcesKey: 'DATA_SOURCES' | 'DATA_SOURCES_LEGACY' = 'DATA_SOURCES';
@ -187,7 +189,7 @@ export class DataProviderService {
PROPERTY_API_KEY_GHOSTFOLIO
)) as string;
if (ghostfolioApiKey || hasRole(user, 'ADMIN')) {
if (includeGhostfolio || ghostfolioApiKey) {
dataSources.push('GHOSTFOLIO');
}
@ -663,9 +665,6 @@ export class DataProviderService {
// Only allow symbols with supported currency
return currency ? true : false;
})
.sort(({ name: name1 }, { name: name2 }) => {
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
})
.map((lookupItem) => {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
if (user.subscription.type === 'Premium') {
@ -679,7 +678,21 @@ export class DataProviderService {
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;
})
.sort(({ name: name1 }, { name: name2 }) => {
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
});
return {

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

@ -30,8 +30,11 @@ import {
HistoricalDividendsResult,
HistoricalHistoryResult
} from 'yahoo-finance2/esm/src/modules/historical';
import { Quote } from 'yahoo-finance2/esm/src/modules/quote';
import { SearchQuoteNonYahoo } from 'yahoo-finance2/script/src/modules/search';
import {
Quote,
QuoteResponseArray
} from 'yahoo-finance2/esm/src/modules/quote';
import { SearchQuoteNonYahoo } from 'yahoo-finance2/esm/src/modules/search';
@Injectable()
export class YahooFinanceService implements DataProviderInterface {
@ -281,11 +284,19 @@ export class YahooFinanceService implements DataProviderInterface {
return true;
});
const marketData = await this.yahooFinance.quote(
quotes.map(({ symbol }) => {
return symbol;
})
);
let marketData: QuoteResponseArray = [];
try {
marketData = await this.yahooFinance.quote(
quotes.map(({ symbol }) => {
return symbol;
})
);
} catch (error) {
if (error?.result?.length > 0) {
marketData = error.result;
}
}
for (const marketDataItem of marketData) {
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 { paths } from '@ghostfolio/client/core/paths';
import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strategy';
import { paths } from '@ghostfolio/common/paths';
import { NgModule } from '@angular/core';
import { RouterModule, Routes, TitleStrategy } from '@angular/router';
@ -14,21 +14,21 @@ const routes: Routes = [
import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
},
{
path: 'account',
path: paths.account,
loadChildren: () =>
import('./pages/user-account/user-account-page.module').then(
(m) => m.UserAccountPageModule
)
},
{
path: 'accounts',
path: paths.accounts,
loadChildren: () =>
import('./pages/accounts/accounts-page.module').then(
(m) => m.AccountsPageModule
)
},
{
path: 'admin',
path: paths.adminControl,
loadChildren: () =>
import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule)
},
@ -38,16 +38,16 @@ const routes: Routes = [
import('./pages/api/api-page.component').then(
(c) => c.GfApiPageComponent
),
path: 'api',
path: paths.api,
title: 'Ghostfolio API'
},
{
path: 'auth',
path: paths.auth,
loadChildren: () =>
import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule)
},
{
path: 'blog',
path: paths.blog,
loadChildren: () =>
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
},
@ -57,7 +57,7 @@ const routes: Routes = [
import('./pages/demo/demo-page.component').then(
(c) => c.GfDemoPageComponent
),
path: 'demo'
path: paths.demo
},
{
path: paths.faq,
@ -74,7 +74,7 @@ const routes: Routes = [
title: $localize`Features`
},
{
path: 'home',
path: paths.home,
loadChildren: () =>
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
},
@ -84,7 +84,7 @@ const routes: Routes = [
import('./pages/i18n/i18n-page.component').then(
(c) => c.GfI18nPageComponent
),
path: 'i18n',
path: paths.i18n,
title: $localize`Internationalization`
},
{
@ -95,19 +95,12 @@ const routes: Routes = [
)
},
{
path: 'open',
path: paths.openStartup,
loadChildren: () =>
import('./pages/open/open-page.module').then((m) => m.OpenPageModule)
},
{
path: 'p',
loadChildren: () =>
import('./pages/public/public-page.module').then(
(m) => m.PublicPageModule
)
},
{
path: 'portfolio',
path: paths.portfolio,
loadChildren: () =>
import('./pages/portfolio/portfolio-page.module').then(
(m) => m.PortfolioPageModule
@ -120,6 +113,13 @@ const routes: Routes = [
(m) => m.PricingPageModule
)
},
{
path: paths.public,
loadChildren: () =>
import('./pages/public/public-page.module').then(
(m) => m.PublicPageModule
)
},
{
path: paths.register,
loadChildren: () =>
@ -135,7 +135,7 @@ const routes: Routes = [
)
},
{
path: 'start',
path: paths.start,
loadChildren: () =>
import('./pages/landing/landing-page.module').then(
(m) => m.LandingPageModule
@ -146,11 +146,11 @@ const routes: Routes = [
import('./pages/webauthn/webauthn-page.component').then(
(c) => c.GfWebauthnPageComponent
),
path: 'webauthn',
path: paths.webauthn,
title: $localize`Sign in`
},
{
path: 'zen',
path: paths.zen,
loadChildren: () =>
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>
@if (hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
}
<li>
@ -91,7 +91,7 @@
}
@if (hasPermissionForStatistics) {
<li>
<a [routerLink]="['/open']">Open Startup</a>
<a [routerLink]="routerLinkOpenStartup">Open Startup</a>
</li>
}
@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 { getCssVariable } from '@ghostfolio/common/helper';
import { InfoItem, User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ColorScheme } from '@ghostfolio/common/types';
@ -62,29 +63,25 @@ export class AppComponent implements OnDestroy, OnInit {
public hasTabs = false;
public info: InfoItem;
public pageTitle: string;
public routerLinkAbout = ['/' + $localize`:snake-case:about`];
public routerLinkAboutChangelog = [
'/' + $localize`:snake-case:about`,
'changelog'
];
public routerLinkAboutLicense = [
'/' + $localize`:snake-case:about`,
$localize`:snake-case:license`
];
public routerLinkAbout = ['/' + paths.about];
public routerLinkAboutChangelog = ['/' + paths.about, paths.changelog];
public routerLinkAboutLicense = ['/' + paths.about, paths.license];
public routerLinkAboutPrivacyPolicy = [
'/' + $localize`:snake-case:about`,
$localize`:snake-case:privacy-policy`
'/' + paths.about,
paths.privacyPolicy
];
public routerLinkAboutTermsOfService = [
'/' + $localize`:snake-case:about`,
$localize`:snake-case:terms-of-service`
'/' + paths.about,
paths.termsOfService
];
public routerLinkFaq = ['/' + $localize`:snake-case:faq`];
public routerLinkFeatures = ['/' + $localize`:snake-case:features`];
public routerLinkMarkets = ['/' + $localize`:snake-case:markets`];
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`];
public routerLinkRegister = ['/' + $localize`:snake-case:register`];
public routerLinkResources = ['/' + $localize`:snake-case:resources`];
public routerLinkBlog = ['/' + paths.blog];
public routerLinkFaq = ['/' + paths.faq];
public routerLinkFeatures = ['/' + paths.features];
public routerLinkMarkets = ['/' + paths.markets];
public routerLinkOpenStartup = ['/' + paths.openStartup];
public routerLinkPricing = ['/' + paths.pricing];
public routerLinkRegister = ['/' + paths.register];
public routerLinkResources = ['/' + paths.resources];
public showFooter = false;
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 { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { Access, User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { Clipboard } from '@angular/cdk/clipboard';
import {
@ -55,7 +56,7 @@ export class AccessTableComponent implements OnChanges {
public getPublicUrl(aId: string): string {
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 {

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

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

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

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

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

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

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

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

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

@ -20,9 +20,9 @@
mat-flat-button
[ngClass]="{
'font-weight-bold':
currentRoute === 'home' || currentRoute === 'zen',
currentRoute === paths.home || currentRoute === paths.zen,
'text-decoration-underline':
currentRoute === 'home' || currentRoute === 'zen'
currentRoute === paths.home || currentRoute === paths.zen
}"
[routerLink]="['/']"
>Overview</a
@ -34,10 +34,10 @@
i18n
mat-flat-button
[ngClass]="{
'font-weight-bold': currentRoute === 'portfolio',
'text-decoration-underline': currentRoute === 'portfolio'
'font-weight-bold': currentRoute === paths.portfolio,
'text-decoration-underline': currentRoute === paths.portfolio
}"
[routerLink]="['/portfolio']"
[routerLink]="routerLinkPortfolio"
>Portfolio</a
>
</li>
@ -47,10 +47,10 @@
i18n
mat-flat-button
[ngClass]="{
'font-weight-bold': currentRoute === 'accounts',
'text-decoration-underline': currentRoute === 'accounts'
'font-weight-bold': currentRoute === paths.accounts,
'text-decoration-underline': currentRoute === paths.accounts
}"
[routerLink]="['/accounts']"
[routerLink]="routerLinkAccounts"
>Accounts</a
>
</li>
@ -61,10 +61,10 @@
i18n
mat-flat-button
[ngClass]="{
'font-weight-bold': currentRoute === 'admin',
'text-decoration-underline': currentRoute === 'admin'
'font-weight-bold': currentRoute === paths.adminControl,
'text-decoration-underline': currentRoute === paths.adminControl
}"
[routerLink]="['/admin']"
[routerLink]="routerLinkAdminControl"
>Admin Control</a
>
</li>
@ -235,7 +235,7 @@
mat-menu-item
[ngClass]="{
'font-weight-bold':
currentRoute === 'home' || currentRoute === 'zen'
currentRoute === paths.home || currentRoute === paths.zen
}"
[routerLink]="['/']"
>Overview</a
@ -245,24 +245,24 @@
i18n
mat-menu-item
[ngClass]="{
'font-weight-bold': currentRoute === 'portfolio'
'font-weight-bold': currentRoute === paths.portfolio
}"
[routerLink]="['/portfolio']"
[routerLink]="routerLinkPortfolio"
>Portfolio</a
>
<a
class="d-flex d-sm-none"
i18n
mat-menu-item
[ngClass]="{ 'font-weight-bold': currentRoute === 'accounts' }"
[routerLink]="['/accounts']"
[ngClass]="{ 'font-weight-bold': currentRoute === paths.accounts }"
[routerLink]="routerLinkAccounts"
>Accounts</a
>
<a
i18n
mat-menu-item
[ngClass]="{ 'font-weight-bold': currentRoute === 'account' }"
[routerLink]="['/account']"
[ngClass]="{ 'font-weight-bold': currentRoute === paths.account }"
[routerLink]="routerLinkAccount"
>My Ghostfolio</a
>
@if (hasPermissionToAccessAdminControl) {
@ -270,8 +270,10 @@
class="d-flex d-sm-none"
i18n
mat-menu-item
[ngClass]="{ 'font-weight-bold': currentRoute === 'admin' }"
[routerLink]="['/admin']"
[ngClass]="{
'font-weight-bold': currentRoute === paths.adminControl
}"
[routerLink]="routerLinkAdminControl"
>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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { Filter, InfoItem, User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
import { GfAssistantComponent } from '@ghostfolio/ui/assistant/assistant.component';
@ -79,17 +80,22 @@ export class HeaderComponent implements OnChanges {
public hasPermissionToCreateUser: boolean;
public impersonationId: string;
public isMenuOpen: boolean;
public routeAbout = $localize`:snake-case:about`;
public routeFeatures = $localize`:snake-case:features`;
public routeMarkets = $localize`:snake-case:markets`;
public routePricing = $localize`:snake-case:pricing`;
public routeResources = $localize`:snake-case:resources`;
public routerLinkAbout = ['/' + $localize`:snake-case:about`];
public routerLinkFeatures = ['/' + $localize`:snake-case:features`];
public routerLinkMarkets = ['/' + $localize`:snake-case:markets`];
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`];
public routerLinkRegister = ['/' + $localize`:snake-case:register`];
public routerLinkResources = ['/' + $localize`:snake-case:resources`];
public paths = paths;
public routeAbout = paths.about;
public routeFeatures = paths.features;
public routeMarkets = paths.markets;
public routePricing = paths.pricing;
public routeResources = paths.resources;
public routerLinkAbout = ['/' + paths.about];
public routerLinkAccount = ['/' + paths.account];
public routerLinkAccounts = ['/' + paths.accounts];
public routerLinkAdminControl = ['/' + paths.adminControl];
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>();

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

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

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

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

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

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

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

@ -36,6 +36,14 @@
[locale]="user?.settings?.locale || undefined"
[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>

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

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

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

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

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

@ -7,11 +7,11 @@
</div>
<div
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">
{{ summary?.ordersCount }}
<ng-container i18n>{summary?.ordersCount, plural,
{{ summary?.activityCount }}
<ng-container i18n>{summary?.activityCount, plural,
=1 {activity}
other {activities}
}</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 {
ChangeDetectionStrategy,
ChangeDetectorRef,
@ -26,7 +28,7 @@ export class SubscriptionInterstitialDialog implements OnInit {
public remainingSkipButtonDelay =
SubscriptionInterstitialDialog.SKIP_BUTTON_DELAY_IN_SECONDS;
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`];
public routerLinkPricing = ['/' + paths.pricing];
public variantIndex: number;
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 { getDateFormatString } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import {
@ -36,7 +37,7 @@ export class UserAccountMembershipComponent implements OnDestroy {
public hasPermissionToUpdateUserSettings: boolean;
public price: number;
public priceId: string;
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`];
public routerLinkPricing = ['/' + paths.pricing];
public trySubscriptionMail =
'mailto:hi@ghostfol.io?Subject=Ghostfolio Premium Trial&body=Hello%0D%0DI am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?%0D%0DKind regards';
public user: User;

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

@ -1,6 +1,7 @@
import { DataService } from '@ghostfolio/client/services/data.service';
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { paths } from '@ghostfolio/common/paths';
import { Injectable } from '@angular/core';
import {
@ -11,20 +12,18 @@ import {
import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { paths } from './paths';
@Injectable({ providedIn: 'root' })
export class AuthGuard {
private static PUBLIC_PAGE_ROUTES = [
`/${paths.about}`,
'/blog',
'/demo',
`/${paths.blog}`,
`/${paths.demo}`,
`/${paths.faq}`,
`/${paths.features}`,
`/${paths.markets}`,
'/open',
'/p',
`/${paths.openStartup}`,
`/${paths.pricing}`,
`/${paths.public}`,
`/${paths.register}`,
`/${paths.resources}`
];
@ -49,21 +48,21 @@ export class AuthGuard {
.pipe(
catchError(() => {
if (utmSource === 'ios') {
this.router.navigate(['/demo']);
this.router.navigate(['/' + paths.demo]);
resolve(false);
} else if (utmSource === 'trusted-web-activity') {
this.router.navigate(['/' + $localize`register`]);
this.router.navigate(['/' + paths.register]);
resolve(false);
} else if (
AuthGuard.PUBLIC_PAGE_ROUTES.filter((publicPageRoute) => {
const [, url] = state.url.split('/');
AuthGuard.PUBLIC_PAGE_ROUTES.some((publicPageRoute) => {
const [, url] = decodeURIComponent(state.url).split('/');
return `/${url}` === publicPageRoute;
})?.length > 0
})
) {
resolve(true);
return EMPTY;
} else if (state.url !== '/start') {
this.router.navigate(['/start']);
this.router.navigate(['/' + paths.start]);
resolve(false);
return EMPTY;
}
@ -89,26 +88,26 @@ export class AuthGuard {
resolve(true);
return;
} else if (
state.url.startsWith('/home') &&
state.url.startsWith(`/${paths.home}`) &&
user.settings.viewMode === 'ZEN'
) {
this.router.navigate(['/zen']);
this.router.navigate(['/' + paths.zen]);
resolve(false);
return;
} else if (state.url.startsWith('/start')) {
} else if (state.url.startsWith(`/${paths.start}`)) {
if (user.settings.viewMode === 'ZEN') {
this.router.navigate(['/zen']);
this.router.navigate(['/' + paths.zen]);
} else {
this.router.navigate(['/home']);
this.router.navigate(['/' + paths.home]);
}
resolve(false);
return;
} else if (
state.url.startsWith('/zen') &&
state.url.startsWith(`/${paths.zen}`) &&
user.settings.viewMode === 'DEFAULT'
) {
this.router.navigate(['/home']);
this.router.navigate(['/' + paths.home]);
resolve(false);
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 { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import {
HTTP_INTERCEPTORS,
@ -74,7 +75,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
});
this.snackBarRef.onAction().subscribe(() => {
this.router.navigate(['/' + $localize`pricing`]);
this.router.navigate(['/' + paths.pricing]);
});
}
} else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) {
@ -110,7 +111,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
} else if (error.status === StatusCodes.UNAUTHORIZED) {
if (!error.url.includes('/data-providers/ghostfolio/status')) {
if (this.webAuthnService.isEnabled()) {
this.router.navigate(['/webauthn']);
this.router.navigate(['/' + paths.webauthn]);
} else {
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 { paths } from '@ghostfolio/client/core/paths';
import { paths } from '@ghostfolio/common/paths';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
@ -18,7 +18,7 @@ const routes: Routes = [
)
},
{
path: 'changelog',
path: paths.changelog,
loadChildren: () =>
import('./changelog/changelog-page.module').then(
(m) => m.ChangelogPageModule
@ -32,7 +32,7 @@ const routes: Routes = [
)
},
{
path: 'oss-friends',
path: paths.ossFriends,
loadChildren: () =>
import('./oss-friends/oss-friends-page.module').then(
(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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
@ -43,17 +44,17 @@ export class AboutPageComponent implements OnDestroy, OnInit {
{
iconName: 'information-circle-outline',
label: $localize`About`,
path: ['/' + $localize`about`]
path: ['/' + paths.about]
},
{
iconName: 'sparkles-outline',
label: $localize`Changelog`,
path: ['/' + $localize`about`, 'changelog']
path: ['/' + paths.about, paths.changelog]
},
{
iconName: 'ribbon-outline',
label: $localize`License`,
path: ['/' + $localize`about`, $localize`license`],
path: ['/' + paths.about, paths.license],
showCondition: !this.hasPermissionForSubscription
}
];
@ -62,14 +63,14 @@ export class AboutPageComponent implements OnDestroy, OnInit {
this.tabs.push({
iconName: 'shield-checkmark-outline',
label: $localize`Privacy Policy`,
path: ['/' + $localize`about`, $localize`privacy-policy`],
path: ['/' + paths.about, paths.privacyPolicy],
showCondition: this.hasPermissionForSubscription
});
this.tabs.push({
iconName: 'document-text-outline',
label: $localize`Terms of Service`,
path: ['/' + $localize`about`, $localize`terms-of-service`],
path: ['/' + paths.about, paths.termsOfService],
showCondition: this.hasPermissionForSubscription
});
@ -81,7 +82,7 @@ export class AboutPageComponent implements OnDestroy, OnInit {
this.tabs.push({
iconName: 'happy-outline',
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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
@ -17,8 +18,10 @@ export class AboutOverviewPageComponent implements OnDestroy, OnInit {
public hasPermissionForStatistics: boolean;
public hasPermissionForSubscription: boolean;
public isLoggedIn: boolean;
public routerLinkFaq = ['/' + $localize`:snake-case:faq`];
public routerLinkFeatures = ['/' + $localize`:snake-case:features`];
public routerLinkBlog = ['/' + paths.blog];
public routerLinkFaq = ['/' + paths.faq];
public routerLinkFeatures = ['/' + paths.features];
public routerLinkOpenStartup = ['/' + paths.openStartup];
public user: User;
private unsubscribeSubject = new Subject<void>();

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

@ -23,7 +23,9 @@
>
@if (hasPermissionForStatistics) {
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
}
. The project has been initiated by
@ -160,7 +162,7 @@
class="py-4 w-100"
color="primary"
mat-flat-button
[routerLink]="['/blog']"
[routerLink]="routerLinkBlog"
>Blog</a
>
</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 { AdminUsersComponent } from '@ghostfolio/client/components/admin-users/admin-users.component';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { paths } from '@ghostfolio/common/paths';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
@ -20,22 +21,22 @@ const routes: Routes = [
title: $localize`Admin Control`
},
{
path: 'jobs',
path: paths.jobs,
component: AdminJobsComponent,
title: $localize`Job Queue`
},
{
path: 'market-data',
path: paths.marketData,
component: AdminMarketDataComponent,
title: $localize`Market Data`
},
{
path: 'settings',
path: paths.settings,
component: AdminSettingsComponent,
title: $localize`Settings`
},
{
path: 'users',
path: paths.users,
component: AdminUsersComponent,
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 { paths } from '@ghostfolio/common/paths';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { DeviceDetectorService } from 'ngx-device-detector';
@ -26,27 +27,27 @@ export class AdminPageComponent implements OnDestroy, OnInit {
{
iconName: 'reader-outline',
label: $localize`Overview`,
path: ['/admin']
path: ['/' + paths.adminControl]
},
{
iconName: 'settings-outline',
label: $localize`Settings`,
path: ['/admin', 'settings']
path: ['/' + paths.adminControl, paths.settings]
},
{
iconName: 'server-outline',
label: $localize`Market Data`,
path: ['/admin', 'market-data']
path: ['/' + paths.adminControl, paths.marketData]
},
{
iconName: 'flash-outline',
label: $localize`Job Queue`,
path: ['/admin', 'jobs']
path: ['/' + paths.adminControl, paths.jobs]
},
{
iconName: 'people-outline',
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -9,6 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './hallo-ghostfolio-page.html'
})
export class HalloGhostfolioPageComponent {
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`];
public routerLinkResources = ['/' + $localize`:snake-case:resources`];
public routerLinkBlog = ['/' + paths.blog];
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -9,6 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './hello-ghostfolio-page.html'
})
export class HelloGhostfolioPageComponent {
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`];
public routerLinkResources = ['/' + $localize`:snake-case:resources`];
public routerLinkBlog = ['/' + paths.blog];
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -9,5 +11,6 @@ import { RouterModule } from '@angular/router';
templateUrl: './first-months-in-open-source-page.html'
})
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -8,4 +10,6 @@ import { RouterModule } from '@angular/router';
selector: 'gf-ghostfolio-meets-internet-identity-page',
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
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'
})
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -9,6 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './500-stars-on-github-page.html'
})
export class FiveHundredStarsOnGitHubPageComponent {
public routerLinkMarkets = ['/' + $localize`:snake-case:markets`];
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`];
public routerLinkBlog = ['/' + paths.blog];
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -8,4 +10,6 @@ import { RouterModule } from '@angular/router';
selector: 'gf-hacktoberfest-2022-page',
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { Component } from '@angular/core';
@ -11,6 +12,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './black-friday-2022-page.html'
})
export class BlackFriday2022PageComponent {
public routerLinkFeatures = ['/' + $localize`:snake-case:features`];
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`];
public routerLinkBlog = ['/' + paths.blog];
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -8,4 +10,6 @@ import { RouterModule } from '@angular/router';
selector: 'gf-the-importance-of-tracking-your-personal-finances-page',
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -8,4 +10,6 @@ import { RouterModule } from '@angular/router';
selector: 'gf-ghostfolio-auf-sackgeld-vorgestellt-page',
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -8,4 +10,6 @@ import { RouterModule } from '@angular/router';
selector: 'gf-ghostfolio-meets-umbrel-page',
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -9,6 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './1000-stars-on-github-page.html'
})
export class ThousandStarsOnGitHubPageComponent {
public routerLinkFeatures = ['/' + $localize`:snake-case:features`];
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`];
public routerLinkBlog = ['/' + paths.blog];
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -9,6 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './unlock-your-financial-potential-with-ghostfolio-page.html'
})
export class UnlockYourFinancialPotentialWithGhostfolioPageComponent {
public routerLinkFeatures = ['/' + $localize`:snake-case:features`];
public routerLinkResources = ['/' + $localize`:snake-case:resources`];
public routerLinkBlog = ['/' + paths.blog];
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -9,5 +11,6 @@ import { RouterModule } from '@angular/router';
templateUrl: './exploring-the-path-to-fire-page.html'
})
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -9,8 +11,6 @@ import { RouterModule } from '@angular/router';
templateUrl: './ghostfolio-joins-oss-friends-page.html'
})
export class GhostfolioJoinsOssFriendsPageComponent {
public routerLinkAboutOssFriends = [
'/' + $localize`:snake-case:about`,
'oss-friends'
];
public routerLinkAboutOssFriends = ['/' + paths.about, paths.ossFriends];
public routerLinkBlog = ['/' + paths.blog];
}

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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -9,11 +11,9 @@ import { RouterModule } from '@angular/router';
templateUrl: './ghostfolio-2-page.html'
})
export class Ghostfolio2PageComponent {
public routerLinkAbout = ['/' + $localize`:snake-case:about`];
public routerLinkAboutChangelog = [
'/' + $localize`:snake-case:about`,
'changelog'
];
public routerLinkFeatures = ['/' + $localize`:snake-case:features`];
public routerLinkMarkets = ['/' + $localize`:snake-case:markets`];
public routerLinkAbout = ['/' + paths.about];
public routerLinkAboutChangelog = ['/' + paths.about, paths.changelog];
public routerLinkBlog = ['/' + paths.blog];
public routerLinkFeatures = ['/' + paths.features];
public routerLinkMarkets = ['/' + paths.markets];
}

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

@ -270,7 +270,7 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -9,5 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './hacktoberfest-2023-page.html'
})
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>
The software is used daily by a thriving global community. With over
<a [routerLink]="['/open']">2’600 stars on GitHub</a> and
<a [routerLink]="['/open']">300’000+ pulls on Docker Hub</a>,
Ghostfolio has gained widespread recognition for its user-friendly
experience and simplicity.
<a [routerLink]="routerLinkOpenStartup">2’600 stars on GitHub</a>
and
<a [routerLink]="routerLinkOpenStartup"
>300’000+ pulls on Docker Hub</a
>, Ghostfolio has gained widespread recognition for its
user-friendly experience and simplicity.
</p>
</section>
<section class="mb-4">
@ -178,7 +180,7 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { Component } from '@angular/core';
@ -11,6 +12,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './black-week-2023-page.html'
})
export class BlackWeek2023PageComponent {
public routerLinkFeatures = ['/' + $localize`:snake-case:features`];
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`];
public routerLinkBlog = ['/' + paths.blog];
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -9,6 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './hacktoberfest-2023-debriefing-page.html'
})
export class Hacktoberfest2023DebriefingPageComponent {
public routerLinkAbout = ['/' + $localize`:snake-case:about`];
public routerLinkFeatures = ['/' + $localize`:snake-case:features`];
public routerLinkAbout = ['/' + paths.about];
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@ -9,5 +11,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './hacktoberfest-2024-page.html'
})
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>
The OSS project counting more than 100 contributors is used daily by
its growing global community. With over
<a [routerLink]="['/open']">4’000 stars on GitHub</a> and
<a [routerLink]="['/open']">800’000+ pulls on Docker Hub</a>,
Ghostfolio has gained widespread recognition for its user-friendly
experience and simplicity.
<a [routerLink]="routerLinkOpenStartup">4’000 stars on GitHub</a>
and
<a [routerLink]="routerLinkOpenStartup"
>800’000+ pulls on Docker Hub</a
>, Ghostfolio has gained widespread recognition for its
user-friendly experience and simplicity.
</p>
</section>
<section class="mb-4">
@ -184,7 +186,7 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { Component } from '@angular/core';
@ -11,6 +12,7 @@ import { RouterModule } from '@angular/router';
templateUrl: './black-weeks-2024-page.html'
})
export class BlackWeeks2024PageComponent {
public routerLinkFeatures = ['/' + $localize`:snake-case:features`];
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`];
public routerLinkBlog = ['/' + paths.blog];
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">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
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 { paths } from '@ghostfolio/common/paths';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
@ -17,12 +18,12 @@ const routes: Routes = [
)
},
{
path: 'saas',
path: paths.saas,
loadChildren: () =>
import('./saas/saas-page.module').then((m) => m.SaasPageModule)
},
{
path: 'self-hosting',
path: paths.selfHosting,
loadChildren: () =>
import('./self-hosting/self-hosting-page.module').then(
(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 { TabConfiguration } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Component, OnDestroy, OnInit } from '@angular/core';
@ -35,18 +36,18 @@ export class FaqPageComponent implements OnDestroy, OnInit {
{
iconName: 'reader-outline',
label: $localize`General`,
path: ['/' + $localize`faq`]
path: ['/' + paths.faq]
},
{
iconName: 'cloudy-outline',
label: $localize`Cloud` + ' (SaaS)',
path: ['/' + $localize`faq`, 'saas'],
path: ['/' + paths.faq, paths.saas],
showCondition: this.hasPermissionForSubscription
},
{
iconName: 'server-outline',
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 { User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
@ -12,10 +13,8 @@ import { Subject, takeUntil } from 'rxjs';
standalone: false
})
export class FaqOverviewPageComponent implements OnDestroy {
public pricingUrl =
`https://ghostfol.io/${document.documentElement.lang}/` +
$localize`:snake-case:pricing`;
public routerLinkFeatures = ['/' + $localize`:snake-case:features`];
public pricingUrl = `https://ghostfol.io/${document.documentElement.lang}/${paths.pricing}`;
public routerLinkFeatures = ['/' + paths.features];
public user: User;
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 { User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
@ -12,11 +13,11 @@ import { Subject, takeUntil } from 'rxjs';
standalone: false
})
export class SaasPageComponent implements OnDestroy {
public pricingUrl =
`https://ghostfol.io/${document.documentElement.lang}/` +
$localize`:snake-case:pricing`;
public routerLinkMarkets = ['/' + $localize`:snake-case:markets`];
public routerLinkRegister = ['/' + $localize`:snake-case:register`];
public pricingUrl = `https://ghostfol.io/${document.documentElement.lang}/${paths.pricing}`;
public routerLinkAccount = ['/' + paths.account];
public routerLinkAccountMembership = ['/' + paths.account, paths.membership];
public routerLinkMarkets = ['/' + paths.markets];
public routerLinkRegister = ['/' + paths.register];
public user: User;
private unsubscribeSubject = new Subject<void>();

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

@ -82,7 +82,7 @@
[enableLink]="false"
/></a>
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
time.</mat-card-content
>
@ -158,7 +158,7 @@
</mat-card-header>
<mat-card-content
>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
>
</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 { Subject } from 'rxjs';
@ -9,9 +11,7 @@ import { Subject } from 'rxjs';
standalone: false
})
export class SelfHostingPageComponent implements OnDestroy {
public pricingUrl =
`https://ghostfol.io/${document.documentElement.lang}/` +
$localize`:snake-case:pricing`;
public pricingUrl = `https://ghostfol.io/${document.documentElement.lang}/${paths.pricing}`;
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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { InfoItem, User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
@ -25,8 +26,8 @@ import { Subject, takeUntil } from 'rxjs';
export class GfFeaturesPageComponent implements OnDestroy {
public hasPermissionForSubscription: boolean;
public info: InfoItem;
public routerLinkRegister = ['/' + $localize`:snake-case:register`];
public routerLinkResources = ['/' + $localize`:snake-case:resources`];
public routerLinkRegister = ['/' + paths.register];
public routerLinkResources = ['/' + paths.resources];
public user: User;
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 { HomeWatchlistComponent } from '@ghostfolio/client/components/home-watchlist/home-watchlist.component';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { paths } from '@ghostfolio/common/paths';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
@ -19,27 +20,22 @@ const routes: Routes = [
component: HomeOverviewComponent
},
{
path: 'holdings',
path: paths.holdings,
component: HomeHoldingsComponent,
title: $localize`Holdings`
},
{
path: 'holdings',
component: HomeHoldingsComponent,
title: $localize`Holdings`
},
{
path: 'summary',
path: paths.summary,
component: HomeSummaryComponent,
title: $localize`Summary`
},
{
path: 'market',
path: paths.market,
component: HomeMarketComponent,
title: $localize`Markets`
},
{
path: 'watchlist',
path: paths.watchlist,
component: HomeWatchlistComponent,
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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { DeviceDetectorService } from 'ngx-device-detector';
@ -36,27 +37,27 @@ export class HomePageComponent implements OnDestroy, OnInit {
{
iconName: 'analytics-outline',
label: $localize`Overview`,
path: ['/home']
path: ['/' + paths.home]
},
{
iconName: 'wallet-outline',
label: $localize`Holdings`,
path: ['/home', 'holdings']
path: ['/' + paths.home, paths.holdings]
},
{
iconName: 'reader-outline',
label: $localize`Summary`,
path: ['/home', 'summary']
path: ['/' + paths.home, paths.summary]
},
{
iconName: 'bookmark-outline',
label: $localize`Watchlist`,
path: ['/home', 'watchlist']
path: ['/' + paths.home, paths.watchlist]
},
{
iconName: 'newspaper-outline',
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 { Statistics } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Component, OnDestroy, OnInit } from '@angular/core';
@ -24,8 +25,10 @@ export class LandingPageComponent implements OnDestroy, OnInit {
public hasPermissionForStatistics: boolean;
public hasPermissionForSubscription: boolean;
public hasPermissionToCreateUser: boolean;
public routerLinkAbout = ['/' + $localize`:snake-case:about`];
public routerLinkRegister = ['/' + $localize`:snake-case:register`];
public routerLinkAbout = ['/' + paths.about];
public routerLinkDemo = ['/' + paths.demo];
public routerLinkOpenStartup = ['/' + paths.openStartup];
public routerLinkRegister = ['/' + paths.register];
public statistics: Statistics;
public testimonials = [
{

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

@ -46,7 +46,7 @@
@if (hasPermissionToCreateUser) {
<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>
@ -60,7 +60,7 @@
<a
class="d-block"
title="Ghostfolio in Numbers: Monthly Active Users (MAU)"
[routerLink]="['/open']"
[routerLink]="routerLinkOpenStartup"
>
<gf-value
i18n
@ -78,7 +78,7 @@
<a
class="d-block"
title="Ghostfolio in Numbers: Stars on GitHub"
[routerLink]="['/open']"
[routerLink]="routerLinkOpenStartup"
>
<gf-value
i18n
@ -96,7 +96,7 @@
<a
class="d-block"
title="Ghostfolio in Numbers: Pulls on Docker Hub"
[routerLink]="['/open']"
[routerLink]="routerLinkOpenStartup"
>
<gf-value
i18n
@ -448,7 +448,9 @@
</a>
@if (hasPermissionForDemo) {
<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>

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) };
});
public currencies: string[] = [];
public currencyOfAssetProfile: string;
public currentMarketPrice = null;
public defaultDateFormat: string;
public isLoading = false;
@ -63,8 +64,10 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
) {}
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.mode = this.data.activity?.id ? 'update' : 'create';
this.dateAdapter.setLocale(this.locale);
const { currencies, platforms } = this.dataService.fetchInfo();
@ -210,7 +213,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.activityForm.get('type').value
)
) {
this.updateSymbol();
this.updateAssetProfile();
}
this.changeDetectorRef.markForCheck();
@ -397,7 +400,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.dialogRef.close(activity);
} else {
(activity as UpdateOrderDto).id = this.data.activity.id;
(activity as UpdateOrderDto).id = this.data.activity?.id;
await validateObjectForForm({
classDto: UpdateOrderDto,
@ -422,7 +425,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.unsubscribeSubject.complete();
}
private updateSymbol() {
private updateAssetProfile() {
this.isLoading = true;
this.changeDetectorRef.markForCheck();
@ -450,6 +453,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.activityForm.get('dataSource').setValue(dataSource);
}
this.currencyOfAssetProfile = currency;
this.currentMarketPrice = marketPrice;
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>
</mat-form-field>
@if (
currencyOfAssetProfile ===
activityForm.get('currencyOfUnitPrice').value &&
currentMarketPrice &&
(data.activity.type === 'BUY' || data.activity.type === 'SELL') &&
['BUY', 'SELL'].includes(data.activity.type) &&
isToday(activityForm.get('date')?.value)
) {
<button

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

Loading…
Cancel
Save