Browse Source

Merge pull request #20 from dandevaud/main

Latest Upstream changes
pull/5027/head
dandevaud 2 years ago
committed by GitHub
parent
commit
2f00981077
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 56
      CHANGELOG.md
  2. 6
      apps/api/src/app/admin/admin.controller.ts
  3. 40
      apps/api/src/app/admin/admin.service.ts
  4. 17
      apps/api/src/app/app.module.ts
  5. 3
      apps/api/src/app/exchange-rate/exchange-rate.controller.ts
  6. 232
      apps/api/src/app/frontend.middleware.ts
  7. 50
      apps/api/src/app/info/info.service.ts
  8. 12
      apps/api/src/app/logo/logo.service.ts
  9. 3
      apps/api/src/app/symbol/symbol.controller.ts
  10. 14
      apps/api/src/app/user/user.service.ts
  11. 153
      apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
  12. 30
      apps/api/src/assets/sitemap.xml
  13. 3
      apps/api/src/main.ts
  14. 128
      apps/api/src/middlewares/html-template.middleware.ts
  15. 5
      apps/api/src/services/configuration/configuration.service.ts
  16. 30
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  17. 32
      apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
  18. 32
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  19. 7
      apps/api/src/services/data-provider/manual/manual.service.ts
  20. 18
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  21. 122
      apps/client/src/app/app-routing.module.ts
  22. 39
      apps/client/src/app/app.component.html
  23. 27
      apps/client/src/app/app.component.ts
  24. 2
      apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts
  25. 5
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  26. 2
      apps/client/src/app/components/admin-overview/admin-overview.html
  27. 4
      apps/client/src/app/components/admin-settings/admin-settings.component.html
  28. 56
      apps/client/src/app/components/header/header.component.html
  29. 11
      apps/client/src/app/components/header/header.component.ts
  30. 2
      apps/client/src/app/components/home-market/home-market.html
  31. 4
      apps/client/src/app/components/home-summary/home-summary.component.ts
  32. 2
      apps/client/src/app/components/home-summary/home-summary.html
  33. 2
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts
  34. 2
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html
  35. 21
      apps/client/src/app/core/auth.guard.ts
  36. 2
      apps/client/src/app/core/http-response.interceptor.ts
  37. 39
      apps/client/src/app/pages/about/about-page-routing.module.ts
  38. 53
      apps/client/src/app/pages/about/about-page.component.ts
  39. 2
      apps/client/src/app/pages/about/changelog/changelog-page.html
  40. 2
      apps/client/src/app/pages/about/license/license-page.html
  41. 20
      apps/client/src/app/pages/about/oss-friends/oss-friends-page-routing.module.ts
  42. 23
      apps/client/src/app/pages/about/oss-friends/oss-friends-page.component.ts
  43. 42
      apps/client/src/app/pages/about/oss-friends/oss-friends-page.html
  44. 19
      apps/client/src/app/pages/about/oss-friends/oss-friends-page.module.ts
  45. 9
      apps/client/src/app/pages/about/oss-friends/oss-friends-page.scss
  46. 2
      apps/client/src/app/pages/about/overview/about-overview-page.component.ts
  47. 8
      apps/client/src/app/pages/about/overview/about-overview-page.html
  48. 2
      apps/client/src/app/pages/about/privacy-policy/privacy-policy-page.html
  49. 2
      apps/client/src/app/pages/accounts/accounts-page.html
  50. 5
      apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts
  51. 4
      apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.html
  52. 5
      apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.component.ts
  53. 4
      apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.html
  54. 4
      apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts
  55. 2
      apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.html
  56. 4
      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
  57. 6
      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
  58. 5
      apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.component.ts
  59. 8
      apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.html
  60. 3
      apps/client/src/app/pages/blog/2022/11/black-friday-2022/black-friday-2022-page.component.ts
  61. 7
      apps/client/src/app/pages/blog/2022/11/black-friday-2022/black-friday-2022-page.html
  62. 5
      apps/client/src/app/pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.component.ts
  63. 8
      apps/client/src/app/pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.html
  64. 5
      apps/client/src/app/pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.component.ts
  65. 16
      apps/client/src/app/pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.html
  66. 4
      apps/client/src/app/pages/blog/2023/07/exploring-the-path-to-fire/exploring-the-path-to-fire-page.component.ts
  67. 8
      apps/client/src/app/pages/blog/2023/07/exploring-the-path-to-fire/exploring-the-path-to-fire-page.html
  68. 8
      apps/client/src/app/pages/blog/blog-page.html
  69. 4
      apps/client/src/app/pages/faq/faq-page.component.ts
  70. 34
      apps/client/src/app/pages/faq/faq-page.html
  71. 1
      apps/client/src/app/pages/features/features-page.component.ts
  72. 17
      apps/client/src/app/pages/features/features-page.html
  73. 2
      apps/client/src/app/pages/landing/landing-page.component.ts
  74. 98
      apps/client/src/app/pages/landing/landing-page.html
  75. 4
      apps/client/src/app/pages/open/open-page.html
  76. 2
      apps/client/src/app/pages/portfolio/activities/activities-page.html
  77. 10
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  78. 40
      apps/client/src/app/pages/portfolio/allocations/allocations-page.html
  79. 2
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  80. 2
      apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
  81. 4
      apps/client/src/app/pages/portfolio/fire/fire-page.html
  82. 6
      apps/client/src/app/pages/portfolio/holdings/holdings-page.component.ts
  83. 2
      apps/client/src/app/pages/portfolio/holdings/holdings-page.html
  84. 2
      apps/client/src/app/pages/pricing/pricing-page.component.ts
  85. 10
      apps/client/src/app/pages/pricing/pricing-page.html
  86. 4
      apps/client/src/app/pages/public/public-page.html
  87. 2
      apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page-routing.module.ts
  88. 2
      apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.component.ts
  89. 14
      apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html
  90. 13
      apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html
  91. 7
      apps/client/src/app/pages/resources/personal-finance-tools/products/altoo-page.component.ts
  92. 7
      apps/client/src/app/pages/resources/personal-finance-tools/products/copilot-money-page.component.ts
  93. 7
      apps/client/src/app/pages/resources/personal-finance-tools/products/delta-page.component.ts
  94. 7
      apps/client/src/app/pages/resources/personal-finance-tools/products/divvydiary-page.component.ts
  95. 7
      apps/client/src/app/pages/resources/personal-finance-tools/products/exirio-page.component.ts
  96. 7
      apps/client/src/app/pages/resources/personal-finance-tools/products/folishare-page.component.ts
  97. 7
      apps/client/src/app/pages/resources/personal-finance-tools/products/getquin-page.component.ts
  98. 7
      apps/client/src/app/pages/resources/personal-finance-tools/products/gospatz-page.component.ts
  99. 7
      apps/client/src/app/pages/resources/personal-finance-tools/products/justetf-page.component.ts
  100. 7
      apps/client/src/app/pages/resources/personal-finance-tools/products/kubera-page.component.ts

56
CHANGELOG.md

@ -5,6 +5,48 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Refreshed the cryptocurrencies list
- Improved the _OSS Friends_ page
## 1.302.0 - 2023-08-20
### Changed
- Improved the language localization for German (`de`)
- Upgraded `angular` from version `16.1.8` to `16.2.1`
- Upgraded `Nx` from version `16.6.0` to `16.7.2`
## 1.301.1 - 2023-08-19
### Added
- Added the data export feature to the user account page
- Added a currencies preset to the historical market data table of the admin control panel
- Added the _OSS Friends_ page
### Changed
- Improved the localized meta data in `html` files
### Fixed
- Fixed the rows with cash positions in the holdings table
- Fixed an issue with the date parsing in the historical market data editor of the admin control panel
## 1.300.0 - 2023-08-11
### Added
- Added more durations in the coupon system
### Changed
- Migrated the remaining requests from `bent` to `got`
## 1.299.1 - 2023-08-10 ## 1.299.1 - 2023-08-10
### Changed ### Changed
@ -601,7 +643,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Changed the slide toggles to checkboxes on the account page - Changed the slide toggles to checkboxes on the user account page
- Changed the slide toggles to checkboxes in the admin control panel - Changed the slide toggles to checkboxes in the admin control panel
- Increased the density of the theme - Increased the density of the theme
- Migrated the style of various components to `@angular/material` `15` (mdc) - Migrated the style of various components to `@angular/material` `15` (mdc)
@ -1163,7 +1205,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the language selector on the account page - Improved the language selector on the user account page
- Improved the wording in the _X-ray_ section (net worth instead of investment) - Improved the wording in the _X-ray_ section (net worth instead of investment)
- Extended the asset profile details dialog in the admin control panel - Extended the asset profile details dialog in the admin control panel
- Updated the browserslist database - Updated the browserslist database
@ -1581,7 +1623,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added a language selector to the account page - Added a language selector to the user account page
- Added support for translated labels in the value component - Added support for translated labels in the value component
### Changed ### Changed
@ -1910,7 +1952,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added the user id to the account page - Added the user id to the user account page
- Added a new view with jobs of the queue to the admin control panel - Added a new view with jobs of the queue to the admin control panel
### Changed ### Changed
@ -3565,7 +3607,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Respected the cash balance on the analysis page - Respected the cash balance on the analysis page
- Improved the settings selectors on the account page - Improved the settings selectors on the user account page
- Harmonized the slogan to "Open Source Wealth Management Software" - Harmonized the slogan to "Open Source Wealth Management Software"
### Fixed ### Fixed
@ -4031,7 +4073,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added a gradient to the line charts - Added a gradient to the line charts
- Added a selector to set the base currency on the account page - Added a selector to set the base currency on the user account page
## 0.81.0 - 06.04.2021 ## 0.81.0 - 06.04.2021
@ -4345,7 +4387,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Added the membership status to the account page - Added the membership status to the user account page
### Fixed ### Fixed

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

@ -38,7 +38,7 @@ import {
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { DataSource, MarketData, Prisma, SymbolProfile } from '@prisma/client'; import { DataSource, MarketData, Prisma, SymbolProfile } from '@prisma/client';
import { isDate } from 'date-fns'; import { isDate, parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AdminService } from './admin.service'; import { AdminService } from './admin.service';
@ -233,7 +233,7 @@ export class AdminController {
); );
} }
const date = new Date(dateString); const date = parseISO(dateString);
if (!isDate(date)) { if (!isDate(date)) {
throw new HttpException( throw new HttpException(
@ -333,7 +333,7 @@ export class AdminController {
); );
} }
const date = new Date(dateString); const date = parseISO(dateString);
return this.marketDataService.updateMarketData({ return this.marketDataService.updateMarketData({
data: { marketPrice: data.marketPrice, state: 'CLOSE' }, data: { marketPrice: data.marketPrice, state: 'CLOSE' },

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

@ -6,14 +6,12 @@ import { MarketDataService } from '@ghostfolio/api/services/market-data/market-d
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
DEFAULT_PAGE_SIZE,
PROPERTY_CURRENCIES
} from '@ghostfolio/common/config';
import { import {
AdminData, AdminData,
AdminMarketData, AdminMarketData,
AdminMarketDataDetails, AdminMarketDataDetails,
AdminMarketDataItem,
Filter, Filter,
UniqueAsset UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -121,7 +119,9 @@ export class AdminService {
[{ symbol: 'asc' }]; [{ symbol: 'asc' }];
const where: Prisma.SymbolProfileWhereInput = {}; const where: Prisma.SymbolProfileWhereInput = {};
if ( if (presetId === 'CURRENCIES') {
return this.getMarketDataForCurrencies();
} else if (
presetId === 'ETF_WITHOUT_COUNTRIES' || presetId === 'ETF_WITHOUT_COUNTRIES' ||
presetId === 'ETF_WITHOUT_SECTORS' presetId === 'ETF_WITHOUT_SECTORS'
) { ) {
@ -313,6 +313,36 @@ export class AdminService {
return response; return response;
} }
private async getMarketDataForCurrencies(): Promise<AdminMarketData> {
const marketDataItems = await this.prismaService.marketData.groupBy({
_count: true,
by: ['dataSource', 'symbol']
});
const marketData: AdminMarketDataItem[] = this.exchangeRateDataService
.getCurrencyPairs()
.map(({ dataSource, symbol }) => {
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
return {
dataSource,
marketDataItemCount,
symbol,
assetClass: 'CASH',
countriesCount: 0,
sectorsCount: 0
};
});
return { marketData, count: marketData.length };
}
private async getUsersWithAnalytics(): Promise<AdminData['users']> { private async getUsersWithAnalytics(): Promise<AdminData['users']> {
let orderBy: any = { let orderBy: any = {
createdAt: 'desc' createdAt: 'desc'

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

@ -12,7 +12,7 @@ import {
SUPPORTED_LANGUAGE_CODES SUPPORTED_LANGUAGE_CODES
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { BullModule } from '@nestjs/bull'; import { BullModule } from '@nestjs/bull';
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleModule } from '@nestjs/schedule';
import { ServeStaticModule } from '@nestjs/serve-static'; import { ServeStaticModule } from '@nestjs/serve-static';
@ -28,7 +28,6 @@ import { BenchmarkModule } from './benchmark/benchmark.module';
import { CacheModule } from './cache/cache.module'; import { CacheModule } from './cache/cache.module';
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module'; import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
import { ExportModule } from './export/export.module'; import { ExportModule } from './export/export.module';
import { FrontendMiddleware } from './frontend.middleware';
import { HealthModule } from './health/health.module'; import { HealthModule } from './health/health.module';
import { ImportModule } from './import/import.module'; import { ImportModule } from './import/import.module';
import { InfoModule } from './info/info.module'; import { InfoModule } from './info/info.module';
@ -75,12 +74,6 @@ import { UserModule } from './user/user.module';
PrismaModule, PrismaModule,
RedisCacheModule, RedisCacheModule,
ScheduleModule.forRoot(), ScheduleModule.forRoot(),
...SUPPORTED_LANGUAGE_CODES.map((languageCode) => {
return ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'client', languageCode),
serveRoot: `/${languageCode}`
});
}),
ServeStaticModule.forRoot({ ServeStaticModule.forRoot({
exclude: ['/api*', '/sitemap.xml'], exclude: ['/api*', '/sitemap.xml'],
rootPath: join(__dirname, '..', 'client'), rootPath: join(__dirname, '..', 'client'),
@ -114,10 +107,4 @@ import { UserModule } from './user/user.module';
controllers: [AppController], controllers: [AppController],
providers: [CronService] providers: [CronService]
}) })
export class AppModule { export class AppModule {}
configure(consumer: MiddlewareConsumer) {
consumer
.apply(FrontendMiddleware)
.forRoutes({ path: '*', method: RequestMethod.ALL });
}
}

3
apps/api/src/app/exchange-rate/exchange-rate.controller.ts

@ -10,6 +10,7 @@ import { AuthGuard } from '@nestjs/passport';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { ExchangeRateService } from './exchange-rate.service'; import { ExchangeRateService } from './exchange-rate.service';
import { parseISO } from 'date-fns';
@Controller('exchange-rate') @Controller('exchange-rate')
export class ExchangeRateController { export class ExchangeRateController {
@ -23,7 +24,7 @@ export class ExchangeRateController {
@Param('dateString') dateString: string, @Param('dateString') dateString: string,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> { ): Promise<IDataProviderHistoricalResponse> {
const date = new Date(dateString); const date = parseISO(dateString);
const exchangeRate = await this.exchangeRateService.getExchangeRate({ const exchangeRate = await this.exchangeRateService.getExchangeRate({
date, date,

232
apps/api/src/app/frontend.middleware.ts

@ -1,232 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import { environment } from '@ghostfolio/api/environments/environment';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { DATE_FORMAT, interpolate } from '@ghostfolio/common/helper';
import { Injectable, NestMiddleware } from '@nestjs/common';
import { format } from 'date-fns';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class FrontendMiddleware implements NestMiddleware {
public indexHtmlDe = '';
public indexHtmlEn = '';
public indexHtmlEs = '';
public indexHtmlFr = '';
public indexHtmlIt = '';
public indexHtmlNl = '';
public indexHtmlPt = '';
private static readonly DEFAULT_DESCRIPTION =
'Ghostfolio is a personal finance dashboard to keep track of your assets like stocks, ETFs or cryptocurrencies across multiple platforms.';
public constructor(
private readonly configurationService: ConfigurationService
) {
try {
this.indexHtmlDe = fs.readFileSync(
this.getPathOfIndexHtmlFile('de'),
'utf8'
);
this.indexHtmlEn = fs.readFileSync(
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
'utf8'
);
this.indexHtmlEs = fs.readFileSync(
this.getPathOfIndexHtmlFile('es'),
'utf8'
);
this.indexHtmlFr = fs.readFileSync(
this.getPathOfIndexHtmlFile('fr'),
'utf8'
);
this.indexHtmlIt = fs.readFileSync(
this.getPathOfIndexHtmlFile('it'),
'utf8'
);
this.indexHtmlNl = fs.readFileSync(
this.getPathOfIndexHtmlFile('nl'),
'utf8'
);
this.indexHtmlPt = fs.readFileSync(
this.getPathOfIndexHtmlFile('pt'),
'utf8'
);
} catch {}
}
public use(request: Request, response: Response, next: NextFunction) {
const currentDate = format(new Date(), DATE_FORMAT);
let featureGraphicPath = 'assets/cover.png';
let title = 'Ghostfolio – Open Source Wealth Management Software';
if (request.path.startsWith('/en/blog/2022/08/500-stars-on-github')) {
featureGraphicPath = 'assets/images/blog/500-stars-on-github.jpg';
title = `500 Stars - ${title}`;
} else if (request.path.startsWith('/en/blog/2022/10/hacktoberfest-2022')) {
featureGraphicPath = 'assets/images/blog/hacktoberfest-2022.png';
title = `Hacktoberfest 2022 - ${title}`;
} else if (request.path.startsWith('/en/blog/2022/11/black-friday-2022')) {
featureGraphicPath = 'assets/images/blog/black-friday-2022.jpg';
title = `Black Friday 2022 - ${title}`;
} else if (
request.path.startsWith(
'/en/blog/2022/12/the-importance-of-tracking-your-personal-finances'
)
) {
featureGraphicPath = 'assets/images/blog/20221226.jpg';
title = `The importance of tracking your personal finances - ${title}`;
} else if (
request.path.startsWith(
'/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt'
)
) {
featureGraphicPath = 'assets/images/blog/ghostfolio-x-sackgeld.png';
title = `Ghostfolio auf Sackgeld.com vorgestellt - ${title}`;
} else if (
request.path.startsWith('/en/blog/2023/02/ghostfolio-meets-umbrel')
) {
featureGraphicPath = 'assets/images/blog/ghostfolio-x-umbrel.png';
title = `Ghostfolio meets Umbrel - ${title}`;
} else if (
request.path.startsWith(
'/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github'
)
) {
featureGraphicPath = 'assets/images/blog/1000-stars-on-github.jpg';
title = `Ghostfolio reaches 1’000 Stars on GitHub - ${title}`;
} else if (
request.path.startsWith(
'/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio'
)
) {
featureGraphicPath = 'assets/images/blog/20230520.jpg';
title = `Unlock your Financial Potential with Ghostfolio - ${title}`;
} else if (
request.path.startsWith('/en/blog/2023/07/exploring-the-path-to-fire')
) {
featureGraphicPath = 'assets/images/blog/20230701.jpg';
title = `Exploring the Path to FIRE - ${title}`;
}
if (
request.path.startsWith('/api/') ||
this.isFileRequest(request.url) ||
!environment.production
) {
// Skip
next();
} else if (request.path === '/de' || request.path.startsWith('/de/')) {
response.send(
interpolate(this.indexHtmlDe, {
currentDate,
featureGraphicPath,
title,
description:
'Mit dem Finanz-Dashboard Ghostfolio können Sie Ihr Vermögen in Form von Aktien, ETFs oder Kryptowährungen verteilt über mehrere Finanzinstitute überwachen.',
languageCode: 'de',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/es' || request.path.startsWith('/es/')) {
response.send(
interpolate(this.indexHtmlEs, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio es un dashboard de finanzas personales para hacer un seguimiento de tus activos como acciones, ETFs o criptodivisas a través de múltiples plataformas.',
languageCode: 'es',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/fr' || request.path.startsWith('/fr/')) {
response.send(
interpolate(this.indexHtmlFr, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio est un dashboard de finances personnelles qui permet de suivre vos actifs comme les actions, les ETF ou les crypto-monnaies sur plusieurs plateformes.',
languageCode: 'fr',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/it' || request.path.startsWith('/it/')) {
response.send(
interpolate(this.indexHtmlIt, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio è un dashboard di finanza personale per tenere traccia delle vostre attività come azioni, ETF o criptovalute su più piattaforme.',
languageCode: 'it',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/nl' || request.path.startsWith('/nl/')) {
response.send(
interpolate(this.indexHtmlNl, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio is een persoonlijk financieel dashboard om uw activa zoals aandelen, ETF’s of cryptocurrencies over meerdere platforms bij te houden.',
languageCode: 'nl',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (request.path === '/pt' || request.path.startsWith('/pt/')) {
response.send(
interpolate(this.indexHtmlPt, {
currentDate,
featureGraphicPath,
title,
description:
'Ghostfolio é um dashboard de finanças pessoais para acompanhar os seus activos como acções, ETFs ou criptomoedas em múltiplas plataformas.',
languageCode: 'pt',
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else {
response.send(
interpolate(this.indexHtmlEn, {
currentDate,
featureGraphicPath,
title,
description: FrontendMiddleware.DEFAULT_DESCRIPTION,
languageCode: DEFAULT_LANGUAGE_CODE,
path: request.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
}
}
private getPathOfIndexHtmlFile(aLocale: string) {
return path.join(__dirname, '..', 'client', aLocale, 'index.html');
}
private isFileRequest(filename: string) {
if (filename === '/assets/LICENSE') {
return true;
} else if (
filename.includes('auth/ey') ||
filename.includes(
'personal-finance-tools/open-source-alternative-to-markets.sh'
)
) {
return false;
}
return filename.split('.').pop() !== filename;
}
}

50
apps/api/src/app/info/info.service.ts

@ -30,9 +30,9 @@ import { permissions } from '@ghostfolio/common/permissions';
import { SubscriptionOffer } from '@ghostfolio/common/types'; import { SubscriptionOffer } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import * as bent from 'bent';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
import { format, subDays } from 'date-fns'; import { format, subDays } from 'date-fns';
import got from 'got';
@Injectable() @Injectable()
export class InfoService { export class InfoService {
@ -172,17 +172,13 @@ export class InfoService {
private async countDockerHubPulls(): Promise<number> { private async countDockerHubPulls(): Promise<number> {
try { try {
const get = bent( const { pull_count } = await got(
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`, `https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
'GET',
'json',
200,
{ {
'User-Agent': 'request' headers: { 'User-Agent': 'request' }
} }
); ).json<any>();
const { pull_count } = await get();
return pull_count; return pull_count;
} catch (error) { } catch (error) {
Logger.error(error, 'InfoService'); Logger.error(error, 'InfoService');
@ -193,16 +189,9 @@ export class InfoService {
private async countGitHubContributors(): Promise<number> { private async countGitHubContributors(): Promise<number> {
try { try {
const get = bent( const { body } = await got('https://github.com/ghostfolio/ghostfolio');
'https://github.com/ghostfolio/ghostfolio',
'GET',
'string',
200,
{}
);
const html = await get(); const $ = cheerio.load(body);
const $ = cheerio.load(html);
return extractNumberFromString( return extractNumberFromString(
$( $(
@ -218,17 +207,13 @@ export class InfoService {
private async countGitHubStargazers(): Promise<number> { private async countGitHubStargazers(): Promise<number> {
try { try {
const get = bent( const { stargazers_count } = await got(
`https://api.github.com/repos/ghostfolio/ghostfolio`, `https://api.github.com/repos/ghostfolio/ghostfolio`,
'GET',
'json',
200,
{ {
'User-Agent': 'request' headers: { 'User-Agent': 'request' }
} }
); ).json<any>();
const { stargazers_count } = await get();
return stargazers_count; return stargazers_count;
} catch (error) { } catch (error) {
Logger.error(error, 'InfoService'); Logger.error(error, 'InfoService');
@ -346,22 +331,21 @@ export class InfoService {
PROPERTY_BETTER_UPTIME_MONITOR_ID PROPERTY_BETTER_UPTIME_MONITOR_ID
)) as string; )) as string;
const get = bent( const { data } = await got(
`https://betteruptime.com/api/v2/monitors/${monitorId}/sla?from=${format( `https://betteruptime.com/api/v2/monitors/${monitorId}/sla?from=${format(
subDays(new Date(), 90), subDays(new Date(), 90),
DATE_FORMAT DATE_FORMAT
)}&to${format(new Date(), DATE_FORMAT)}`, )}&to${format(new Date(), DATE_FORMAT)}`,
'GET',
'json',
200,
{ {
Authorization: `Bearer ${this.configurationService.get( headers: {
'BETTER_UPTIME_API_KEY' Authorization: `Bearer ${this.configurationService.get(
)}` 'BETTER_UPTIME_API_KEY'
)}`
}
} }
); ).json<any>();
const { data } = await get();
return data.attributes.availability / 100; return data.attributes.availability / 100;
} catch (error) { } catch (error) {
Logger.error(error, 'InfoService'); Logger.error(error, 'InfoService');

12
apps/api/src/app/logo/logo.service.ts

@ -2,7 +2,7 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/sy
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { HttpException, Injectable } from '@nestjs/common'; import { HttpException, Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import * as bent from 'bent'; import got from 'got';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@Injectable() @Injectable()
@ -41,15 +41,11 @@ export class LogoService {
} }
private getBuffer(aUrl: string) { private getBuffer(aUrl: string) {
const get = bent( return got(
`https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`, `https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`,
'GET',
'buffer',
200,
{ {
'User-Agent': 'request' headers: { 'User-Agent': 'request' }
} }
); ).buffer();
return get();
} }
} }

3
apps/api/src/app/symbol/symbol.controller.ts

@ -21,6 +21,7 @@ import { isDate, isEmpty } from 'lodash';
import { LookupItem } from './interfaces/lookup-item.interface'; import { LookupItem } from './interfaces/lookup-item.interface';
import { SymbolItem } from './interfaces/symbol-item.interface'; import { SymbolItem } from './interfaces/symbol-item.interface';
import { SymbolService } from './symbol.service'; import { SymbolService } from './symbol.service';
import { parseISO } from 'date-fns';
@Controller('symbol') @Controller('symbol')
export class SymbolController { export class SymbolController {
@ -93,7 +94,7 @@ export class SymbolController {
@Param('dateString') dateString: string, @Param('dateString') dateString: string,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> { ): Promise<IDataProviderHistoricalResponse> {
const date = new Date(dateString); const date = parseISO(dateString);
if (!isDate(date)) { if (!isDate(date)) {
throw new HttpException( throw new HttpException(

14
apps/api/src/app/user/user.service.ts

@ -4,7 +4,11 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config'; import {
DEFAULT_CURRENCY,
PROPERTY_IS_READ_ONLY_MODE,
locale
} from '@ghostfolio/common/config';
import { User as IUser, UserSettings } from '@ghostfolio/common/interfaces'; import { User as IUser, UserSettings } from '@ghostfolio/common/interfaces';
import { import {
getPermissions, getPermissions,
@ -21,8 +25,6 @@ const crypto = require('crypto');
@Injectable() @Injectable()
export class UserService { export class UserService {
public static DEFAULT_CURRENCY = 'USD';
private baseCurrency: string; private baseCurrency: string;
public constructor( public constructor(
@ -145,8 +147,7 @@ export class UserService {
// Set default value for base currency // Set default value for base currency
if (!(user.Settings.settings as UserSettings)?.baseCurrency) { if (!(user.Settings.settings as UserSettings)?.baseCurrency) {
(user.Settings.settings as UserSettings).baseCurrency = (user.Settings.settings as UserSettings).baseCurrency = DEFAULT_CURRENCY;
UserService.DEFAULT_CURRENCY;
} }
// Set default value for date range // Set default value for date range
@ -186,6 +187,9 @@ export class UserService {
if (Analytics?.activityCount % frequency === 1) { if (Analytics?.activityCount % frequency === 1) {
currentPermissions.push(permissions.enableSubscriptionInterstitial); currentPermissions.push(permissions.enableSubscriptionInterstitial);
} }
// Reset benchmark
user.Settings.settings.benchmark = undefined;
} }
if (user.subscription?.type === 'Premium') { if (user.subscription?.type === 'Premium') {

153
apps/api/src/assets/cryptocurrencies/cryptocurrencies.json

@ -51,7 +51,9 @@
"3FT": "ThreeFold Token", "3FT": "ThreeFold Token",
"3ULL": "3ULL Coin", "3ULL": "3ULL Coin",
"3XD": "3DChain", "3XD": "3DChain",
"420CHAN": "420chan",
"4ART": "4ART Coin", "4ART": "4ART Coin",
"4CHAN": "4Chan",
"4JNET": "4JNET", "4JNET": "4JNET",
"77G": "GraphenTech", "77G": "GraphenTech",
"7E": "7ELEVEN", "7E": "7ELEVEN",
@ -60,6 +62,7 @@
"8BT": "8 Circuit Studios", "8BT": "8 Circuit Studios",
"8PAY": "8Pay", "8PAY": "8Pay",
"8X8": "8X8 Protocol", "8X8": "8X8 Protocol",
"9GAG": "9GAG",
"A5T": "Alpha5", "A5T": "Alpha5",
"AAA": "Moon Rabbit", "AAA": "Moon Rabbit",
"AAB": "AAX Token", "AAB": "AAX Token",
@ -101,6 +104,7 @@
"ACN": "AvonCoin", "ACN": "AvonCoin",
"ACOIN": "ACoin", "ACOIN": "ACoin",
"ACP": "Anarchists Prime", "ACP": "Anarchists Prime",
"ACQ": "Acquire.Fi",
"ACS": "Access Protocol", "ACS": "Access Protocol",
"ACT": "Achain", "ACT": "Achain",
"ACTIN": "Actinium", "ACTIN": "Actinium",
@ -180,7 +184,7 @@
"AGX": "Agricoin", "AGX": "Agricoin",
"AHOO": "Ahoolee", "AHOO": "Ahoolee",
"AHT": "AhaToken", "AHT": "AhaToken",
"AI": "Multiverse", "AI": "AiDoge",
"AIB": "AdvancedInternetBlock", "AIB": "AdvancedInternetBlock",
"AIBB": "AiBB", "AIBB": "AiBB",
"AIBK": "AIB Utility Token", "AIBK": "AIB Utility Token",
@ -213,6 +217,7 @@
"AKA": "Akroma", "AKA": "Akroma",
"AKITA": "Akita Inu", "AKITA": "Akita Inu",
"AKN": "Akoin", "AKN": "Akoin",
"AKNC": "Aave KNC v1",
"AKRO": "Akropolis", "AKRO": "Akropolis",
"AKT": "Akash Network", "AKT": "Akash Network",
"AKTIO": "AKTIO Coin", "AKTIO": "AKTIO Coin",
@ -237,12 +242,14 @@
"ALIC": "AliCoin", "ALIC": "AliCoin",
"ALICE": "My Neighbor Alice", "ALICE": "My Neighbor Alice",
"ALIEN": "AlienCoin", "ALIEN": "AlienCoin",
"ALINK": "Aave LINK v1",
"ALIS": "ALISmedia", "ALIS": "ALISmedia",
"ALITA": "Alita Network", "ALITA": "Alita Network",
"ALIX": "AlinX", "ALIX": "AlinX",
"ALKI": "Alkimi", "ALKI": "Alkimi",
"ALLBI": "ALL BEST ICO", "ALLBI": "ALL BEST ICO",
"ALLEY": "NFT Alley", "ALLEY": "NFT Alley",
"ALLIN": "All in",
"ALN": "Aluna", "ALN": "Aluna",
"ALOHA": "Aloha", "ALOHA": "Aloha",
"ALP": "Alphacon", "ALP": "Alphacon",
@ -410,12 +417,14 @@
"ARIX": "Arix", "ARIX": "Arix",
"ARK": "ARK", "ARK": "ARK",
"ARKER": "Arker", "ARKER": "Arker",
"ARKM": "Arkham",
"ARKN": "Ark Rivals", "ARKN": "Ark Rivals",
"ARM": "Armory Coin", "ARM": "Armory Coin",
"ARMOR": "ARMOR", "ARMOR": "ARMOR",
"ARMR": "ARMR", "ARMR": "ARMR",
"ARMS": "2Acoin", "ARMS": "2Acoin",
"ARNA": "ARNA Panacea", "ARNA": "ARNA Panacea",
"ARNM": "Arenum",
"ARNO": "ARNO", "ARNO": "ARNO",
"ARNX": "Aeron", "ARNX": "Aeron",
"ARNXM": "Armor NXM", "ARNXM": "Armor NXM",
@ -472,6 +481,7 @@
"ASTO": "Altered State Token", "ASTO": "Altered State Token",
"ASTON": "Aston", "ASTON": "Aston",
"ASTR": "Astar", "ASTR": "Astar",
"ASTRAFER": "Astrafer",
"ASTRAL": "Astral", "ASTRAL": "Astral",
"ASTRO": "AstroSwap", "ASTRO": "AstroSwap",
"ASTROC": "Astroport Classic", "ASTROC": "Astroport Classic",
@ -531,6 +541,7 @@
"AURY": "Aurory", "AURY": "Aurory",
"AUSCM": "Auric Network", "AUSCM": "Auric Network",
"AUSD": "Appeal dollar", "AUSD": "Appeal dollar",
"AUSDC": "Aave USDC v1",
"AUT": "Autoria", "AUT": "Autoria",
"AUTHORSHIP": "Authorship", "AUTHORSHIP": "Authorship",
"AUTO": "Auto", "AUTO": "Auto",
@ -612,6 +623,7 @@
"BACK": "DollarBack", "BACK": "DollarBack",
"BACOIN": "BACoin", "BACOIN": "BACoin",
"BACON": "BaconDAO (BACON)", "BACON": "BaconDAO (BACON)",
"BAD": "Bad Idea AI",
"BADGER": "Badger DAO", "BADGER": "Badger DAO",
"BAG": "BondAppetit", "BAG": "BondAppetit",
"BAGS": "Basis Gold Share", "BAGS": "Basis Gold Share",
@ -662,6 +674,7 @@
"BBCT": "TraDove B2BCoin", "BBCT": "TraDove B2BCoin",
"BBDT": "BBD Token", "BBDT": "BBD Token",
"BBF": "Bubblefong", "BBF": "Bubblefong",
"BBFT": "Block Busters Tech Token",
"BBG": "BigBang", "BBG": "BigBang",
"BBGC": "BigBang Game", "BBGC": "BigBang Game",
"BBI": "BelugaPay", "BBI": "BelugaPay",
@ -725,6 +738,7 @@
"BDX": "Beldex", "BDX": "Beldex",
"BDY": "Buddy DAO", "BDY": "Buddy DAO",
"BEACH": "BeachCoin", "BEACH": "BeachCoin",
"BEAI": "BeNFT Solutions",
"BEAM": "Beam", "BEAM": "Beam",
"BEAN": "BeanCash", "BEAN": "BeanCash",
"BEAST": "CryptoBeast", "BEAST": "CryptoBeast",
@ -806,6 +820,7 @@
"BIDR": "Binance IDR Stable Coin", "BIDR": "Binance IDR Stable Coin",
"BIFI": "Beefy.Finance", "BIFI": "Beefy.Finance",
"BIFIF": "BiFi", "BIFIF": "BiFi",
"BIG": "Big Eyes",
"BIGHAN": "BighanCoin", "BIGHAN": "BighanCoin",
"BIGSB": "BigShortBets", "BIGSB": "BigShortBets",
"BIGUP": "BigUp", "BIGUP": "BigUp",
@ -1090,6 +1105,7 @@
"BRNK": "Brank", "BRNK": "Brank",
"BRNX": "Bronix", "BRNX": "Bronix",
"BRO": "Bitradio", "BRO": "Bitradio",
"BROCK": "Bitrock",
"BRONZ": "BitBronze", "BRONZ": "BitBronze",
"BRT": "Bikerush", "BRT": "Bikerush",
"BRTR": "Barter", "BRTR": "Barter",
@ -1226,7 +1242,7 @@
"BULL": "Bullieverse", "BULL": "Bullieverse",
"BULLC": "BuySell", "BULLC": "BuySell",
"BULLION": "BullionFX", "BULLION": "BullionFX",
"BULLS": "BullshitCoin", "BULLS": "Bull Coin",
"BULLSH": "Bullshit Inu", "BULLSH": "Bullshit Inu",
"BUMN": "BUMooN", "BUMN": "BUMooN",
"BUMP": "Bumper", "BUMP": "Bumper",
@ -1319,8 +1335,10 @@
"CAP": "BottleCaps", "CAP": "BottleCaps",
"CAPD": "Capdax", "CAPD": "Capdax",
"CAPP": "Cappasity", "CAPP": "Cappasity",
"CAPRICOIN": "CapriCoin",
"CAPS": "Ternoa", "CAPS": "Ternoa",
"CAPT": "Bitcoin Captain", "CAPT": "Bitcoin Captain",
"CAPTAINPLANET": "Captain Planet",
"CAR": "CarBlock", "CAR": "CarBlock",
"CARAT": "Carats Token", "CARAT": "Carats Token",
"CARBON": "Carboncoin", "CARBON": "Carboncoin",
@ -1478,6 +1496,7 @@
"CHECKR": "CheckerChain", "CHECKR": "CheckerChain",
"CHECOIN": "CheCoin", "CHECOIN": "CheCoin",
"CHEDDA": "Chedda", "CHEDDA": "Chedda",
"CHEEL": "Cheelee",
"CHEESE": "CHEESE", "CHEESE": "CHEESE",
"CHEESUS": "Cheesus", "CHEESUS": "Cheesus",
"CHEQ": "CHEQD Network", "CHEQ": "CHEQD Network",
@ -1520,7 +1539,8 @@
"CHX": "Own", "CHX": "Own",
"CHY": "Concern Poverty Chain", "CHY": "Concern Poverty Chain",
"CHZ": "Chiliz", "CHZ": "Chiliz",
"CIC": "CIChain", "CIC": "Crazy Internet Coin",
"CICHAIN": "CIChain",
"CIF": "Crypto Improvement Fund", "CIF": "Crypto Improvement Fund",
"CIM": "COINCOME", "CIM": "COINCOME",
"CIN": "CinderCoin", "CIN": "CinderCoin",
@ -1630,7 +1650,6 @@
"COB": "Cobinhood", "COB": "Cobinhood",
"COC": "Coin of the champions", "COC": "Coin of the champions",
"COCK": "Shibacock", "COCK": "Shibacock",
"COCOS": "COCOS BCX",
"CODEO": "Codeo Token", "CODEO": "Codeo Token",
"CODEX": "CODEX Finance", "CODEX": "CODEX Finance",
"CODI": "Codi Finance", "CODI": "Codi Finance",
@ -1659,7 +1678,7 @@
"COLX": "ColossusCoinXT", "COLX": "ColossusCoinXT",
"COM": "Coliseum", "COM": "Coliseum",
"COMB": "Combo", "COMB": "Combo",
"COMBO": "Furucombo", "COMBO": "COMBO",
"COMFI": "CompliFi", "COMFI": "CompliFi",
"COMM": "Community Coin", "COMM": "Community Coin",
"COMMUNITYCOIN": "Community Coin", "COMMUNITYCOIN": "Community Coin",
@ -1672,7 +1691,6 @@
"CONI": "CoinBene", "CONI": "CoinBene",
"CONS": "ConSpiracy Coin", "CONS": "ConSpiracy Coin",
"CONSENTIUM": "Consentium", "CONSENTIUM": "Consentium",
"CONT": "Contentos",
"CONUN": "CONUN", "CONUN": "CONUN",
"CONV": "Convergence", "CONV": "Convergence",
"COOK": "Cook", "COOK": "Cook",
@ -1683,17 +1701,19 @@
"COPS": "Cops Finance", "COPS": "Cops Finance",
"COR": "Corion", "COR": "Corion",
"CORAL": "CoralPay", "CORAL": "CoralPay",
"CORE": "Coreum", "CORE": "Core",
"COREDAO": "coreDAO", "COREDAO": "coreDAO",
"COREG": "Core Group Asset", "COREG": "Core Group Asset",
"COREUM": "Coreum",
"CORGI": "Corgi Inu", "CORGI": "Corgi Inu",
"CORN": "CORN", "CORN": "CORN",
"CORX": "CorionX", "CORX": "CorionX",
"COS": "COS", "COS": "Contentos",
"COSHI": "CoShi Inu", "COSHI": "CoShi Inu",
"COSM": "CosmoChain", "COSM": "CosmoChain",
"COSMIC": "CosmicSwap", "COSMIC": "CosmicSwap",
"COSP": "Cosplay Token", "COSP": "Cosplay Token",
"COSS": "COS",
"COSX": "Cosmecoin", "COSX": "Cosmecoin",
"COT": "CoTrader", "COT": "CoTrader",
"COTI": "COTI", "COTI": "COTI",
@ -1729,7 +1749,7 @@
"CPOOL": "Clearpool", "CPOOL": "Clearpool",
"CPROP": "CPROP", "CPROP": "CPROP",
"CPRX": "Crypto Perx", "CPRX": "Crypto Perx",
"CPS": "CapriCoin", "CPS": "Cryptostone",
"CPT": "Cryptaur", "CPT": "Cryptaur",
"CPU": "CPUcoin", "CPU": "CPUcoin",
"CPX": "Apex Token", "CPX": "Apex Token",
@ -1796,6 +1816,7 @@
"CRTS": "Cratos", "CRTS": "Cratos",
"CRU": "Crust Network", "CRU": "Crust Network",
"CRV": "Curve DAO Token", "CRV": "Curve DAO Token",
"CRVUSD": "crvUSD",
"CRW": "Crown Coin", "CRW": "Crown Coin",
"CRWD": "CRWD Network", "CRWD": "CRWD Network",
"CRWNY": "Crowny Token", "CRWNY": "Crowny Token",
@ -1843,7 +1864,7 @@
"CTLX": "Cash Telex", "CTLX": "Cash Telex",
"CTN": "Continuum Finance", "CTN": "Continuum Finance",
"CTO": "Crypto", "CTO": "Crypto",
"CTP": "Captain Planet", "CTP": "Ctomorrow Platform",
"CTPL": "Cultiplan", "CTPL": "Cultiplan",
"CTPT": "Contents Protocol", "CTPT": "Contents Protocol",
"CTR": "Creator Platform", "CTR": "Creator Platform",
@ -2007,6 +2028,7 @@
"DBC": "DeepBrain Chain", "DBC": "DeepBrain Chain",
"DBCCOIN": "Datablockchain", "DBCCOIN": "Datablockchain",
"DBD": "Day By Day", "DBD": "Day By Day",
"DBEAR": "DBear Coin",
"DBET": "Decent.bet", "DBET": "Decent.bet",
"DBIC": "DubaiCoin", "DBIC": "DubaiCoin",
"DBIX": "DubaiCoin", "DBIX": "DubaiCoin",
@ -2058,6 +2080,7 @@
"DEEP": "DeepCloud AI", "DEEP": "DeepCloud AI",
"DEEPG": "Deep Gold", "DEEPG": "Deep Gold",
"DEEX": "DEEX", "DEEX": "DEEX",
"DEEZ": "DEEZ NUTS",
"DEFI": "Defi", "DEFI": "Defi",
"DEFI5": "DEFI Top 5 Tokens Index", "DEFI5": "DEFI Top 5 Tokens Index",
"DEFIL": "DeFIL", "DEFIL": "DeFIL",
@ -2162,11 +2185,12 @@
"DIEM": "Facebook Diem", "DIEM": "Facebook Diem",
"DIESEL": "Diesel", "DIESEL": "Diesel",
"DIFX": "Digital Financial Exchange", "DIFX": "Digital Financial Exchange",
"DIG": "Dignity", "DIG": "DIEGO",
"DIGG": "DIGG", "DIGG": "DIGG",
"DIGIC": "DigiCube", "DIGIC": "DigiCube",
"DIGIF": "DigiFel", "DIGIF": "DigiFel",
"DIGITAL": "Digital Reserve Currency", "DIGITAL": "Digital Reserve Currency",
"DIGNITY": "Dignity",
"DIGS": "Diggits", "DIGS": "Diggits",
"DIKO": "Arkadiko", "DIKO": "Arkadiko",
"DILI": "D Community", "DILI": "D Community",
@ -2246,6 +2270,7 @@
"DOGBOSS": "Dog Boss", "DOGBOSS": "Dog Boss",
"DOGDEFI": "DogDeFiCoin", "DOGDEFI": "DogDeFiCoin",
"DOGE": "Dogecoin", "DOGE": "Dogecoin",
"DOGE20": "Doge 2.0",
"DOGEBNB": "DogeBNB", "DOGEBNB": "DogeBNB",
"DOGEC": "DogeCash", "DOGEC": "DogeCash",
"DOGECEO": "Doge CEO", "DOGECEO": "Doge CEO",
@ -2559,6 +2584,7 @@
"EMC2": "Einsteinium", "EMC2": "Einsteinium",
"EMD": "Emerald", "EMD": "Emerald",
"EMIGR": "EmiratesGoldCoin", "EMIGR": "EmiratesGoldCoin",
"EML": "EML Protocol",
"EMN.CUR": "Eastman Chemical", "EMN.CUR": "Eastman Chemical",
"EMON": "Ethermon", "EMON": "Ethermon",
"EMOT": "Sentigraph.io", "EMOT": "Sentigraph.io",
@ -2692,6 +2718,7 @@
"ETHD": "Ethereum Dark", "ETHD": "Ethereum Dark",
"ETHER": "Etherparty", "ETHER": "Etherparty",
"ETHERDELTA": "EtherDelta", "ETHERDELTA": "EtherDelta",
"ETHERKING": "Ether Kingdoms Token",
"ETHERNITY": "Ethernity Chain", "ETHERNITY": "Ethernity Chain",
"ETHF": "EthereumFair", "ETHF": "EthereumFair",
"ETHIX": "EthicHub", "ETHIX": "EthicHub",
@ -2709,6 +2736,7 @@
"ETHSHIB": "Eth Shiba", "ETHSHIB": "Eth Shiba",
"ETHV": "Ethverse", "ETHV": "Ethverse",
"ETHW": "Ethereum PoW", "ETHW": "Ethereum PoW",
"ETHX": "Stader ETHx",
"ETHY": "Ethereum Yield", "ETHY": "Ethereum Yield",
"ETI": "EtherInc", "ETI": "EtherInc",
"ETK": "Energi Token", "ETK": "Energi Token",
@ -2722,7 +2750,7 @@
"ETR": "Electric Token", "ETR": "Electric Token",
"ETRNT": "Eternal Trusts", "ETRNT": "Eternal Trusts",
"ETS": "ETH Share", "ETS": "ETH Share",
"ETSC": "Ether star blockchain", "ETSC": "Ether star blockchain",
"ETT": "EncryptoTel", "ETT": "EncryptoTel",
"ETY": "Ethereum Cloud", "ETY": "Ethereum Cloud",
"ETZ": "EtherZero", "ETZ": "EtherZero",
@ -2773,6 +2801,7 @@
"EXB": "ExaByte (EXB)", "EXB": "ExaByte (EXB)",
"EXC": "Eximchain", "EXC": "Eximchain",
"EXCC": "ExchangeCoin", "EXCC": "ExchangeCoin",
"EXCHANGEN": "ExchangeN",
"EXCL": "Exclusive Coin", "EXCL": "Exclusive Coin",
"EXE": "ExeCoin", "EXE": "ExeCoin",
"EXFI": "Flare Finance", "EXFI": "Flare Finance",
@ -2781,7 +2810,7 @@
"EXLT": "ExtraLovers", "EXLT": "ExtraLovers",
"EXM": "EXMO Coin", "EXM": "EXMO Coin",
"EXMR": "EXMR FDN", "EXMR": "EXMR FDN",
"EXN": "ExchangeN", "EXN": "Exeno",
"EXO": "Exosis", "EXO": "Exosis",
"EXP": "Expanse", "EXP": "Expanse",
"EXRD": "Radix", "EXRD": "Radix",
@ -2814,6 +2843,7 @@
"FAIR": "FairCoin", "FAIR": "FairCoin",
"FAIRC": "Faireum Token", "FAIRC": "Faireum Token",
"FAIRG": "FairGame", "FAIRG": "FairGame",
"FAKE": "FAKE COIN",
"FAKT": "Medifakt", "FAKT": "Medifakt",
"FALCONS": "Falcon Swaps", "FALCONS": "Falcon Swaps",
"FAME": "Fame MMA", "FAME": "Fame MMA",
@ -2870,6 +2900,7 @@
"FEN": "First Ever NFT", "FEN": "First Ever NFT",
"FENOMY": "Fenomy", "FENOMY": "Fenomy",
"FER": "Ferro", "FER": "Ferro",
"FERC": "FairERC20",
"FERMA": "Ferma", "FERMA": "Ferma",
"FESS": "Fesschain", "FESS": "Fesschain",
"FET": "Fetch.AI", "FET": "Fetch.AI",
@ -2931,7 +2962,7 @@
"FLASH": "Flashstake", "FLASH": "Flashstake",
"FLASHC": "FLASH coin", "FLASHC": "FLASH coin",
"FLC": "FlowChainCoin", "FLC": "FlowChainCoin",
"FLD": "FLUID", "FLD": "FluidAI",
"FLDC": "Folding Coin", "FLDC": "Folding Coin",
"FLDT": "FairyLand", "FLDT": "FairyLand",
"FLETA": "FLETA", "FLETA": "FLETA",
@ -3091,6 +3122,7 @@
"FUEL": "Jetfuel Finance", "FUEL": "Jetfuel Finance",
"FUJIN": "Fujinto", "FUJIN": "Fujinto",
"FUKU": "Furukuru", "FUKU": "Furukuru",
"FUMO": "Alien Milady Fumo",
"FUN": "FUN Token", "FUN": "FUN Token",
"FUNC": "FunCoin", "FUNC": "FunCoin",
"FUND": "Unification", "FUND": "Unification",
@ -3101,6 +3133,7 @@
"FUNDZ": "FundFantasy", "FUNDZ": "FundFantasy",
"FUNK": "Cypherfunks Coin", "FUNK": "Cypherfunks Coin",
"FUR": "Furio", "FUR": "Furio",
"FURU": "Furucombo",
"FURY": "Engines of Fury", "FURY": "Engines of Fury",
"FUS": "Fus", "FUS": "Fus",
"FUSE": "Fuse Network Token", "FUSE": "Fuse Network Token",
@ -3118,6 +3151,7 @@
"FXP": "FXPay", "FXP": "FXPay",
"FXS": "Frax Share", "FXS": "Frax Share",
"FXT": "FuzeX", "FXT": "FuzeX",
"FXY": "Floxypay",
"FYN": "Affyn", "FYN": "Affyn",
"FYP": "FlypMe", "FYP": "FlypMe",
"FYZ": "Fyooz", "FYZ": "Fyooz",
@ -3172,6 +3206,7 @@
"GAT": "GATCOIN", "GAT": "GATCOIN",
"GATE": "GATENet", "GATE": "GATENet",
"GATEWAY": "Gateway Protocol", "GATEWAY": "Gateway Protocol",
"GAYPEPE": "Gay Pepe",
"GAZE": "GazeTV", "GAZE": "GazeTV",
"GB": "GoldBlocks", "GB": "GoldBlocks",
"GBA": "Geeba", "GBA": "Geeba",
@ -3222,6 +3257,7 @@
"GEMZ": "Gemz Social", "GEMZ": "Gemz Social",
"GEN": "DAOstack", "GEN": "DAOstack",
"GENE": "Genopets", "GENE": "Genopets",
"GENIE": "The Genie",
"GENIX": "Genix", "GENIX": "Genix",
"GENS": "Genshiro", "GENS": "Genshiro",
"GENSTAKE": "Genstake", "GENSTAKE": "Genstake",
@ -3261,6 +3297,7 @@
"GHCOLD": "Galaxy Heroes Coin", "GHCOLD": "Galaxy Heroes Coin",
"GHD": "Giftedhands", "GHD": "Giftedhands",
"GHNY": "Grizzly Honey", "GHNY": "Grizzly Honey",
"GHO": "GHO",
"GHOST": "GhostbyMcAfee", "GHOST": "GhostbyMcAfee",
"GHOSTCOIN": "GhostCoin", "GHOSTCOIN": "GhostCoin",
"GHOSTM": "GhostMarket", "GHOSTM": "GhostMarket",
@ -3274,6 +3311,7 @@
"GIFT": "GiftNet", "GIFT": "GiftNet",
"GIG": "GigaCoin", "GIG": "GigaCoin",
"GIGA": "GigaSwap", "GIGA": "GigaSwap",
"GIGX": "GigXCoin",
"GIM": "Gimli", "GIM": "Gimli",
"GIMMER": "Gimmer", "GIMMER": "Gimmer",
"GIN": "GINcoin", "GIN": "GINcoin",
@ -3385,6 +3423,7 @@
"GOVT": "The Government Network", "GOVT": "The Government Network",
"GOZ": "Göztepe S.K. Fan Token", "GOZ": "Göztepe S.K. Fan Token",
"GP": "Wizards And Dragons", "GP": "Wizards And Dragons",
"GPBP": "Genius Playboy Billionaire Philanthropist",
"GPKR": "Gold Poker", "GPKR": "Gold Poker",
"GPL": "Gold Pressed Latinum", "GPL": "Gold Pressed Latinum",
"GPPT": "Pluto Project Coin", "GPPT": "Pluto Project Coin",
@ -3501,7 +3540,8 @@
"HALF": "0.5X Long Bitcoin Token", "HALF": "0.5X Long Bitcoin Token",
"HALFSHIT": "0.5X Long Shitcoin Index Token", "HALFSHIT": "0.5X Long Shitcoin Index Token",
"HALLO": "Halloween Coin", "HALLO": "Halloween Coin",
"HALO": "Halo Platform", "HALO": "Halo Coin",
"HALOPLATFORM": "Halo Platform",
"HAM": "Hamster", "HAM": "Hamster",
"HAMS": "HamsterCoin", "HAMS": "HamsterCoin",
"HANA": "Hanacoin", "HANA": "Hanacoin",
@ -3598,6 +3638,7 @@
"HILL": "President Clinton", "HILL": "President Clinton",
"HINA": "Hina Inu", "HINA": "Hina Inu",
"HINT": "Hintchain", "HINT": "Hintchain",
"HIPPO": "HIPPO",
"HIRE": "HireMatch", "HIRE": "HireMatch",
"HIT": "HitChain", "HIT": "HitChain",
"HITBTC": "HitBTC Token", "HITBTC": "HitBTC Token",
@ -3634,6 +3675,7 @@
"HNTR": "Hunter", "HNTR": "Hunter",
"HNY": "Honey", "HNY": "Honey",
"HNZO": "Hanzo Inu", "HNZO": "Hanzo Inu",
"HOBO": "HOBO THE BEAR",
"HOD": "HoDooi.com", "HOD": "HoDooi.com",
"HODL": "HOdlcoin", "HODL": "HOdlcoin",
"HOGE": "Hoge Finance", "HOGE": "Hoge Finance",
@ -3839,7 +3881,7 @@
"IMPCN": "Brain Space", "IMPCN": "Brain Space",
"IMPER": "Impermax", "IMPER": "Impermax",
"IMPS": "Impulse Coin", "IMPS": "Impulse Coin",
"IMPT": "Ether Kingdoms Token", "IMPT": "IMPT",
"IMPULSE": "IMPULSE by FDR", "IMPULSE": "IMPULSE by FDR",
"IMS": "Independent Money System", "IMS": "Independent Money System",
"IMST": "Imsmart", "IMST": "Imsmart",
@ -4001,6 +4043,7 @@
"JAM": "Tune.Fm", "JAM": "Tune.Fm",
"JANE": "JaneCoin", "JANE": "JaneCoin",
"JAR": "Jarvis+", "JAR": "Jarvis+",
"JARED": "Jared From Subway",
"JASMY": "JasmyCoin", "JASMY": "JasmyCoin",
"JBS": "JumBucks Coin", "JBS": "JumBucks Coin",
"JBX": "Juicebox", "JBX": "Juicebox",
@ -4163,9 +4206,10 @@
"KIN": "Kin", "KIN": "Kin",
"KIND": "Kind Ads", "KIND": "Kind Ads",
"KINE": "Kine Protocol", "KINE": "Kine Protocol",
"KING": "King Finance", "KING": "KING",
"KING93": "King93", "KING93": "King93",
"KINGDOMQUEST": "Kingdom Quest", "KINGDOMQUEST": "Kingdom Quest",
"KINGF": "King Finance",
"KINGSHIB": "King Shiba", "KINGSHIB": "King Shiba",
"KINGSWAP": "KingSwap", "KINGSWAP": "KingSwap",
"KINT": "Kintsugi", "KINT": "Kintsugi",
@ -4175,6 +4219,7 @@
"KISC": "Kaiser", "KISC": "Kaiser",
"KISHIMOTO": "Kishimoto Inu", "KISHIMOTO": "Kishimoto Inu",
"KISHU": "Kishu Inu", "KISHU": "Kishu Inu",
"KITA": "KITA INU",
"KITSU": "Kitsune Inu", "KITSU": "Kitsune Inu",
"KITTY": "Kitty Inu", "KITTY": "Kitty Inu",
"KKO": "Kineko", "KKO": "Kineko",
@ -4267,10 +4312,12 @@
"KUBO": "KUBO", "KUBO": "KUBO",
"KUBOS": "KubosCoin", "KUBOS": "KubosCoin",
"KUE": "Kuende", "KUE": "Kuende",
"KUJI": "Kujira",
"KUMA": "Kuma Inu", "KUMA": "Kuma Inu",
"KUNCI": "Kunci Coin", "KUNCI": "Kunci Coin",
"KUR": "Kuro", "KUR": "Kuro",
"KURT": "Kurrent", "KURT": "Kurrent",
"KUSA": "Kusa Inu",
"KUSD": "Kowala", "KUSD": "Kowala",
"KUSH": "KushCoin", "KUSH": "KushCoin",
"KUV": "Kuverit", "KUV": "Kuverit",
@ -4280,6 +4327,7 @@
"KVT": "Kinesis Velocity Token", "KVT": "Kinesis Velocity Token",
"KWATT": "4New", "KWATT": "4New",
"KWD": "KIWI DEFI", "KWD": "KIWI DEFI",
"KWENTA": "Kwenta",
"KWH": "KWHCoin", "KWH": "KWHCoin",
"KWIK": "KwikSwap", "KWIK": "KwikSwap",
"KWS": "Knight War Spirits", "KWS": "Knight War Spirits",
@ -4299,7 +4347,9 @@
"LABX": "Stakinglab", "LABX": "Stakinglab",
"LACCOIN": "LocalAgro", "LACCOIN": "LocalAgro",
"LACE": "Lovelace World", "LACE": "Lovelace World",
"LADYS": "Milady Meme Coin",
"LAEEB": "LaEeb", "LAEEB": "LaEeb",
"LAELAPS": "Laelaps",
"LAIKA": "Laika Protocol", "LAIKA": "Laika Protocol",
"LALA": "LaLa World", "LALA": "LaLa World",
"LAMB": "Lambda", "LAMB": "Lambda",
@ -4455,13 +4505,14 @@
"LLAND": "Lyfe Land", "LLAND": "Lyfe Land",
"LLG": "Loligo", "LLG": "Loligo",
"LLION": "Lydian Lion", "LLION": "Lydian Lion",
"LM": "LM Token", "LM": "LeisureMeta",
"LMAO": "LMAO Finance", "LMAO": "LMAO Finance",
"LMC": "LomoCoin", "LMC": "LomoCoin",
"LMCH": "Latamcash", "LMCH": "Latamcash",
"LMCSWAP": "LimoCoin SWAP", "LMCSWAP": "LimoCoin SWAP",
"LMR": "Lumerin", "LMR": "Lumerin",
"LMT": "Lympo Market Token", "LMT": "Lympo Market Token",
"LMTOKEN": "LM Token",
"LMXC": "LimonX", "LMXC": "LimonX",
"LMY": "Lunch Money", "LMY": "Lunch Money",
"LN": "LINK", "LN": "LINK",
@ -4530,6 +4581,7 @@
"LRG": "Largo Coin", "LRG": "Largo Coin",
"LRN": "Loopring [NEO]", "LRN": "Loopring [NEO]",
"LSD": "LightSpeedCoin", "LSD": "LightSpeedCoin",
"LSETH": "Liquid Staked ETH",
"LSK": "Lisk", "LSK": "Lisk",
"LSP": "Lumenswap", "LSP": "Lumenswap",
"LSS": "Lossless", "LSS": "Lossless",
@ -4626,6 +4678,7 @@
"MAEP": "Maester Protocol", "MAEP": "Maester Protocol",
"MAG": "Magnet", "MAG": "Magnet",
"MAGIC": "Magic", "MAGIC": "Magic",
"MAGICF": "MagicFox",
"MAHA": "MahaDAO", "MAHA": "MahaDAO",
"MAI": "Mindsync", "MAI": "Mindsync",
"MAID": "MaidSafe Coin", "MAID": "MaidSafe Coin",
@ -4639,6 +4692,7 @@
"MANDOX": "MandoX", "MANDOX": "MandoX",
"MANGA": "Manga Token", "MANGA": "Manga Token",
"MANNA": "Manna", "MANNA": "Manna",
"MANTLE": "Mantle",
"MAP": "MAP Protocol", "MAP": "MAP Protocol",
"MAPC": "MapCoin", "MAPC": "MapCoin",
"MAPE": "Mecha Morphing", "MAPE": "Mecha Morphing",
@ -4672,6 +4726,7 @@
"MATIC": "Polygon", "MATIC": "Polygon",
"MATPAD": "MaticPad", "MATPAD": "MaticPad",
"MATTER": "AntiMatter", "MATTER": "AntiMatter",
"MAV": "Maverick Protocol",
"MAX": "MaxCoin", "MAX": "MaxCoin",
"MAXR": "Max Revive", "MAXR": "Max Revive",
"MAY": "Theresa May Coin", "MAY": "Theresa May Coin",
@ -4776,6 +4831,7 @@
"MESA": "MetaVisa", "MESA": "MetaVisa",
"MESG": "MESG", "MESG": "MESG",
"MESH": "MeshBox", "MESH": "MeshBox",
"MESSI": "MESSI COIN",
"MET": "Metronome", "MET": "Metronome",
"META": "Metadium", "META": "Metadium",
"METAC": "Metacoin", "METAC": "Metacoin",
@ -4881,6 +4937,7 @@
"MIODIO": "MIODIOCOIN", "MIODIO": "MIODIOCOIN",
"MIOTA": "IOTA", "MIOTA": "IOTA",
"MIR": "Mirror Protocol", "MIR": "Mirror Protocol",
"MIRACLE": "MIRACLE",
"MIRC": "MIR COIN", "MIRC": "MIR COIN",
"MIS": "Mithril Share", "MIS": "Mithril Share",
"MISA": "Sangkara", "MISA": "Sangkara",
@ -4938,7 +4995,6 @@
"MNRB": "MoneyRebel", "MNRB": "MoneyRebel",
"MNS": "Monnos", "MNS": "Monnos",
"MNST": "MoonStarter", "MNST": "MoonStarter",
"MNT": "microNFT",
"MNTC": "Manet Coin", "MNTC": "Manet Coin",
"MNTG": "Monetas", "MNTG": "Monetas",
"MNTL": "AssetMantle", "MNTL": "AssetMantle",
@ -4967,6 +5023,7 @@
"MOF": "Molecular Future (TRC20)", "MOF": "Molecular Future (TRC20)",
"MOFI": "MobiFi", "MOFI": "MobiFi",
"MOFOLD": "Molecular Future (ERC20)", "MOFOLD": "Molecular Future (ERC20)",
"MOG": "Mog Coin",
"MOGU": "Mogu", "MOGU": "Mogu",
"MOGX": "Mogu", "MOGX": "Mogu",
"MOI": "MyOwnItem", "MOI": "MyOwnItem",
@ -4989,9 +5046,11 @@
"MONEYIMT": "MoneyToken", "MONEYIMT": "MoneyToken",
"MONF": "Monfter", "MONF": "Monfter",
"MONG": "MongCoin", "MONG": "MongCoin",
"MONG20": "Mongoose 2.0",
"MONI": "Monsta Infinite", "MONI": "Monsta Infinite",
"MONK": "Monkey Project", "MONK": "Monkey Project",
"MONKEY": "Monkey", "MONKEY": "Monkey",
"MONKEYS": "Monkeys Token",
"MONO": "MonoX", "MONO": "MonoX",
"MONONOKEINU": "Mononoke Inu", "MONONOKEINU": "Mononoke Inu",
"MONS": "Monsters Clan", "MONS": "Monsters Clan",
@ -5011,11 +5070,13 @@
"MOONSHOT": "Moonshot", "MOONSHOT": "Moonshot",
"MOOO": "Hashtagger", "MOOO": "Hashtagger",
"MOOV": "dotmoovs", "MOOV": "dotmoovs",
"MOOX": "Moox Protocol",
"MOPS": "Mops", "MOPS": "Mops",
"MORA": "Meliora", "MORA": "Meliora",
"MORE": "More Coin", "MORE": "More Coin",
"MOS": "MOS Coin", "MOS": "MOS Coin",
"MOT": "Olympus Labs", "MOT": "Olympus Labs",
"MOTG": "MetaOctagon",
"MOTI": "Motion", "MOTI": "Motion",
"MOTO": "Motocoin", "MOTO": "Motocoin",
"MOV": "MovieCoin", "MOV": "MovieCoin",
@ -5076,6 +5137,7 @@
"MSWAP": "MoneySwap", "MSWAP": "MoneySwap",
"MT": "MyToken", "MT": "MyToken",
"MTA": "Meta", "MTA": "Meta",
"MTB": "MetaBridge",
"MTBC": "Metabolic", "MTBC": "Metabolic",
"MTC": "MEDICAL TOKEN CURRENCY", "MTC": "MEDICAL TOKEN CURRENCY",
"MTCMN": "MTC Mesh", "MTCMN": "MTC Mesh",
@ -5108,6 +5170,7 @@
"MUE": "MonetaryUnit", "MUE": "MonetaryUnit",
"MULTI": "Multichain", "MULTI": "Multichain",
"MULTIBOT": "Multibot", "MULTIBOT": "Multibot",
"MULTIV": "Multiverse",
"MUN": "MUNcoin", "MUN": "MUNcoin",
"MUNCH": "Munch Token", "MUNCH": "Munch Token",
"MUSD": "mStable USD", "MUSD": "mStable USD",
@ -5648,6 +5711,7 @@
"OZP": "OZAPHYRE", "OZP": "OZAPHYRE",
"P202": "Project 202", "P202": "Project 202",
"P2PS": "P2P Solutions Foundation", "P2PS": "P2P Solutions Foundation",
"PAAL": "PAAL AI",
"PAC": "PAC Protocol", "PAC": "PAC Protocol",
"PACOCA": "Pacoca", "PACOCA": "Pacoca",
"PAD": "NearPad", "PAD": "NearPad",
@ -5736,6 +5800,7 @@
"PEARL": "Pearl Finance", "PEARL": "Pearl Finance",
"PEC": "PeaceCoin", "PEC": "PeaceCoin",
"PEEL": "Meta Apes", "PEEL": "Meta Apes",
"PEEPA": "Peepa",
"PEEPS": "The People’s Coin", "PEEPS": "The People’s Coin",
"PEG": "PegNet", "PEG": "PegNet",
"PEGS": "PegShares", "PEGS": "PegShares",
@ -5748,6 +5813,7 @@
"PEOPLE": "ConstitutionDAO", "PEOPLE": "ConstitutionDAO",
"PEOS": "pEOS", "PEOS": "pEOS",
"PEPE": "Pepe", "PEPE": "Pepe",
"PEPE20": "Pepe 2.0",
"PEPECASH": "Pepe Cash", "PEPECASH": "Pepe Cash",
"PEPPER": "Pepper Token", "PEPPER": "Pepper Token",
"PEPS": "PEPS Coin", "PEPS": "PEPS Coin",
@ -5822,6 +5888,7 @@
"PINK": "PinkCoin", "PINK": "PinkCoin",
"PINKX": "PantherCoin", "PINKX": "PantherCoin",
"PINMO": "Pinmo", "PINMO": "Pinmo",
"PINO": "Pinocchu",
"PINU": "Piccolo Inu", "PINU": "Piccolo Inu",
"PIO": "Pioneershares", "PIO": "Pioneershares",
"PIPI": "Pippi Finance", "PIPI": "Pippi Finance",
@ -5885,6 +5952,7 @@
"PLS": "Pulsechain", "PLS": "Pulsechain",
"PLSD": "PulseDogecoin", "PLSD": "PulseDogecoin",
"PLSPAD": "PulsePad", "PLSPAD": "PulsePad",
"PLSX": "PulseX",
"PLT": "Poollotto.finance", "PLT": "Poollotto.finance",
"PLTC": "PlatonCoin", "PLTC": "PlatonCoin",
"PLTX": "PlutusX", "PLTX": "PlutusX",
@ -5911,7 +5979,6 @@
"PNK": "Kleros", "PNK": "Kleros",
"PNL": "True PNL", "PNL": "True PNL",
"PNODE": "Pinknode", "PNODE": "Pinknode",
"PNP": "LogisticsX",
"PNT": "pNetwork Token", "PNT": "pNetwork Token",
"PNX": "PhantomX", "PNX": "PhantomX",
"PNY": "Peony Coin", "PNY": "Peony Coin",
@ -5927,6 +5994,7 @@
"POINTS": "Cryptsy Points", "POINTS": "Cryptsy Points",
"POK": "Pokmonsters", "POK": "Pokmonsters",
"POKEM": "Pokemonio", "POKEM": "Pokemonio",
"POKEMON": "Pokemon",
"POKER": "PokerCoin", "POKER": "PokerCoin",
"POKT": "Pocket Network", "POKT": "Pocket Network",
"POL": "Pool-X", "POL": "Pool-X",
@ -6010,6 +6078,7 @@
"PRIME": "Echelon Prime", "PRIME": "Echelon Prime",
"PRIMECHAIN": "PrimeChain", "PRIMECHAIN": "PrimeChain",
"PRINT": "Printer.Finance", "PRINT": "Printer.Finance",
"PRINTERIUM": "Printerium",
"PRINTS": "FingerprintsDAO", "PRINTS": "FingerprintsDAO",
"PRISM": "Prism", "PRISM": "Prism",
"PRIX": "Privatix", "PRIX": "Privatix",
@ -6033,7 +6102,7 @@
"PROTON": "Proton", "PROTON": "Proton",
"PROUD": "PROUD Money", "PROUD": "PROUD Money",
"PROXI": "PROXI", "PROXI": "PROXI",
"PRP": "Papyrus", "PRP": "Pepe Prime",
"PRPS": "Purpose", "PRPS": "Purpose",
"PRPT": "Purple Token", "PRPT": "Purple Token",
"PRQ": "PARSIQ", "PRQ": "PARSIQ",
@ -6042,7 +6111,7 @@
"PRTG": "Pre-Retogeum", "PRTG": "Pre-Retogeum",
"PRV": "PrivacySwap", "PRV": "PrivacySwap",
"PRVS": "Previse", "PRVS": "Previse",
"PRX": "Printerium", "PRX": "Parex",
"PRXY": "Proxy", "PRXY": "Proxy",
"PRY": "PRIMARY", "PRY": "PRIMARY",
"PSB": "Planet Sandbox", "PSB": "Planet Sandbox",
@ -6120,6 +6189,7 @@
"PYRAM": "Pyram Token", "PYRAM": "Pyram Token",
"PYRK": "Pyrk", "PYRK": "Pyrk",
"PYT": "Payther", "PYT": "Payther",
"PYUSD": "PayPal USD",
"PZM": "Prizm", "PZM": "Prizm",
"Q1S": "Quantum1Net", "Q1S": "Quantum1Net",
"Q2C": "QubitCoin", "Q2C": "QubitCoin",
@ -6178,6 +6248,7 @@
"QUA": "Quantum Tech", "QUA": "Quantum Tech",
"QUACK": "Rich Quack", "QUACK": "Rich Quack",
"QUAM": "Quam Network", "QUAM": "Quam Network",
"QUANT": "Quant Finance",
"QUARASHI": "Quarashi Network", "QUARASHI": "Quarashi Network",
"QUARTZ": "Sandclock", "QUARTZ": "Sandclock",
"QUASA": "Quasacoin", "QUASA": "Quasacoin",
@ -6201,7 +6272,7 @@
"RAC": "RAcoin", "RAC": "RAcoin",
"RACA": "Radio Caca", "RACA": "Radio Caca",
"RACEFI": "RaceFi", "RACEFI": "RaceFi",
"RAD": "Radicle", "RAD": "Radworks",
"RADAR": "DappRadar", "RADAR": "DappRadar",
"RADI": "RadicalCoin", "RADI": "RadicalCoin",
"RADIO": "RadioShack", "RADIO": "RadioShack",
@ -6220,7 +6291,7 @@
"RAM": "Ramifi Protocol", "RAM": "Ramifi Protocol",
"RAMP": "RAMP", "RAMP": "RAMP",
"RANKER": "RankerDao", "RANKER": "RankerDao",
"RAP": "Rapture", "RAP": "Philosoraptor",
"RAPDOGE": "RapDoge", "RAPDOGE": "RapDoge",
"RARE": "SuperRare", "RARE": "SuperRare",
"RARI": "Rarible", "RARI": "Rarible",
@ -6277,6 +6348,7 @@
"REA": "Realisto", "REA": "Realisto",
"REAL": "RealLink", "REAL": "RealLink",
"REALM": "Realm", "REALM": "Realm",
"REALMS": "Realms of Ethernity",
"REALPLATFORM": "REAL", "REALPLATFORM": "REAL",
"REALY": "Realy Metaverse", "REALY": "Realy Metaverse",
"REAP": "ReapChain", "REAP": "ReapChain",
@ -6287,6 +6359,7 @@
"RED": "RED TOKEN", "RED": "RED TOKEN",
"REDC": "RedCab", "REDC": "RedCab",
"REDCO": "Redcoin", "REDCO": "Redcoin",
"REDDIT": "Reddit",
"REDI": "REDi", "REDI": "REDi",
"REDLANG": "RED", "REDLANG": "RED",
"REDLC": "Redlight Chain", "REDLC": "Redlight Chain",
@ -6324,7 +6397,7 @@
"REST": "Restore", "REST": "Restore",
"RET": "RealTract", "RET": "RealTract",
"RETAIL": "Retail.Global", "RETAIL": "Retail.Global",
"RETH": "Realms of Ethernity", "RETH": "Rocket Pool ETH",
"RETH2": "rETH2", "RETH2": "rETH2",
"RETIRE": "Retire Token", "RETIRE": "Retire Token",
"REU": "REUCOIN", "REU": "REUCOIN",
@ -6351,6 +6424,7 @@
"RGP": "Rigel Protocol", "RGP": "Rigel Protocol",
"RGT": "Rari Governance Token", "RGT": "Rari Governance Token",
"RHEA": "Rhea", "RHEA": "Rhea",
"RHINO": "RHINO",
"RHOC": "RChain", "RHOC": "RChain",
"RHP": "Rhypton Club", "RHP": "Rhypton Club",
"RIC": "Riecoin", "RIC": "Riecoin",
@ -6490,6 +6564,7 @@
"RWE": "Real-World Evidence", "RWE": "Real-World Evidence",
"RWN": "Rowan Token", "RWN": "Rowan Token",
"RWS": "Robonomics Web Services", "RWS": "Robonomics Web Services",
"RXD": "Radiant",
"RXT": "RIMAUNANGIS", "RXT": "RIMAUNANGIS",
"RYC": "RoyalCoin", "RYC": "RoyalCoin",
"RYCN": "RoyalCoin 2.0", "RYCN": "RoyalCoin 2.0",
@ -6564,6 +6639,7 @@
"SBTC": "Super Bitcoin", "SBTC": "Super Bitcoin",
"SC": "Siacoin", "SC": "Siacoin",
"SCA": "SiaClassic", "SCA": "SiaClassic",
"SCAM": "Scam Coin",
"SCAP": "SafeCapital", "SCAP": "SafeCapital",
"SCAR": "Velhalla", "SCAR": "Velhalla",
"SCASH": "SpaceCash", "SCASH": "SpaceCash",
@ -6624,6 +6700,7 @@
"SEER": "SEER", "SEER": "SEER",
"SEI": "Sei", "SEI": "Sei",
"SEL": "SelenCoin", "SEL": "SelenCoin",
"SELF": "SELFCrypto",
"SEM": "Semux", "SEM": "Semux",
"SEN": "Sentaro", "SEN": "Sentaro",
"SENATE": "SENATE", "SENATE": "SENATE",
@ -6665,6 +6742,7 @@
"SGE": "Society of Galactic Exploration", "SGE": "Society of Galactic Exploration",
"SGLY": "Singularity", "SGLY": "Singularity",
"SGN": "Signals Network", "SGN": "Signals Network",
"SGO": "SafuuGO",
"SGOLD": "SpaceGold", "SGOLD": "SpaceGold",
"SGP": "SGPay", "SGP": "SGPay",
"SGR": "Sogur Currency", "SGR": "Sogur Currency",
@ -6684,6 +6762,7 @@
"SHEESH": "Sheesh it is bussin bussin", "SHEESH": "Sheesh it is bussin bussin",
"SHEESHA": "Sheesha Finance", "SHEESHA": "Sheesha Finance",
"SHELL": "Shell Token", "SHELL": "Shell Token",
"SHERA": "Shera Tokens",
"SHFL": "SHUFFLE!", "SHFL": "SHUFFLE!",
"SHFT": "Shyft Network", "SHFT": "Shyft Network",
"SHI": "Shirtum", "SHI": "Shirtum",
@ -6719,6 +6798,8 @@
"SHR": "ShareToken", "SHR": "ShareToken",
"SHREK": "ShrekCoin", "SHREK": "ShrekCoin",
"SHROOM": "Shroom.Finance", "SHROOM": "Shroom.Finance",
"SHROOMFOX": "Magic Shroom",
"SHS": "SHEESH",
"SHX": "Stronghold Token", "SHX": "Stronghold Token",
"SI": "Siren", "SI": "Siren",
"SIB": "SibCoin", "SIB": "SibCoin",
@ -7018,9 +7099,11 @@
"STEN": "Steneum Coin", "STEN": "Steneum Coin",
"STEP": "Step Finance", "STEP": "Step Finance",
"STEPH": "Step Hero", "STEPH": "Step Hero",
"STEPR": "Step",
"STEPS": "Steps", "STEPS": "Steps",
"STERLINGCOIN": "SterlingCoin", "STERLINGCOIN": "SterlingCoin",
"STETH": "Staked Ether", "STETH": "Staked Ether",
"STEWIE": "Stewie Coin",
"STEX": "STEX", "STEX": "STEX",
"STF": "Structure Finance", "STF": "Structure Finance",
"STFX": "STFX", "STFX": "STFX",
@ -7055,7 +7138,7 @@
"STR": "Sourceless", "STR": "Sourceless",
"STRAKS": "Straks", "STRAKS": "Straks",
"STRAX": "Stratis", "STRAX": "Stratis",
"STRAY": "Animal Token", "STRAY": "Stray Dog",
"STREAM": "STREAMIT COIN", "STREAM": "STREAMIT COIN",
"STRIP": "Stripto", "STRIP": "Stripto",
"STRK": "Strike", "STRK": "Strike",
@ -7361,6 +7444,7 @@
"TOM": "TOM Finance", "TOM": "TOM Finance",
"TOMAHAWKCOIN": "Tomahawkcoin", "TOMAHAWKCOIN": "Tomahawkcoin",
"TOMB": "Tomb", "TOMB": "Tomb",
"TOMI": "tomiNet",
"TOMO": "TomoChain", "TOMO": "TomoChain",
"TOMOE": "TomoChain ERC20", "TOMOE": "TomoChain ERC20",
"TOMS": "TomTomCoin", "TOMS": "TomTomCoin",
@ -7385,6 +7469,7 @@
"TOTM": "Totem", "TOTM": "Totem",
"TOWER": "Tower", "TOWER": "Tower",
"TOWN": "Town Star", "TOWN": "Town Star",
"TOX": "INTOverse",
"TOZ": "Tozex", "TOZ": "Tozex",
"TP": "Token Swap", "TP": "Token Swap",
"TPAD": "TrustPad", "TPAD": "TrustPad",
@ -7600,6 +7685,7 @@
"UNITY": "SuperNET", "UNITY": "SuperNET",
"UNIVRS": "Universe", "UNIVRS": "Universe",
"UNIX": "UniX", "UNIX": "UniX",
"UNLEASH": "UnleashClub",
"UNN": "UNION Protocol Governance Token", "UNN": "UNION Protocol Governance Token",
"UNO": "Unobtanium", "UNO": "Unobtanium",
"UNORE": "UnoRe", "UNORE": "UnoRe",
@ -7673,6 +7759,7 @@
"UTT": "United Traders Token", "UTT": "United Traders Token",
"UTU": "UTU Protocol", "UTU": "UTU Protocol",
"UUU": "U Network", "UUU": "U Network",
"UWU": "uwu",
"UZUMAKI": "Uzumaki Inu", "UZUMAKI": "Uzumaki Inu",
"VAB": "Vabble", "VAB": "Vabble",
"VADER": "Vader Protocol", "VADER": "Vader Protocol",
@ -7695,6 +7782,7 @@
"VCF": "Valencia CF Fan Token", "VCF": "Valencia CF Fan Token",
"VCG": "VCGamers", "VCG": "VCGamers",
"VCK": "28VCK", "VCK": "28VCK",
"VCORE": "VCORE",
"VDG": "VeriDocGlobal", "VDG": "VeriDocGlobal",
"VDL": "Vidulum", "VDL": "Vidulum",
"VDO": "VidioCoin", "VDO": "VidioCoin",
@ -7710,6 +7798,7 @@
"VEIL": "VEIL", "VEIL": "VEIL",
"VELA": "Vela Token", "VELA": "Vela Token",
"VELO": "Velo", "VELO": "Velo",
"VELOD": "Velodrome Finance",
"VELOX": "Velox", "VELOX": "Velox",
"VELOXPROJECT": "Velox", "VELOXPROJECT": "Velox",
"VEMP": "vEmpire DDAO", "VEMP": "vEmpire DDAO",
@ -7782,6 +7871,7 @@
"VNT": "VNT Chain", "VNT": "VNT Chain",
"VNTW": "Value Network Token", "VNTW": "Value Network Token",
"VNX": "VisionX", "VNX": "VisionX",
"VNXAU": "VNX Gold",
"VNXLU": "VNX Exchange", "VNXLU": "VNX Exchange",
"VOCO": "Provoco", "VOCO": "Provoco",
"VODKA": "Vodka Token", "VODKA": "Vodka Token",
@ -7902,7 +7992,8 @@
"WEC": "Whole Earth Coin", "WEC": "Whole Earth Coin",
"WEGEN": "WeGen Platform", "WEGEN": "WeGen Platform",
"WELD": "Weld", "WELD": "Weld",
"WELL": "Well", "WELL": "Moonwell",
"WELLTOKEN": "Well",
"WELT": "Fabwelt", "WELT": "Fabwelt",
"WELUPS": "Welups Blockchain", "WELUPS": "Welups Blockchain",
"WEMIX": "WEMIX", "WEMIX": "WEMIX",
@ -7958,6 +8049,7 @@
"WIX": "Wixlar", "WIX": "Wixlar",
"WIZ": "WIZ Protocol", "WIZ": "WIZ Protocol",
"WKD": "Wakanda Inu", "WKD": "Wakanda Inu",
"WLD": "Worldcoin",
"WLF": "Wolfs Group", "WLF": "Wolfs Group",
"WLITI": "wLITI", "WLITI": "wLITI",
"WLK": "Wolk", "WLK": "Wolk",
@ -7983,6 +8075,7 @@
"WNZ": "Winerz", "WNZ": "Winerz",
"WOA": "Wrapped Origin Axie", "WOA": "Wrapped Origin Axie",
"WOD": "World of Defish", "WOD": "World of Defish",
"WOID": "WORLD ID",
"WOJ": "Wojak Finance", "WOJ": "Wojak Finance",
"WOLF": "Insanity Coin", "WOLF": "Insanity Coin",
"WOLFILAND": "Wolfiland", "WOLFILAND": "Wolfiland",
@ -8000,6 +8093,7 @@
"WOOFY": "Woofy", "WOOFY": "Woofy",
"WOOL": "Wolf Game Wool", "WOOL": "Wolf Game Wool",
"WOONK": "Woonkly", "WOONK": "Woonkly",
"WOOO": "wooonen",
"WOOP": "Woonkly Power", "WOOP": "Woonkly Power",
"WOP": "WorldPay", "WOP": "WorldPay",
"WORLD": "World Token", "WORLD": "World Token",
@ -8010,6 +8104,7 @@
"WOZX": "Efforce", "WOZX": "Efforce",
"WPC": "WePiggy Coin", "WPC": "WePiggy Coin",
"WPE": "OPES (Wrapped PE)", "WPE": "OPES (Wrapped PE)",
"WPLS": "Wrapped Pulse",
"WPP": "Green Energy Token", "WPP": "Green Energy Token",
"WPR": "WePower", "WPR": "WePower",
"WQT": "Work Quest", "WQT": "Work Quest",
@ -8049,6 +8144,7 @@
"WZEC": "Wrapped Zcash", "WZEC": "Wrapped Zcash",
"WZENIQ": "Wrapped Zeniq (ETH)", "WZENIQ": "Wrapped Zeniq (ETH)",
"WZRD": "Wizardia", "WZRD": "Wizardia",
"X": "AI-X",
"X2": "X2Coin", "X2": "X2Coin",
"X2Y2": "X2Y2", "X2Y2": "X2Y2",
"X42": "X42 Protocol", "X42": "X42 Protocol",
@ -8096,7 +8192,7 @@
"XCI": "Cannabis Industry Coin", "XCI": "Cannabis Industry Coin",
"XCLR": "ClearCoin", "XCLR": "ClearCoin",
"XCM": "CoinMetro", "XCM": "CoinMetro",
"XCN": "Chain", "XCN": "Onyxcoin",
"XCO": "XCoin", "XCO": "XCoin",
"XCONSOL": "X-Consoles", "XCONSOL": "X-Consoles",
"XCP": "CounterParty", "XCP": "CounterParty",
@ -8365,6 +8461,7 @@
"YUANG": "Yuang Coin", "YUANG": "Yuang Coin",
"YUCJ": "Yu Coin", "YUCJ": "Yu Coin",
"YUCT": "Yucreat", "YUCT": "Yucreat",
"YUDI": "Yudi",
"YUM": "Yumerium", "YUM": "Yumerium",
"YUMMY": "Yummy", "YUMMY": "Yummy",
"YUP": "Crowdholding", "YUP": "Crowdholding",

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

@ -66,6 +66,10 @@
<loc>https://ghostfol.io/de/ueber-uns/lizenz</loc> <loc>https://ghostfol.io/de/ueber-uns/lizenz</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ueber-uns/oss-friends</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en</loc> <loc>https://ghostfol.io/en</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -82,6 +86,10 @@
<loc>https://ghostfol.io/en/about/license</loc> <loc>https://ghostfol.io/en/about/license</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/about/oss-friends</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/blog</loc> <loc>https://ghostfol.io/en/blog</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -314,6 +322,10 @@
<loc>https://ghostfol.io/es/sobre/licencia</loc> <loc>https://ghostfol.io/es/sobre/licencia</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/es/sobre/oss-friends</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/es/sobre/politica-de-privacidad</loc> <loc>https://ghostfol.io/es/sobre/politica-de-privacidad</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -334,6 +346,10 @@
<loc>https://ghostfol.io/fr/a-propos/licence</loc> <loc>https://ghostfol.io/fr/a-propos/licence</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/fr/a-propos/oss-friends</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/fr/a-propos/politique-de-confidentialite</loc> <loc>https://ghostfol.io/fr/a-propos/politique-de-confidentialite</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -388,12 +404,16 @@
<loc>https://ghostfol.io/it/informazioni-su/changelog</loc> <loc>https://ghostfol.io/it/informazioni-su/changelog</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/informazioni-su/informativa-sulla-privacy</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/it/informazioni-su/licenza</loc> <loc>https://ghostfol.io/it/informazioni-su/licenza</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/it/informazioni-su/informativa-sulla-privacy</loc> <loc>https://ghostfol.io/it/informazioni-su/oss-friends</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
@ -452,6 +472,10 @@
<loc>https://ghostfol.io/nl/over/licentie</loc> <loc>https://ghostfol.io/nl/over/licentie</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/over/oss-friends</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/over/privacybeleid</loc> <loc>https://ghostfol.io/nl/over/privacybeleid</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -512,6 +536,10 @@
<loc>https://ghostfol.io/pt/sobre/licenca</loc> <loc>https://ghostfol.io/pt/sobre/licenca</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/pt/sobre/oss-friends</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/pt/sobre/politica-de-privacidade</loc> <loc>https://ghostfol.io/pt/sobre/politica-de-privacidade</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>

3
apps/api/src/main.ts

@ -7,6 +7,7 @@ import helmet from 'helmet';
import { AppModule } from './app/app.module'; import { AppModule } from './app/app.module';
import { environment } from './environments/environment'; import { environment } from './environments/environment';
import { HtmlTemplateMiddleware } from './middlewares/html-template.middleware';
async function bootstrap() { async function bootstrap() {
const configApp = await NestFactory.create(AppModule); const configApp = await NestFactory.create(AppModule);
@ -52,6 +53,8 @@ async function bootstrap() {
); );
} }
app.use(HtmlTemplateMiddleware);
const BASE_CURRENCY = configService.get<string>('BASE_CURRENCY'); const BASE_CURRENCY = configService.get<string>('BASE_CURRENCY');
const HOST = configService.get<string>('HOST') || '0.0.0.0'; const HOST = configService.get<string>('HOST') || '0.0.0.0';
const PORT = configService.get<number>('PORT') || 3333; const PORT = configService.get<number>('PORT') || 3333;

128
apps/api/src/middlewares/html-template.middleware.ts

@ -0,0 +1,128 @@
import * as fs from 'fs';
import { join } from 'path';
import { environment } from '@ghostfolio/api/environments/environment';
import {
DEFAULT_LANGUAGE_CODE,
DEFAULT_ROOT_URL,
SUPPORTED_LANGUAGE_CODES
} from '@ghostfolio/common/config';
import { DATE_FORMAT, interpolate } from '@ghostfolio/common/helper';
import { format } from 'date-fns';
import { NextFunction, Request, Response } from 'express';
const descriptions = {
de: 'Mit dem Finanz-Dashboard Ghostfolio können Sie Ihr Vermögen in Form von Aktien, ETFs oder Kryptowährungen verteilt über mehrere Finanzinstitute überwachen.',
en: 'Ghostfolio is a personal finance dashboard to keep track of your assets like stocks, ETFs or cryptocurrencies across multiple platforms.',
es: 'Ghostfolio es un dashboard de finanzas personales para hacer un seguimiento de tus activos como acciones, ETFs o criptodivisas a través de múltiples plataformas.',
fr: 'Ghostfolio est un dashboard de finances personnelles qui permet de suivre vos actifs comme les actions, les ETF ou les crypto-monnaies sur plusieurs plateformes.',
it: 'Ghostfolio è un dashboard di finanza personale per tenere traccia delle vostre attività come azioni, ETF o criptovalute su più piattaforme.',
nl: 'Ghostfolio is een persoonlijk financieel dashboard om uw activa zoals aandelen, ETF’s of cryptocurrencies over meerdere platforms bij te houden.',
pt: 'Ghostfolio é um dashboard de finanças pessoais para acompanhar os seus activos como acções, ETFs ou criptomoedas em múltiplas plataformas.'
};
const title = 'Ghostfolio – Open Source Wealth Management Software';
const titleShort = 'Ghostfolio';
let indexHtmlMap: { [languageCode: string]: string } = {};
try {
indexHtmlMap = SUPPORTED_LANGUAGE_CODES.reduce(
(map, languageCode) => ({
...map,
[languageCode]: fs.readFileSync(
join(__dirname, '..', 'client', languageCode, 'index.html'),
'utf8'
)
}),
{}
);
} catch {}
const locales = {
'/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt': {
featureGraphicPath: 'assets/images/blog/ghostfolio-x-sackgeld.png',
title: `Ghostfolio auf Sackgeld.com vorgestellt - ${titleShort}`
},
'/en/blog/2022/08/500-stars-on-github': {
featureGraphicPath: 'assets/images/blog/500-stars-on-github.jpg',
title: `500 Stars - ${titleShort}`
},
'/en/blog/2022/10/hacktoberfest-2022': {
featureGraphicPath: 'assets/images/blog/hacktoberfest-2022.png',
title: `Hacktoberfest 2022 - ${titleShort}`
},
'/en/blog/2022/12/the-importance-of-tracking-your-personal-finances': {
featureGraphicPath: 'assets/images/blog/20221226.jpg',
title: `The importance of tracking your personal finances - ${titleShort}`
},
'/en/blog/2023/02/ghostfolio-meets-umbrel': {
featureGraphicPath: 'assets/images/blog/ghostfolio-x-umbrel.png',
title: `Ghostfolio meets Umbrel - ${titleShort}`
},
'/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github': {
featureGraphicPath: 'assets/images/blog/1000-stars-on-github.jpg',
title: `Ghostfolio reaches 1’000 Stars on GitHub - ${titleShort}`
},
'/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio': {
featureGraphicPath: 'assets/images/blog/20230520.jpg',
title: `Unlock your Financial Potential with Ghostfolio - ${titleShort}`
},
'/en/blog/2023/07/exploring-the-path-to-fire': {
featureGraphicPath: 'assets/images/blog/20230701.jpg',
title: `Exploring the Path to FIRE - ${titleShort}`
}
};
const isFileRequest = (filename: string) => {
if (filename === '/assets/LICENSE') {
return true;
} else if (
filename.includes('auth/ey') ||
filename.includes(
'personal-finance-tools/open-source-alternative-to-markets.sh'
)
) {
return false;
}
return filename.split('.').pop() !== filename;
};
export const HtmlTemplateMiddleware = async (
request: Request,
response: Response,
next: NextFunction
) => {
const path = request.originalUrl.replace(/\/$/, '');
let languageCode = path.substr(1, 2);
if (!SUPPORTED_LANGUAGE_CODES.includes(languageCode)) {
languageCode = DEFAULT_LANGUAGE_CODE;
}
const currentDate = format(new Date(), DATE_FORMAT);
const rootUrl = process.env.ROOT_URL || DEFAULT_ROOT_URL;
if (
path.startsWith('/api/') ||
isFileRequest(path) ||
!environment.production
) {
// Skip
next();
} else {
const indexHtml = interpolate(indexHtmlMap[languageCode], {
currentDate,
languageCode,
path,
rootUrl,
description: descriptions[languageCode],
featureGraphicPath:
locales[path]?.featureGraphicPath ?? 'assets/cover.png',
title: locales[path]?.title ?? title
});
return response.send(indexHtml);
}
};

5
apps/api/src/services/configuration/configuration.service.ts

@ -1,4 +1,5 @@
import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface'; import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface';
import { DEFAULT_CURRENCY, DEFAULT_ROOT_URL } from '@ghostfolio/common/config';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { bool, cleanEnv, host, json, num, port, str } from 'envalid'; import { bool, cleanEnv, host, json, num, port, str } from 'envalid';
@ -13,7 +14,7 @@ export class ConfigurationService {
ALPHA_VANTAGE_API_KEY: str({ default: '' }), ALPHA_VANTAGE_API_KEY: str({ default: '' }),
BASE_CURRENCY: str({ BASE_CURRENCY: str({
choices: ['AUD', 'CAD', 'CNY', 'EUR', 'GBP', 'JPY', 'RUB', 'USD'], choices: ['AUD', 'CAD', 'CNY', 'EUR', 'GBP', 'JPY', 'RUB', 'USD'],
default: 'USD' default: DEFAULT_CURRENCY
}), }),
BETTER_UPTIME_API_KEY: str({ default: '' }), BETTER_UPTIME_API_KEY: str({ default: '' }),
CACHE_QUOTES_TTL: num({ default: 1 }), CACHE_QUOTES_TTL: num({ default: 1 }),
@ -46,7 +47,7 @@ export class ConfigurationService {
REDIS_HOST: str({ default: 'localhost' }), REDIS_HOST: str({ default: 'localhost' }),
REDIS_PASSWORD: str({ default: '' }), REDIS_PASSWORD: str({ default: '' }),
REDIS_PORT: port({ default: 6379 }), REDIS_PORT: port({ default: 6379 }),
ROOT_URL: str({ default: 'http://localhost:4200' }), ROOT_URL: str({ default: DEFAULT_ROOT_URL }),
STRIPE_PUBLIC_KEY: str({ default: '' }), STRIPE_PUBLIC_KEY: str({ default: '' }),
STRIPE_SECRET_KEY: str({ default: '' }), STRIPE_SECRET_KEY: str({ default: '' }),
TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }), TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }),

30
apps/api/src/services/data-provider/coingecko/coingecko.service.ts

@ -15,8 +15,8 @@ import {
DataSource, DataSource,
SymbolProfile SymbolProfile
} from '@prisma/client'; } from '@prisma/client';
import bent from 'bent';
import { format, fromUnixTime, getUnixTime } from 'date-fns'; import { format, fromUnixTime, getUnixTime } from 'date-fns';
import got from 'got';
@Injectable() @Injectable()
export class CoinGeckoService implements DataProviderInterface { export class CoinGeckoService implements DataProviderInterface {
@ -45,8 +45,7 @@ export class CoinGeckoService implements DataProviderInterface {
}; };
try { try {
const get = bent(`${this.URL}/coins/${aSymbol}`, 'GET', 'json', 200); const { name } = await got(`${this.URL}/coins/${aSymbol}`).json<any>();
const { name } = await get();
response.name = name; response.name = name;
} catch (error) { } catch (error) {
@ -79,17 +78,13 @@ export class CoinGeckoService implements DataProviderInterface {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}> { }> {
try { try {
const get = bent( const { prices } = await got(
`${ `${
this.URL this.URL
}/coins/${aSymbol}/market_chart/range?vs_currency=${this.baseCurrency.toLowerCase()}&from=${getUnixTime( }/coins/${aSymbol}/market_chart/range?vs_currency=${this.baseCurrency.toLowerCase()}&from=${getUnixTime(
from from
)}&to=${getUnixTime(to)}`, )}&to=${getUnixTime(to)}`
'GET', ).json<any>();
'json',
200
);
const { prices } = await get();
const result: { const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
@ -132,15 +127,11 @@ export class CoinGeckoService implements DataProviderInterface {
} }
try { try {
const get = bent( const response = await got(
`${this.URL}/simple/price?ids=${aSymbols.join( `${this.URL}/simple/price?ids=${aSymbols.join(
',' ','
)}&vs_currencies=${this.baseCurrency.toLowerCase()}`, )}&vs_currencies=${this.baseCurrency.toLowerCase()}`
'GET', ).json<any>();
'json',
200
);
const response = await get();
for (const symbol in response) { for (const symbol in response) {
if (Object.prototype.hasOwnProperty.call(response, symbol)) { if (Object.prototype.hasOwnProperty.call(response, symbol)) {
@ -174,8 +165,9 @@ export class CoinGeckoService implements DataProviderInterface {
let items: LookupItem[] = []; let items: LookupItem[] = [];
try { try {
const get = bent(`${this.URL}/search?query=${query}`, 'GET', 'json', 200); const { coins } = await got(
const { coins } = await get(); `${this.URL}/search?query=${query}`
).json<any>();
items = coins.map(({ id: symbol, name }) => { items = coins.map(({ id: symbol, name }) => {
return { return {

32
apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts

@ -3,9 +3,7 @@ import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client'; import { SymbolProfile } from '@prisma/client';
import bent from 'bent'; import got from 'got';
const getJSON = bent('json');
@Injectable() @Injectable()
export class TrackinsightDataEnhancerService implements DataEnhancerInterface { export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
@ -34,11 +32,13 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
return response; return response;
} }
const profile = await getJSON( const profile = await got(
`${TrackinsightDataEnhancerService.baseUrl}/data-api/funds/${symbol}.json` `${TrackinsightDataEnhancerService.baseUrl}/data-api/funds/${symbol}.json`
).catch(() => { )
return {}; .json<any>()
}); .catch(() => {
return {};
});
const isin = profile.isin?.split(';')?.[0]; const isin = profile.isin?.split(';')?.[0];
@ -46,15 +46,17 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
response.isin = isin; response.isin = isin;
} }
const holdings = await getJSON( const holdings = await got(
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json` `${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`
).catch(() => { )
return getJSON( .json<any>()
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${ .catch(() => {
symbol.split('.')?.[0] return got(
}.json` `${TrackinsightDataEnhancerService.baseUrl}/holdings/${
); symbol.split('.')?.[0]
}); }.json`
);
});
if (holdings?.weight < 0.95) { if (holdings?.weight < 0.95) {
// Skip if data is inaccurate // Skip if data is inaccurate

32
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -10,8 +10,8 @@ import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import bent from 'bent';
import { format, isAfter, isBefore, isSameDay } from 'date-fns'; import { format, isAfter, isBefore, isSameDay } from 'date-fns';
import got from 'got';
@Injectable() @Injectable()
export class FinancialModelingPrepService implements DataProviderInterface { export class FinancialModelingPrepService implements DataProviderInterface {
@ -64,13 +64,9 @@ export class FinancialModelingPrepService implements DataProviderInterface {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}> { }> {
try { try {
const get = bent( const { historical } = await got(
`${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}`, `${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}`
'GET', ).json<any>();
'json',
200
);
const { historical } = await get();
const result: { const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; [symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
@ -115,13 +111,9 @@ export class FinancialModelingPrepService implements DataProviderInterface {
} }
try { try {
const get = bent( const response = await got(
`${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`, `${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`
'GET', ).json<any>();
'json',
200
);
const response = await get();
for (const { price, symbol } of response) { for (const { price, symbol } of response) {
results[symbol] = { results[symbol] = {
@ -153,13 +145,9 @@ export class FinancialModelingPrepService implements DataProviderInterface {
let items: LookupItem[] = []; let items: LookupItem[] = [];
try { try {
const get = bent( const result = await got(
`${this.URL}/search?query=${query}&apikey=${this.apiKey}`, `${this.URL}/search?query=${query}&apikey=${this.apiKey}`
'GET', ).json<any>();
'json',
200
);
const result = await get();
items = result.map(({ currency, name, symbol }) => { items = result.map(({ currency, name, symbol }) => {
return { return {

7
apps/api/src/services/data-provider/manual/manual.service.ts

@ -14,10 +14,10 @@ import {
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import bent from 'bent';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
import { isUUID } from 'class-validator'; import { isUUID } from 'class-validator';
import { addDays, format, isBefore } from 'date-fns'; import { addDays, format, isBefore } from 'date-fns';
import got from 'got';
@Injectable() @Injectable()
export class ManualService implements DataProviderInterface { export class ManualService implements DataProviderInterface {
@ -95,10 +95,9 @@ export class ManualService implements DataProviderInterface {
return {}; return {};
} }
const get = bent(url, 'GET', 'string', 200, headers); const { body } = await got(url, { headers });
const html = await get(); const $ = cheerio.load(body);
const $ = cheerio.load(html);
const value = extractNumberFromString($(selector).text()); const value = extractNumberFromString($(selector).text());

18
apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts

@ -10,8 +10,8 @@ import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import bent from 'bent';
import { format } from 'date-fns'; import { format } from 'date-fns';
import got from 'got';
@Injectable() @Injectable()
export class RapidApiService implements DataProviderInterface { export class RapidApiService implements DataProviderInterface {
@ -135,19 +135,17 @@ export class RapidApiService implements DataProviderInterface {
oneYearAgo: { value: number; valueText: string }; oneYearAgo: { value: number; valueText: string };
}> { }> {
try { try {
const get = bent( const { fgi } = await got(
`https://fear-and-greed-index.p.rapidapi.com/v1/fgi`, `https://fear-and-greed-index.p.rapidapi.com/v1/fgi`,
'GET',
'json',
200,
{ {
useQueryString: true, headers: {
'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com', useQueryString: 'true',
'x-rapidapi-key': this.configurationService.get('RAPID_API_API_KEY') 'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com',
'x-rapidapi-key': this.configurationService.get('RAPID_API_API_KEY')
}
} }
); ).json<any>();
const { fgi } = await get();
return fgi; return fgi;
} catch (error) { } catch (error) {
Logger.error(error, 'RapidApiService'); Logger.error(error, 'RapidApiService');

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

@ -4,25 +4,29 @@ import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strate
import { ModulePreloadService } from './core/module-preload.service'; import { ModulePreloadService } from './core/module-preload.service';
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`
};
const routes: Routes = [ const routes: Routes = [
...[ {
'about', path: paths.about,
/////
'a-propos',
'informazioni-su',
'over',
'sobre',
'ueber-uns'
].map((path) => ({
path,
loadChildren: () => loadChildren: () =>
import('./pages/about/about-page.module').then((m) => m.AboutPageModule) import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
})), },
{ {
path: 'account', path: 'account',
loadChildren: () => loadChildren: () =>
import('./pages/account/account-page.module').then( import('./pages/user-account/user-account-page.module').then(
(m) => m.AccountPageModule (m) => m.UserAccountPageModule
) )
}, },
{ {
@ -42,64 +46,40 @@ const routes: Routes = [
loadChildren: () => loadChildren: () =>
import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule) import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule)
}, },
...['blog'].map((path) => ({ {
path, path: 'blog',
loadChildren: () => loadChildren: () =>
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule) import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
})), },
{ {
path: 'demo', path: 'demo',
loadChildren: () => loadChildren: () =>
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule) import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
}, },
...[ {
'faq', path: paths.faq,
/////
'domande-piu-frequenti',
'foire-aux-questions',
'haeufig-gestellte-fragen',
'perguntas-mais-frequentes',
'preguntas-mas-frecuentes',
'vaak-gestelde-vragen'
].map((path) => ({
path,
loadChildren: () => loadChildren: () =>
import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule) import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule)
})), },
...[ {
'features', path: paths.features,
/////
'fonctionnalites',
'funcionalidades',
'funzionalita',
'kenmerken'
].map((path) => ({
path,
loadChildren: () => loadChildren: () =>
import('./pages/features/features-page.module').then( import('./pages/features/features-page.module').then(
(m) => m.FeaturesPageModule (m) => m.FeaturesPageModule
) )
})), },
{ {
path: 'home', path: 'home',
loadChildren: () => loadChildren: () =>
import('./pages/home/home-page.module').then((m) => m.HomePageModule) import('./pages/home/home-page.module').then((m) => m.HomePageModule)
}, },
...[ {
'markets', path: paths.markets,
/////
'maerkte',
'marches',
'markten',
'mercados',
'mercati'
].map((path) => ({
path,
loadChildren: () => loadChildren: () =>
import('./pages/markets/markets-page.module').then( import('./pages/markets/markets-page.module').then(
(m) => m.MarketsPageModule (m) => m.MarketsPageModule
) )
})), },
{ {
path: 'open', path: 'open',
loadChildren: () => loadChildren: () =>
@ -119,53 +99,27 @@ const routes: Routes = [
(m) => m.PortfolioPageModule (m) => m.PortfolioPageModule
) )
}, },
...[ {
'pricing', path: paths.pricing,
/////
'precios',
'precos',
'preise',
'prezzi',
'prijzen',
'prix'
].map((path) => ({
path,
loadChildren: () => loadChildren: () =>
import('./pages/pricing/pricing-page.module').then( import('./pages/pricing/pricing-page.module').then(
(m) => m.PricingPageModule (m) => m.PricingPageModule
) )
})), },
...[ {
'register', path: paths.register,
/////
'enregistrement',
'iscrizione',
'registo',
'registratie',
'registrierung',
'registro'
].map((path) => ({
path,
loadChildren: () => loadChildren: () =>
import('./pages/register/register-page.module').then( import('./pages/register/register-page.module').then(
(m) => m.RegisterPageModule (m) => m.RegisterPageModule
) )
})), },
...[ {
'resources', path: paths.resources,
/////
'bronnen',
'recursos',
'ressourcen',
'ressources',
'risorse'
].map((path) => ({
path,
loadChildren: () => loadChildren: () =>
import('./pages/resources/resources-page.module').then( import('./pages/resources/resources-page.module').then(
(m) => m.ResourcesPageModule (m) => m.ResourcesPageModule
) )
})), },
{ {
path: 'start', path: 'start',
loadChildren: () => loadChildren: () =>

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

@ -19,7 +19,7 @@
<a <a
*ngIf="canCreateAccount" *ngIf="canCreateAccount"
class="text-center" class="text-center"
[routerLink]="['/register']" [routerLink]="routerLinkRegister"
> >
<div <div
class="cursor-pointer d-inline-block info-message px-3 py-2" class="cursor-pointer d-inline-block info-message px-3 py-2"
@ -43,22 +43,7 @@
<router-outlet></router-outlet> <router-outlet></router-outlet>
</main> </main>
<footer <footer *ngIf="showFooter" class="d-flex justify-content-center py-4 w-100">
*ngIf="
(currentRoute === 'blog' ||
currentRoute === 'faq' ||
currentRoute === 'features' ||
currentRoute === 'markets' ||
currentRoute === 'open' ||
currentRoute === 'p' ||
currentRoute === 'pricing' ||
currentRoute === 'resources' ||
currentRoute === 'register' ||
currentRoute === 'start') &&
deviceType !== 'mobile'
"
class="d-flex justify-content-center py-4 w-100"
>
<div class="container"> <div class="container">
<div class="mb-3 row"> <div class="mb-3 row">
<div class="col-sm"> <div class="col-sm">
@ -68,36 +53,38 @@
<div class="h6 mt-2" i18n>Personal Finance</div> <div class="h6 mt-2" i18n>Personal Finance</div>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li *ngIf="hasPermissionToAccessFearAndGreedIndex"> <li *ngIf="hasPermissionToAccessFearAndGreedIndex">
<a i18n [routerLink]="['/markets']">Markets</a> <a i18n [routerLink]="routerLinkMarkets">Markets</a>
</li> </li>
<li><a i18n [routerLink]="['/resources']">Resources</a></li> <li><a i18n [routerLink]="routerLinkResources">Resources</a></li>
</ul> </ul>
</div> </div>
<div class="col-sm"> <div class="col-sm">
<div class="h6 mt-2">Ghostfolio</div> <div class="h6 mt-2">Ghostfolio</div>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li><a i18n [routerLink]="['/about']">About</a></li> <li><a i18n [routerLink]="routerLinkAbout">About</a></li>
<li *ngIf="hasPermissionForBlog"> <li *ngIf="hasPermissionForBlog">
<a i18n [routerLink]="['/blog']">Blog</a> <a i18n [routerLink]="['/blog']">Blog</a>
</li> </li>
<li> <li>
<a i18n [routerLink]="['/about', 'changelog']">Changelog</a> <a i18n [routerLink]="routerLinkAboutChangelog">Changelog</a>
</li> </li>
<li><a i18n [routerLink]="['/features']">Features</a></li> <li><a i18n [routerLink]="routerLinkFeatures">Features</a></li>
<li *ngIf="hasPermissionForSubscription"> <li *ngIf="hasPermissionForSubscription">
<a i18n [routerLink]="['/faq']">Frequently Asked Questions (FAQ)</a> <a i18n [routerLink]="routerLinkFaq"
>Frequently Asked Questions (FAQ)</a
>
</li> </li>
<li> <li>
<a i18n [routerLink]="['/about', 'license']">License</a> <a i18n [routerLink]="routerLinkAboutLicense">License</a>
</li> </li>
<li *ngIf="hasPermissionForStatistics"> <li *ngIf="hasPermissionForStatistics">
<a [routerLink]="['/open']">Open Startup</a> <a [routerLink]="['/open']">Open Startup</a>
</li> </li>
<li *ngIf="hasPermissionForSubscription"> <li *ngIf="hasPermissionForSubscription">
<a i18n [routerLink]="['/pricing']">Pricing</a> <a i18n [routerLink]="routerLinkPricing">Pricing</a>
</li> </li>
<li *ngIf="hasPermissionForSubscription"> <li *ngIf="hasPermissionForSubscription">
<a i18n [routerLink]="['/about', 'privacy-policy']" <a i18n [routerLink]="routerLinkAboutPrivacyPolicy"
>Privacy Policy</a >Privacy Policy</a
> >
</li> </li>

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

@ -38,6 +38,20 @@ export class AppComponent implements OnDestroy, OnInit {
public hasPermissionToAccessFearAndGreedIndex: boolean; public hasPermissionToAccessFearAndGreedIndex: boolean;
public info: InfoItem; public info: InfoItem;
public pageTitle: string; public pageTitle: string;
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkAboutChangelog = ['/' + $localize`about`, 'changelog'];
public routerLinkAboutLicense = ['/' + $localize`about`, $localize`license`];
public routerLinkAboutPrivacyPolicy = [
'/' + $localize`about`,
$localize`privacy-policy`
];
public routerLinkFaq = ['/' + $localize`faq`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkMarkets = ['/' + $localize`markets`];
public routerLinkPricing = ['/' + $localize`pricing`];
public routerLinkRegister = ['/' + $localize`register`];
public routerLinkResources = ['/' + $localize`resources`];
public showFooter = false;
public user: User; public user: User;
public version = environment.version; public version = environment.version;
@ -89,6 +103,19 @@ export class AppComponent implements OnDestroy, OnInit {
const urlSegments = urlSegmentGroup.segments; const urlSegments = urlSegmentGroup.segments;
this.currentRoute = urlSegments[0].path; this.currentRoute = urlSegments[0].path;
this.showFooter =
(this.currentRoute === 'blog' ||
this.currentRoute === this.routerLinkFaq[0].slice(1) ||
this.currentRoute === this.routerLinkFeatures[0].slice(1) ||
this.currentRoute === this.routerLinkMarkets[0].slice(1) ||
this.currentRoute === 'open' ||
this.currentRoute === 'p' ||
this.currentRoute === this.routerLinkPricing[0].slice(1) ||
this.currentRoute === this.routerLinkRegister[0].slice(1) ||
this.currentRoute === this.routerLinkResources[0].slice(1) ||
this.currentRoute === 'start') &&
this.deviceType !== 'mobile';
if (this.deviceType === 'mobile') { if (this.deviceType === 'mobile') {
setTimeout(() => { setTimeout(() => {
const index = this.title.getTitle().indexOf('–'); const index = this.title.getTitle().indexOf('–');

2
apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts

@ -154,7 +154,7 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
day: string; day: string;
yearMonth: string; yearMonth: string;
}) { }) {
const date = new Date(`${yearMonth}-${day}`); const date = parseISO(`${yearMonth}-${day}`);
const marketPrice = this.marketDataByMonth[yearMonth]?.[day]?.marketPrice; const marketPrice = this.marketDataByMonth[yearMonth]?.[day]?.marketPrice;
if (isSameDay(date, new Date())) { if (isSameDay(date, new Date())) {

5
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -60,6 +60,11 @@ export class AdminMarketDataComponent
}; };
}) })
.concat([ .concat([
{
id: 'CURRENCIES',
label: $localize`Currencies`,
type: <Filter['type']>'PRESET_ID'
},
{ {
id: 'ETF_WITHOUT_COUNTRIES', id: 'ETF_WITHOUT_COUNTRIES',
label: $localize`ETFs without Countries`, label: $localize`ETFs without Countries`,

2
apps/client/src/app/components/admin-overview/admin-overview.html

@ -169,6 +169,8 @@
<mat-option value="7 days">7 Days</mat-option> <mat-option value="7 days">7 Days</mat-option>
<mat-option value="14 days">14 Days</mat-option> <mat-option value="14 days">14 Days</mat-option>
<mat-option value="30 days">30 Days</mat-option> <mat-option value="30 days">30 Days</mat-option>
<mat-option value="90 days">90 Days</mat-option>
<mat-option value="180 days">180 Days</mat-option>
<mat-option value="1 year">1 Year</mat-option> <mat-option value="1 year">1 Year</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>

4
apps/client/src/app/components/admin-settings/admin-settings.component.html

@ -1,14 +1,14 @@
<div class="container"> <div class="container">
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<h3 class="text-center" i18n>Platforms</h3> <h2 class="text-center" i18n>Platforms</h2>
<gf-admin-platform></gf-admin-platform> <gf-admin-platform></gf-admin-platform>
</div> </div>
</div> </div>
<!-- <!--
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3 class="text-center" i18n>Tags</h3> <h2 class="text-center" i18n>Tags</h2>
</div> </div>
</div> </div>
--> -->

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

@ -69,10 +69,10 @@
i18n i18n
mat-flat-button mat-flat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === 'resources', 'font-weight-bold': currentRoute === routeResources,
'text-decoration-underline': currentRoute === 'resources' 'text-decoration-underline': currentRoute === routeResources
}" }"
[routerLink]="['/resources']" [routerLink]="routerLinkResources"
>Resources</a >Resources</a
> >
</li> </li>
@ -87,10 +87,10 @@
i18n i18n
mat-flat-button mat-flat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === 'pricing', 'font-weight-bold': currentRoute === routePricing,
'text-decoration-underline': currentRoute === 'pricing' 'text-decoration-underline': currentRoute === routePricing
}" }"
[routerLink]="['/pricing']" [routerLink]="routerLinkPricing"
>Pricing</a >Pricing</a
> >
</li> </li>
@ -100,10 +100,10 @@
i18n i18n
mat-flat-button mat-flat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === 'about', 'font-weight-bold': currentRoute === routeAbout,
'text-decoration-underline': currentRoute === 'about' 'text-decoration-underline': currentRoute === routeAbout
}" }"
[routerLink]="['/about']" [routerLink]="routerLinkAbout"
>About</a >About</a
> >
</li> </li>
@ -210,9 +210,9 @@
i18n i18n
mat-menu-item mat-menu-item
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === 'resources' 'font-weight-bold': currentRoute === routeResources
}" }"
[routerLink]="['/resources']" [routerLink]="routerLinkResources"
>Resources</a >Resources</a
> >
<a <a
@ -223,16 +223,16 @@
class="d-flex d-sm-none" class="d-flex d-sm-none"
i18n i18n
mat-menu-item mat-menu-item
[ngClass]="{ 'font-weight-bold': currentRoute === 'pricing' }" [ngClass]="{ 'font-weight-bold': currentRoute === routePricing }"
[routerLink]="['/pricing']" [routerLink]="routerLinkPricing"
>Pricing</a >Pricing</a
> >
<a <a
class="d-flex d-sm-none" class="d-flex d-sm-none"
i18n i18n
mat-menu-item mat-menu-item
[ngClass]="{ 'font-weight-bold': currentRoute === 'about' }" [ngClass]="{ 'font-weight-bold': currentRoute === routeAbout }"
[routerLink]="['/about']" [routerLink]="routerLinkAbout"
>About Ghostfolio</a >About Ghostfolio</a
> >
<hr class="d-flex d-sm-none m-0" /> <hr class="d-flex d-sm-none m-0" />
@ -260,10 +260,10 @@
i18n i18n
mat-flat-button mat-flat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === 'features', 'font-weight-bold': currentRoute === routeFeatures,
'text-decoration-underline': currentRoute === 'features' 'text-decoration-underline': currentRoute === routeFeatuers
}" }"
[routerLink]="['/features']" [routerLink]="routerLinkFeatures"
>Features</a >Features</a
> >
</li> </li>
@ -273,10 +273,10 @@
i18n i18n
mat-flat-button mat-flat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === 'about', 'font-weight-bold': currentRoute === routeAbout,
'text-decoration-underline': currentRoute === 'about' 'text-decoration-underline': currentRoute === routeAbout
}" }"
[routerLink]="['/about']" [routerLink]="routerLinkAbout"
>About</a >About</a
> >
</li> </li>
@ -285,10 +285,10 @@
i18n i18n
mat-flat-button mat-flat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === 'pricing', 'font-weight-bold': currentRoute === routePricing,
'text-decoration-underline': currentRoute === 'pricing' 'text-decoration-underline': currentRoute === routePricing
}" }"
[routerLink]="['/pricing']" [routerLink]="routerLinkPricing"
>Pricing</a >Pricing</a
> >
</li> </li>
@ -301,10 +301,10 @@
i18n i18n
mat-flat-button mat-flat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === 'markets', 'font-weight-bold': currentRoute === routeMarkets,
'text-decoration-underline': currentRoute === 'markets' 'text-decoration-underline': currentRoute === routeMarkets
}" }"
[routerLink]="['/markets']" [routerLink]="routerLinkMarkets"
>Markets</a >Markets</a
> >
</li> </li>
@ -329,7 +329,7 @@
class="d-none d-sm-block" class="d-none d-sm-block"
color="primary" color="primary"
mat-flat-button mat-flat-button
[routerLink]="['/register']" [routerLink]="routerLinkRegister"
><ng-container i18n>Get started</ng-container> ><ng-container i18n>Get started</ng-container>
</a> </a>
</li> </li>

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

@ -42,6 +42,17 @@ export class HeaderComponent implements OnChanges {
public hasPermissionToCreateUser: boolean; public hasPermissionToCreateUser: boolean;
public impersonationId: string; public impersonationId: string;
public isMenuOpen: boolean; public isMenuOpen: boolean;
public routeAbout = $localize`about`;
public routeFeatures = $localize`features`;
public routeMarkets = $localize`markets`;
public routePricing = $localize`pricing`;
public routeResources = $localize`resources`;
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkMarkets = ['/' + $localize`markets`];
public routerLinkPricing = ['/' + $localize`pricing`];
public routerLinkRegister = ['/' + $localize`register`];
public routerLinkResources = ['/' + $localize`resources`];
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();

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

@ -1,5 +1,5 @@
<div class="container"> <div class="container">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Markets</h3> <h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Markets</h1>
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col-xs-12 col-md-8 offset-md-2"> <div class="col-xs-12 col-md-8 offset-md-2">
<div class="mb-2 text-center text-muted"> <div class="mb-2 text-center text-muted">

4
apps/client/src/app/components/home-summary/home-summary.component.ts

@ -101,7 +101,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
this.isLoading = true; this.isLoading = true;
this.dataService this.dataService
.fetchPortfolioDetails({}) .fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ summary }) => { .subscribe(({ summary }) => {
this.summary = summary; this.summary = summary;
@ -121,7 +121,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
}); });
this.snackBarRef.onAction().subscribe(() => { this.snackBarRef.onAction().subscribe(() => {
this.router.navigate(['/pricing']); this.router.navigate(['/' + $localize`pricing`]);
}); });
} }

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

@ -1,5 +1,5 @@
<div class="container pb-3 px-3"> <div class="container pb-3 px-3">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Summary</h3> <h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Summary</h1>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-8 offset-md-2"> <div class="col-xs-12 col-md-8 offset-md-2">
<mat-card appearance="outlined"> <mat-card appearance="outlined">

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

@ -11,6 +11,8 @@ import { SubscriptionInterstitialDialogParams } from './interfaces/interfaces';
templateUrl: 'subscription-interstitial-dialog.html' templateUrl: 'subscription-interstitial-dialog.html'
}) })
export class SubscriptionInterstitialDialog { export class SubscriptionInterstitialDialog {
public routerLinkPricing = ['/' + $localize`pricing`];
public constructor( public constructor(
@Inject(MAT_DIALOG_DATA) public data: SubscriptionInterstitialDialogParams, @Inject(MAT_DIALOG_DATA) public data: SubscriptionInterstitialDialogParams,
public dialogRef: MatDialogRef<SubscriptionInterstitialDialog> public dialogRef: MatDialogRef<SubscriptionInterstitialDialog>

2
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html

@ -56,7 +56,7 @@
<a <a
color="primary" color="primary"
mat-flat-button mat-flat-button
[routerLink]="['/pricing']" [routerLink]="routerLinkPricing"
(click)="closeDialog()" (click)="closeDialog()"
> >
<span i18n>Upgrade Plan</span> <span i18n>Upgrade Plan</span>

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

@ -4,6 +4,7 @@ import {
Router, Router,
RouterStateSnapshot RouterStateSnapshot
} from '@angular/router'; } from '@angular/router';
import { paths } from '@ghostfolio/client/app-routing.module';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service'; import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
@ -13,21 +14,17 @@ import { catchError } from 'rxjs/operators';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AuthGuard { export class AuthGuard {
private static PUBLIC_PAGE_ROUTES = [ private static PUBLIC_PAGE_ROUTES = [
'/about', `/${paths.about}`,
'/about/changelog',
'/about/privacy-policy',
'/blog', '/blog',
'/de/blog',
'/demo', '/demo',
'/en/blog', `/${paths.faq}`,
'/faq', `/${paths.features}`,
'/features', `/${paths.markets}`,
'/markets',
'/open', '/open',
'/p', '/p',
'/pricing', `/${paths.pricing}`,
'/register', `/${paths.register}`,
'/resources' `/${paths.resources}`
]; ];
constructor( constructor(
@ -53,7 +50,7 @@ export class AuthGuard {
this.router.navigate(['/demo']); this.router.navigate(['/demo']);
resolve(false); resolve(false);
} else if (utmSource === 'trusted-web-activity') { } else if (utmSource === 'trusted-web-activity') {
this.router.navigate(['/register']); this.router.navigate(['/' + $localize`register`]);
resolve(false); resolve(false);
} else if ( } else if (
AuthGuard.PUBLIC_PAGE_ROUTES.filter((publicPageRoute) => AuthGuard.PUBLIC_PAGE_ROUTES.filter((publicPageRoute) =>

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

@ -77,7 +77,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
}); });
this.snackBarRef.onAction().subscribe(() => { this.snackBarRef.onAction().subscribe(() => {
this.router.navigate(['/pricing']); this.router.navigate(['/' + $localize`pricing`]);
}); });
} }
} else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) { } else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) {

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

@ -3,6 +3,8 @@ import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { AboutPageComponent } from './about-page.component'; import { AboutPageComponent } from './about-page.component';
import { paths } from '@ghostfolio/client/app-routing.module';
import * as path from 'path';
const routes: Routes = [ const routes: Routes = [
{ {
@ -22,38 +24,27 @@ const routes: Routes = [
(m) => m.ChangelogPageModule (m) => m.ChangelogPageModule
) )
}, },
...[ {
'license', path: paths.license,
/////
'licenca',
'licence',
'licencia',
'licentie',
'lizenz',
'licenza'
].map((path) => ({
path,
loadChildren: () => loadChildren: () =>
import('./license/license-page.module').then( import('./license/license-page.module').then(
(m) => m.LicensePageModule (m) => m.LicensePageModule
) )
})), },
...[ {
'privacy-policy', path: 'oss-friends',
///// loadChildren: () =>
'datenschutzbestimmungen', import('./oss-friends/oss-friends-page.module').then(
'informativa-sulla-privacy', (m) => m.OpenSourceSoftwareFriendsPageModule
'politique-de-confidentialite', )
'politica-de-privacidad', },
'politica-de-privacidade', {
'privacybeleid' path: paths.privacyPolicy,
].map((path) => ({
path,
loadChildren: () => loadChildren: () =>
import('./privacy-policy/privacy-policy-page.module').then( import('./privacy-policy/privacy-policy-page.module').then(
(m) => m.PrivacyPolicyPageModule (m) => m.PrivacyPolicyPageModule
) )
})) }
], ],
component: AboutPageComponent, component: AboutPageComponent,
path: '', path: '',

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

@ -44,30 +44,31 @@ export class AboutPageComponent implements OnDestroy, OnInit {
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => { .subscribe((state) => {
this.tabs = [
{
iconName: 'reader-outline',
label: $localize`About`,
path: ['/' + $localize`about`]
},
{
iconName: 'sparkles-outline',
label: $localize`Changelog`,
path: ['/' + $localize`about`, 'changelog']
},
{
iconName: 'ribbon-outline',
label: $localize`License`,
path: ['/' + $localize`about`, $localize`license`]
}
];
if (state?.user) { if (state?.user) {
this.tabs = [ this.tabs.push({
{ iconName: 'shield-checkmark-outline',
iconName: 'reader-outline', label: $localize`Privacy Policy`,
label: $localize`About`, path: ['/' + $localize`about`, $localize`privacy-policy`],
path: ['/about'] showCondition: this.hasPermissionForSubscription
}, });
{
iconName: 'sparkles-outline',
label: $localize`Changelog`,
path: ['/about', 'changelog']
},
{
iconName: 'ribbon-outline',
label: $localize`License`,
path: ['/about', 'license']
},
{
iconName: 'shield-checkmark-outline',
label: $localize`Privacy Policy`,
path: ['/about', 'privacy-policy'],
showCondition: this.hasPermissionForSubscription
}
];
this.user = state.user; this.user = state.user;
this.hasMessage = this.hasMessage =
@ -78,6 +79,12 @@ export class AboutPageComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
this.tabs.push({
iconName: 'happy-outline',
label: 'OSS Friends',
path: ['/' + $localize`about`, 'oss-friends']
});
}); });
} }

2
apps/client/src/app/pages/about/changelog/changelog-page.html

@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Changelog</h1> <h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Changelog</h1>
<div class="changelog"> <div class="changelog">
<markdown [src]="'../assets/CHANGELOG.md'"></markdown> <markdown [src]="'../assets/CHANGELOG.md'"></markdown>
</div> </div>

2
apps/client/src/app/pages/about/license/license-page.html

@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>License</h1> <h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>License</h1>
<div> <div>
<markdown [src]="'../assets/LICENSE'"></markdown> <markdown [src]="'../assets/LICENSE'"></markdown>
</div> </div>

20
apps/client/src/app/pages/about/oss-friends/oss-friends-page-routing.module.ts

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { OpenSourceSoftwareFriendsPageComponent } from './oss-friends-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: OpenSourceSoftwareFriendsPageComponent,
path: '',
title: 'OSS Friends'
}
];
@NgModule({
exports: [RouterModule],
imports: [RouterModule.forChild(routes)]
})
export class OpenSourceSoftwareFriendsPageRoutingModule {}

23
apps/client/src/app/pages/about/oss-friends/oss-friends-page.component.ts

@ -0,0 +1,23 @@
import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
const ossFriends = require('../../../../assets/oss-friends.json');
@Component({
host: { class: 'page' },
selector: 'gf-oss-friends-page',
styleUrls: ['./oss-friends-page.scss'],
templateUrl: './oss-friends-page.html'
})
export class OpenSourceSoftwareFriendsPageComponent implements OnDestroy {
public ossFriends = ossFriends.data;
private unsubscribeSubject = new Subject<void>();
public constructor() {}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

42
apps/client/src/app/pages/about/oss-friends/oss-friends-page.html

@ -0,0 +1,42 @@
<div class="container">
<div class="mb-5 row">
<div class="col">
<h1 class="h3 mb-4 text-center">
<span class="d-none d-sm-block"
><ng-container i18n>Our</ng-container> OSS Friends</span
>
<small class="text-muted" i18n
>Discover other exciting Open Source Software projects</small
>
</h1>
<div class="row">
<div
*ngFor="let ossFriend of ossFriends"
class="col-xs-12 col-md-4 mb-3"
>
<a target="_blank" [href]="ossFriend.href">
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-header>
<mat-card-title class="h4">{{ ossFriend.name }}</mat-card-title>
</mat-card-header>
<mat-card-content class="flex-grow-1">
<p>{{ ossFriend.description }}</p>
</mat-card-content>
<mat-card-actions class="justify-content-end">
<a mat-button target="_blank" [href]="ossFriend.href">
<span
><ng-container i18n>Visit</ng-container> {{ ossFriend.name
}}</span
><ion-icon
class="ml-1"
name="arrow-forward-outline"
></ion-icon>
</a>
</mat-card-actions>
</mat-card>
</a>
</div>
</div>
</div>
</div>
</div>

19
apps/client/src/app/pages/about/oss-friends/oss-friends-page.module.ts

@ -0,0 +1,19 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { OpenSourceSoftwareFriendsPageRoutingModule } from './oss-friends-page-routing.module';
import { OpenSourceSoftwareFriendsPageComponent } from './oss-friends-page.component';
@NgModule({
declarations: [OpenSourceSoftwareFriendsPageComponent],
imports: [
CommonModule,
MatButtonModule,
MatCardModule,
OpenSourceSoftwareFriendsPageRoutingModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class OpenSourceSoftwareFriendsPageModule {}

9
apps/client/src/app/pages/about/oss-friends/oss-friends-page.scss

@ -0,0 +1,9 @@
:host {
display: block;
.mat-mdc-card {
&:hover {
border-color: var(--gf-theme-primary-500);
}
}
}

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

@ -18,6 +18,8 @@ export class AboutOverviewPageComponent implements OnDestroy, OnInit {
public hasPermissionForStatistics: boolean; public hasPermissionForStatistics: boolean;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
public isLoggedIn: boolean; public isLoggedIn: boolean;
public routerLinkFaq = ['/' + $localize`faq`];
public routerLinkFeatures = ['/' + $localize`features`];
public user: User; public user: User;
public version = environment.version; public version = environment.version;

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

@ -1,7 +1,9 @@
<div class="container"> <div class="container">
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<h3 class="d-none d-sm-block mb-3 text-center">About Ghostfolio</h3> <h1 class="d-none d-sm-block h3 mb-4 text-center">
<ng-container i18n>About Ghostfolio</ng-container>
</h1>
<div class="about-container"> <div class="about-container">
<p> <p>
Ghostfolio is a lightweight wealth management application for Ghostfolio is a lightweight wealth management application for
@ -46,7 +48,7 @@
<p> <p>
If you encounter a bug or would like to suggest an improvement or a If you encounter a bug or would like to suggest an improvement or a
new new
<a [routerLink]="['/features']">feature</a>, please join the <a [routerLink]="routerLinkFeatures">feature</a>, please join the
Ghostfolio Ghostfolio
<a <a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg" href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
@ -139,7 +141,7 @@
class="py-4 w-100" class="py-4 w-100"
color="primary" color="primary"
mat-flat-button mat-flat-button
[routerLink]="['/faq']" [routerLink]="routerLinkFaq"
>Frequently Asked Questions (FAQ)</a >Frequently Asked Questions (FAQ)</a
> >
</div> </div>

2
apps/client/src/app/pages/about/privacy-policy/privacy-policy-page.html

@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Privacy Policy</h3> <h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Privacy Policy</h1>
<markdown [src]="'../assets/privacy-policy.md'"></markdown> <markdown [src]="'../assets/privacy-policy.md'"></markdown>
</div> </div>
</div> </div>

2
apps/client/src/app/pages/accounts/accounts-page.html

@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Accounts</h3> <h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Accounts</h1>
<div class="accounts"> <div class="accounts">
<gf-accounts-table <gf-accounts-table
[accounts]="accounts" [accounts]="accounts"

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

@ -9,4 +9,7 @@ import { RouterModule } from '@angular/router';
standalone: true, standalone: true,
templateUrl: './hallo-ghostfolio-page.html' templateUrl: './hallo-ghostfolio-page.html'
}) })
export class HalloGhostfolioPageComponent {} export class HalloGhostfolioPageComponent {
public routerLinkPricing = ['/' + $localize`pricing`];
public routerLinkResources = ['/' + $localize`resources`];
}

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

@ -19,7 +19,7 @@
Aufgrund der steigenden Inflation und den Negativzinsen befasse ich Aufgrund der steigenden Inflation und den Negativzinsen befasse ich
mich seit einiger Zeit, wie ich mein Vermögen möglichst mich seit einiger Zeit, wie ich mein Vermögen möglichst
diversifiziert anlegen kann. Konkret verfolge ich eine diversifiziert anlegen kann. Konkret verfolge ich eine
<a [routerLink]="['/resources']">Buy and Hold Strategie</a> mit <a [routerLink]="routerLinkResources">Buy and Hold Strategie</a> mit
Investitionen in verschiedene Anlageklassen verteilt auf Investitionen in verschiedene Anlageklassen verteilt auf
unterschiedliche Plattformen. Deshalb suchte ich nach einer App, die unterschiedliche Plattformen. Deshalb suchte ich nach einer App, die
mein Portfolio ganzheitlich zusammenfasst. Bei meiner mein Portfolio ganzheitlich zusammenfasst. Bei meiner
@ -119,7 +119,7 @@
Anlagestrategie? Ich freue mich über alle, die Ghostfolio Anlagestrategie? Ich freue mich über alle, die Ghostfolio
ausprobieren. Bist du überzeugt vom Potential der Software? Jede ausprobieren. Bist du überzeugt vom Potential der Software? Jede
Unterstützung für Ghostfolio ist willkommen. Sei es mit einer Unterstützung für Ghostfolio ist willkommen. Sei es mit einer
<a [routerLink]="['/pricing']">Ghostfolio Premium</a> <a [routerLink]="routerLinkPricing">Ghostfolio Premium</a>
Subscription zur Finanzierung des Hostings, einem positiven Rating Subscription zur Finanzierung des Hostings, einem positiven Rating
im im
<a <a

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

@ -9,4 +9,7 @@ import { RouterModule } from '@angular/router';
standalone: true, standalone: true,
templateUrl: './hello-ghostfolio-page.html' templateUrl: './hello-ghostfolio-page.html'
}) })
export class HelloGhostfolioPageComponent {} export class HelloGhostfolioPageComponent {
public routerLinkPricing = ['/' + $localize`pricing`];
public routerLinkResources = ['/' + $localize`resources`];
}

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

@ -19,7 +19,7 @@
Due to rising inflation and negative interest rates, I have been Due to rising inflation and negative interest rates, I have been
looking for some time at how I can invest my assets in the most looking for some time at how I can invest my assets in the most
diversified way possible. Specifically, I follow a diversified way possible. Specifically, I follow a
<a [routerLink]="['/resources']">buy and hold strategy</a> with <a [routerLink]="routerLinkResources">buy and hold strategy</a> with
investments in different asset classes spread across different investments in different asset classes spread across different
platforms. Therefore, I was looking for an app that would platforms. Therefore, I was looking for an app that would
holistically aggregate my portfolio. During my research on the holistically aggregate my portfolio. During my research on the
@ -115,7 +115,7 @@
strategy? I'm happy for everyone who tries Ghostfolio. Are you strategy? I'm happy for everyone who tries Ghostfolio. Are you
convinced of its potential? Any support for Ghostfolio is welcome. convinced of its potential? Any support for Ghostfolio is welcome.
Be it with a Be it with a
<a [routerLink]="['/pricing']">Ghostfolio Premium</a> <a [routerLink]="routerLinkPricing">Ghostfolio Premium</a>
Subscription to finance the hosting, a positive rating in the Subscription to finance the hosting, a positive rating in the
<a <a
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa" href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"

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

@ -9,4 +9,6 @@ import { RouterModule } from '@angular/router';
standalone: true, standalone: true,
templateUrl: './first-months-in-open-source-page.html' templateUrl: './first-months-in-open-source-page.html'
}) })
export class FirstMonthsInOpenSourcePageComponent {} export class FirstMonthsInOpenSourcePageComponent {
public routerLinkPricing = ['/' + $localize`pricing`];
}

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

@ -86,7 +86,7 @@
</p> </p>
<p> <p>
My personal goal is to reach break-even with the Saas offering (<a My personal goal is to reach break-even with the Saas offering (<a
[routerLink]="['/pricing']" [routerLink]="routerLinkPricing"
>Ghostfolio Premium</a >Ghostfolio Premium</a
>) and regularly report about the progress and my learnings on this >) and regularly report about the progress and my learnings on this
exciting journey. exciting journey.

4
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

@ -9,4 +9,6 @@ import { RouterModule } from '@angular/router';
standalone: true, standalone: true,
templateUrl: './how-do-i-get-my-finances-in-order-page.html' templateUrl: './how-do-i-get-my-finances-in-order-page.html'
}) })
export class HowDoIGetMyFinancesInOrderPageComponent {} export class HowDoIGetMyFinancesInOrderPageComponent {
public routerLinkResources = ['/' + $localize`resources`];
}

6
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

@ -9,9 +9,9 @@
<section class="mb-4"> <section class="mb-4">
<p> <p>
Before you can think of Before you can think of
<a [routerLink]="['/resources']">long-term investing</a>, you have <a [routerLink]="routerLinkResources">long-term investing</a>, you
to get your finances in order. Take a look at Peter's journey to see have to get your finances in order. Take a look at Peter's journey
how you can achieve it, too. to see how you can achieve it, too.
</p> </p>
<p> <p>
Peter enjoys life, but sometimes he overspends a bit. He realizes it Peter enjoys life, but sometimes he overspends a bit. He realizes it

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

@ -9,4 +9,7 @@ import { RouterModule } from '@angular/router';
standalone: true, standalone: true,
templateUrl: './500-stars-on-github-page.html' templateUrl: './500-stars-on-github-page.html'
}) })
export class FiveHundredStarsOnGitHubPageComponent {} export class FiveHundredStarsOnGitHubPageComponent {
public routerLinkMarkets = ['/' + $localize`markets`];
public routerLinkPricing = ['/' + $localize`pricing`];
}

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

@ -71,10 +71,10 @@
<h2 class="h4">Break-even Point</h2> <h2 class="h4">Break-even Point</h2>
<p> <p>
Despite the complicated Despite the complicated
<a [routerLink]="['/markets']">economic situation</a> at this time, <a [routerLink]="routerLinkMarkets">economic situation</a> at this
the goal set at the beginning of the year to build a sustainable time, the goal set at the beginning of the year to build a
business and reach break-even with the SaaS offering (<a sustainable business and reach break-even with the SaaS offering (<a
[routerLink]="['/pricing']" [routerLink]="routerLinkPricing"
>Ghostfolio Premium</a >Ghostfolio Premium</a
>) has been achieved. We will continue to leverage the revenue to >) has been achieved. We will continue to leverage the revenue to
further improve the fully managed cloud offering for our paying further improve the fully managed cloud offering for our paying

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

@ -11,5 +11,6 @@ import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
templateUrl: './black-friday-2022-page.html' templateUrl: './black-friday-2022-page.html'
}) })
export class BlackFriday2022PageComponent { export class BlackFriday2022PageComponent {
public constructor() {} public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkPricing = ['/' + $localize`pricing`];
} }

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

@ -35,17 +35,18 @@
software presents the current assets (stocks, ETFs, software presents the current assets (stocks, ETFs,
cryptocurrencies, commodities etc.) in real time to make solid, cryptocurrencies, commodities etc.) in real time to make solid,
data-driven investment decisions. Check out the numerous data-driven investment decisions. Check out the numerous
<a [routerLink]="['/features']">features</a> to manage your wealth. <a [routerLink]="routerLinkFeatures">features</a> to manage your
wealth.
</p> </p>
</section> </section>
<section class="mb-4"> <section class="mb-4">
<p> <p>
Snap the limited Black Friday 2022 deal before it’s gone. For Snap the limited Black Friday 2022 deal before it’s gone. For
detailed information on plans and pricing, please visit our detailed information on plans and pricing, please visit our
<a [routerLink]="['/pricing']">pricing page</a>. <a [routerLink]="routerLinkPricing">pricing page</a>.
</p> </p>
<p class="text-center"> <p class="text-center">
<a color="primary" mat-flat-button [routerLink]="['/pricing']" <a color="primary" mat-flat-button [routerLink]="routerLinkPricing"
>Get the Deal</a >Get the Deal</a
> >
</p> </p>

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

@ -9,4 +9,7 @@ import { RouterModule } from '@angular/router';
standalone: true, standalone: true,
templateUrl: './1000-stars-on-github-page.html' templateUrl: './1000-stars-on-github-page.html'
}) })
export class ThousandStarsOnGitHubPageComponent {} export class ThousandStarsOnGitHubPageComponent {
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkPricing = ['/' + $localize`pricing`];
}

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

@ -92,7 +92,7 @@
<p> <p>
These self-hosting platforms allow users to run applications on These self-hosting platforms allow users to run applications on
their own hardware rather than rely on a their own hardware rather than rely on a
<a [routerLink]="['/pricing']">SaaS offering</a>. As a result, <a [routerLink]="routerLinkPricing">SaaS offering</a>. As a result,
Ghostfolio has become accessible to an even wider range of users who Ghostfolio has become accessible to an even wider range of users who
would like to take control of their wealth management. would like to take control of their wealth management.
</p> </p>
@ -108,9 +108,9 @@
As the project continues to evolve, we can expect to see even more As the project continues to evolve, we can expect to see even more
exciting developments and innovations around Ghostfolio which guides exciting developments and innovations around Ghostfolio which guides
users through the process of users through the process of
<a [routerLink]="['/features']">tracking their assets</a>, such as <a [routerLink]="routerLinkFeatures">tracking their assets</a>, such
stocks, ETFs, or cryptocurrencies. Especially in the areas of data as stocks, ETFs, or cryptocurrencies. Especially in the areas of
import and portfolio analysis. data import and portfolio analysis.
</p> </p>
<p> <p>
We are honored to be a part of this vibrant and growing community, We are honored to be a part of this vibrant and growing community,

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

@ -9,4 +9,7 @@ import { RouterModule } from '@angular/router';
standalone: true, standalone: true,
templateUrl: './unlock-your-financial-potential-with-ghostfolio-page.html' templateUrl: './unlock-your-financial-potential-with-ghostfolio-page.html'
}) })
export class UnlockYourFinancialPotentialWithGhostfolioPageComponent {} export class UnlockYourFinancialPotentialWithGhostfolioPageComponent {
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResources = ['/' + $localize`resources`];
}

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

@ -44,13 +44,13 @@
<h2 class="h4">Empowering Buy & Hold Strategies</h2> <h2 class="h4">Empowering Buy & Hold Strategies</h2>
<p> <p>
For those committed to a For those committed to a
<a [routerLink]="['/resources']">buy & hold strategy</a>, Ghostfolio <a [routerLink]="routerLinkResources">buy & hold strategy</a>,
provides an intuitive interface to monitor long-term investments. Ghostfolio provides an intuitive interface to monitor long-term
Users can track performance over time, gaining insights into investments. Users can track performance over time, gaining insights
portfolio growth and stability. With strong visualizations and into portfolio growth and stability. With strong visualizations and
reporting <a [routerLink]="['/features']">features</a>, Ghostfolio reporting <a [routerLink]="routerLinkFeatures">features</a>,
equips users to make well-informed decisions aligned with their Ghostfolio equips users to make well-informed decisions aligned with
long-term investment goals. their long-term investment goals.
</p> </p>
</section> </section>
<section class="mb-4"> <section class="mb-4">
@ -91,7 +91,7 @@
<h2 class="h4">Driving Financial Independence (FIRE)</h2> <h2 class="h4">Driving Financial Independence (FIRE)</h2>
<p> <p>
Achieving Achieving
<a [routerLink]="['/resources']">financial independence</a> <a [routerLink]="routerLinkResources">financial independence</a>
including early retirement (<a including early retirement (<a
href="../en/blog/2023/07/exploring-the-path-to-fire" href="../en/blog/2023/07/exploring-the-path-to-fire"
>FIRE</a >FIRE</a

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

@ -9,4 +9,6 @@ import { RouterModule } from '@angular/router';
standalone: true, standalone: true,
templateUrl: './exploring-the-path-to-fire-page.html' templateUrl: './exploring-the-path-to-fire-page.html'
}) })
export class ExploringThePathToFirePageComponent {} export class ExploringThePathToFirePageComponent {
public routerLinkFeatures = ['/' + $localize`features`];
}

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

@ -135,10 +135,10 @@
track your investments, and make informed decisions to accelerate track your investments, and make informed decisions to accelerate
your progress towards financial independence. Ghostfolio also your progress towards financial independence. Ghostfolio also
provides a dedicated provides a dedicated
<a [routerLink]="['/features']">FIRE calculator</a>, allowing you to <a [routerLink]="routerLinkFeatures">FIRE calculator</a>, allowing
simulate your customized plan to achieve FIRE. You get the tools to you to simulate your customized plan to achieve FIRE. You get the
optimize your financial journey and confidently strive for a future tools to optimize your financial journey and confidently strive for
that is both personally fulfilling and financially secure. a future that is both personally fulfilling and financially secure.
</p> </p>
</section> </section>
<section class="mb-4 py-3"> <section class="mb-4 py-3">

8
apps/client/src/app/pages/blog/blog-page.html

@ -1,7 +1,13 @@
<div class="container"> <div class="container">
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Blog</h3> <h1 class="h3 mb-4 text-center">
<span class="d-none d-sm-block" i18n>Blog</span>
<small class="text-muted" i18n
>Discover the latest Ghostfolio updates and insights on personal
finance</small
>
</h1>
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-content> <mat-card-content>
<div class="container p-0"> <div class="container p-0">

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

@ -10,6 +10,10 @@ import { Subject, takeUntil } from 'rxjs';
templateUrl: './faq-page.html' templateUrl: './faq-page.html'
}) })
export class FaqPageComponent implements OnDestroy { export class FaqPageComponent implements OnDestroy {
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkMarkets = ['/' + $localize`markets`];
public routerLinkPricing = ['/' + $localize`pricing`];
public routerLinkRegister = ['/' + $localize`register`];
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();

34
apps/client/src/app/pages/faq/faq-page.html

@ -1,9 +1,9 @@
<div class="container"> <div class="container">
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<h3 class="d-none d-sm-block mb-3 text-center"> <h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>
Frequently Asked Questions (FAQ) Frequently Asked Questions (FAQ)
</h3> </h1>
<p> <p>
Find quick answers to commonly asked questions about Ghostfolio in our Find quick answers to commonly asked questions about Ghostfolio in our
Frequently Asked Questions (FAQ) section. Discover what Ghostfolio is, Frequently Asked Questions (FAQ) section. Discover what Ghostfolio is,
@ -39,7 +39,7 @@
> >
<mat-card-content> <mat-card-content>
Please find a feature overview to manage your wealth Please find a feature overview to manage your wealth
<a [routerLink]="['/features']">here</a>. <a [routerLink]="routerLinkFeatures">here</a>.
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
@ -47,7 +47,7 @@
<mat-card-title>How do I start?</mat-card-title></mat-card-header <mat-card-title>How do I start?</mat-card-title></mat-card-header
> >
<mat-card-content> <mat-card-content>
You can sign up via the “<a [routerLink]="['/register']" You can sign up via the “<a [routerLink]="routerLinkRegister"
>Get Started</a >Get Started</a
>” button at the top of the page. You have multiple options to join >” button at the top of the page. You have multiple options to join
Ghostfolio: Create an account with a security token, using Ghostfolio: Create an account with a security token, using
@ -93,7 +93,7 @@
world. The world. The
<a href="https://github.com/ghostfolio/ghostfolio">source code</a> is <a href="https://github.com/ghostfolio/ghostfolio">source code</a> is
fully available as open source software (OSS). Thanks to our generous fully available as open source software (OSS). Thanks to our generous
<a [routerLink]="['/pricing']">Ghostfolio Premium</a> users and <a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> users and
<a href="https://www.buymeacoffee.com/ghostfolio">sponsors</a> we have <a href="https://www.buymeacoffee.com/ghostfolio">sponsors</a> we have
the ability to run a free, limited plan for novice the ability to run a free, limited plan for novice
investors.</mat-card-content investors.</mat-card-content
@ -105,8 +105,8 @@
> >
<mat-card-content <mat-card-content
>Yes, it is! Our >Yes, it is! Our
<a [routerLink]="['/pricing']">pricing page</a> details everything you <a [routerLink]="routerLinkPricing">pricing page</a> details
get for free.</mat-card-content everything you get for free.</mat-card-content
> >
</mat-card> </mat-card>
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
@ -127,7 +127,8 @@
></mat-card-header ></mat-card-header
> >
<mat-card-content <mat-card-content
>By offering <a [routerLink]="['/pricing']">Ghostfolio Premium</a>, a >By offering
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a>, a
subscription plan with a managed hosting service and enhanced subscription plan with a managed hosting service and enhanced
features, we fund our business while providing added value to our features, we fund our business while providing added value to our
users.</mat-card-content users.</mat-card-content
@ -140,12 +141,12 @@
></mat-card-header ></mat-card-header
> >
<mat-card-content <mat-card-content
><a [routerLink]="['/pricing']">Ghostfolio Premium</a> is a fully ><a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> is a fully
managed Ghostfolio cloud offering for ambitious investors. The revenue managed Ghostfolio cloud offering for ambitious investors. The revenue
is used to cover the hosting infrastructure and to fund the ongoing is used to cover the hosting infrastructure and to fund the ongoing
development. It is the Open Source code base with some extras like the development. It is the Open Source code base with some extras like the
<a [routerLink]="['/markets']">markets overview</a> and a professional <a [routerLink]="routerLinkMarkets">markets overview</a> and a
data provider.</mat-card-content professional data provider.</mat-card-content
> >
</mat-card> </mat-card>
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
@ -156,9 +157,9 @@
> >
<mat-card-content <mat-card-content
>Yes, you can try >Yes, you can try
<a [routerLink]="['/pricing']">Ghostfolio Premium</a> by signing up <a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> by signing
for Ghostfolio and applying for a trial (see “My Ghostfolio”). It is up for Ghostfolio and applying for a trial (see “My Ghostfolio”). It
easy, free and there is no commitment. You can stop using it at any is easy, free and there is no commitment. You can stop using it at any
time.</mat-card-content time.</mat-card-content
> >
</mat-card> </mat-card>
@ -214,8 +215,9 @@
</mat-card-header> </mat-card-header>
<mat-card-content <mat-card-content
>Any support for Ghostfolio is welcome. Be it with a >Any support for Ghostfolio is welcome. Be it with a
<a [routerLink]="['/pricing']">Ghostfolio Premium</a> subscription to <a [routerLink]="routerLinkPricing">Ghostfolio Premium</a>
finance the hosting infrastructure, a positive rating in the subscription to finance the hosting infrastructure, a positive rating
in the
<a <a
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa" href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
>Google Play Store</a >Google Play Store</a

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

@ -14,6 +14,7 @@ import { Subject, takeUntil } from 'rxjs';
export class FeaturesPageComponent implements OnDestroy { export class FeaturesPageComponent implements OnDestroy {
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
public info: InfoItem; public info: InfoItem;
public routerLinkRegister = ['/' + $localize`register`];
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();

17
apps/client/src/app/pages/features/features-page.html

@ -1,13 +1,12 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Features</h3> <h1 class="h3 mb-4 text-center">
<div class="mb-4"> <span class="d-none d-sm-block" i18n>Features</span>
<p> <small class="text-muted" i18n>
Check out the numerous features of <strong>Ghostfolio</strong> to Check out the numerous features of Ghostfolio to manage your wealth
manage your wealth. </small>
</p> </h1>
</div>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card appearance="outlined" class="d-flex flex-column h-100"> <mat-card appearance="outlined" class="d-flex flex-column h-100">
@ -212,7 +211,7 @@
</h4> </h4>
<p class="m-0"> <p class="m-0">
Check the current market mood (<a Check the current market mood (<a
[routerLink]="['/resources']" [routerLink]="routerLinkResources"
>Fear & Greed Index</a >Fear & Greed Index</a
>) within the app. >) within the app.
</p> </p>
@ -295,7 +294,7 @@
</div> </div>
<div *ngIf="!user" class="row"> <div *ngIf="!user" class="row">
<div class="col mt-3 text-center"> <div class="col mt-3 text-center">
<a color="primary" i18n mat-flat-button [routerLink]="['/register']" <a color="primary" i18n mat-flat-button [routerLink]="routerLinkRegister"
>Get Started</a >Get Started</a
> >
</div> </div>

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

@ -22,6 +22,8 @@ export class LandingPageComponent implements OnDestroy, OnInit {
public hasPermissionForStatistics: boolean; public hasPermissionForStatistics: boolean;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
public hasPermissionToCreateUser: boolean; public hasPermissionToCreateUser: boolean;
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkRegister = ['/' + $localize`register`];
public statistics: Statistics; public statistics: Statistics;
public testimonials = [ public testimonials = [
{ {

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

@ -1,10 +1,10 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col text-center"> <div class="col text-center">
<h1 class="font-weight-bold intro mt-5"> <h1 class="font-weight-bold intro mt-5" i18n>
Manage your wealth like a boss Manage your wealth like a boss
</h1> </h1>
<p class="lead mb-4"> <p class="lead mb-4" i18n>
Ghostfolio is a privacy-first, open source dashboard for your personal Ghostfolio is a privacy-first, open source dashboard for your personal
finances. Break down your asset allocation, know your net worth and make finances. Break down your asset allocation, know your net worth and make
solid, data-driven investment decisions. solid, data-driven investment decisions.
@ -31,13 +31,20 @@
<div class="button-container mb-5 row"> <div class="button-container mb-5 row">
<div class="align-items-center col d-flex justify-content-center"> <div class="align-items-center col d-flex justify-content-center">
<ng-container *ngIf="hasPermissionToCreateUser"> <ng-container *ngIf="hasPermissionToCreateUser">
<a color="primary" mat-flat-button [routerLink]="['/register']"> <a
color="primary"
i18n
mat-flat-button
[routerLink]="routerLinkRegister"
>
Get Started Get Started
</a> </a>
</ng-container> </ng-container>
<ng-container *ngIf="hasPermissionForDemo"> <ng-container *ngIf="hasPermissionForDemo">
<div *ngIf="hasPermissionToCreateUser" class="mx-3 text-muted">or</div> <div *ngIf="hasPermissionToCreateUser" class="mx-3 text-muted" i18n>
<a mat-stroked-button [routerLink]="['/demo']"> Live Demo </a> or
</div>
<a i18n mat-stroked-button [routerLink]="['/demo']">Live Demo</a>
</ng-container> </ng-container>
</div> </div>
</div> </div>
@ -53,6 +60,7 @@
[routerLink]="['/open']" [routerLink]="['/open']"
> >
<gf-value <gf-value
i18n
icon="people-outline" icon="people-outline"
size="large" size="large"
[value]="statistics?.activeUsers30d ?? '-'" [value]="statistics?.activeUsers30d ?? '-'"
@ -70,6 +78,7 @@
[routerLink]="['/open']" [routerLink]="['/open']"
> >
<gf-value <gf-value
i18n
icon="star-outline" icon="star-outline"
size="large" size="large"
[value]="statistics?.gitHubStargazers ?? '-'" [value]="statistics?.gitHubStargazers ?? '-'"
@ -87,6 +96,7 @@
[routerLink]="['/open']" [routerLink]="['/open']"
> >
<gf-value <gf-value
i18n
icon="cloud-download-outline" icon="cloud-download-outline"
size="large" size="large"
[value]="statistics?.dockerHubPulls ?? '-'" [value]="statistics?.dockerHubPulls ?? '-'"
@ -97,7 +107,9 @@
</div> </div>
<div class="row mb-5"> <div class="row mb-5">
<div class="col-12 text-center text-muted"><small>As seen in</small></div> <div class="col-12 text-center text-muted">
<small i18n>As seen in</small>
</div>
<div class="col-md-3 d-flex justify-content-center my-1"> <div class="col-md-3 d-flex justify-content-center my-1">
<a <a
class="d-block logo logo-alternative-to mask" class="d-block logo logo-alternative-to mask"
@ -190,11 +202,11 @@
<div class="pt-3 row"> <div class="pt-3 row">
<div class="col text-center"> <div class="col text-center">
<h2 class="h4 mb-1 text-center"> <h2 class="h4 mb-1 text-center" i18n>
Protect your <strong>assets</strong>. Refine your Protect your <strong>assets</strong>. Refine your
<strong>personal investment strategy</strong>. <strong>personal investment strategy</strong>.
</h2> </h2>
<p class="lead m-0"> <p class="lead m-0" i18n>
Ghostfolio empowers busy people to keep track of stocks, ETFs or Ghostfolio empowers busy people to keep track of stocks, ETFs or
cryptocurrencies without being tracked. cryptocurrencies without being tracked.
</p> </p>
@ -205,19 +217,20 @@
<div class="col-md-4 my-2"> <div class="col-md-4 my-2">
<mat-card appearance="outlined"> <mat-card appearance="outlined">
<mat-card-header> <mat-card-header>
<mat-card-title>360° View</mat-card-title> <mat-card-title i18n>360° View</mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content i18n>
Get the full picture of your personal finances across multiple Get the full picture of your personal finances across multiple
platforms.
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
<div class="col-md-4 my-2"> <div class="col-md-4 my-2">
<mat-card appearance="outlined"> <mat-card appearance="outlined">
<mat-card-header> <mat-card-header>
<mat-card-title>Web3 Ready</mat-card-title> <mat-card-title i18n>Web3 Ready</mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content i18n>
Use Ghostfolio anonymously and own your financial data. Use Ghostfolio anonymously and own your financial data.
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
@ -225,9 +238,9 @@
<div class="col-md-4 my-2"> <div class="col-md-4 my-2">
<mat-card appearance="outlined"> <mat-card appearance="outlined">
<mat-card-header> <mat-card-header>
<mat-card-title>Open Source</mat-card-title> <mat-card-title i18n>Open Source</mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content i18n>
Benefit from continuous improvements through a strong community. Benefit from continuous improvements through a strong community.
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
@ -236,51 +249,53 @@
<div class="row my-5"> <div class="row my-5">
<div class="col-md-6 offset-md-3"> <div class="col-md-6 offset-md-3">
<h2 class="h4 mb-1 text-center">Why <strong>Ghostfolio</strong>?</h2> <h2 class="h4 mb-1 text-center" i18n>Why <strong>Ghostfolio</strong>?</h2>
<p class="lead mb-3 text-center">Ghostfolio is for you if you are...</p> <p class="lead mb-3 text-center" i18n>
Ghostfolio is for you if you are...
</p>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li class="d-flex mb-3"> <li class="d-flex mb-3">
<span class="mr-3">💼 </span <span class="mr-3">💼 </span
><span ><span i18n
>trading stocks, ETFs or cryptocurrencies on multiple >trading stocks, ETFs or cryptocurrencies on multiple
platforms</span platforms</span
> >
</li> </li>
<li class="d-flex mb-3"> <li class="d-flex mb-3">
<span class="mr-3">🏦</span <span class="mr-3">🏦</span
><span>pursuing a buy & hold strategy</span> ><span i18n>pursuing a buy & hold strategy</span>
</li> </li>
<li class="d-flex mb-3"> <li class="d-flex mb-3">
<span class="mr-3">🎯</span <span class="mr-3">🎯</span
><span ><span i18n
>interested in getting insights of your portfolio composition</span >interested in getting insights of your portfolio composition</span
> >
</li> </li>
<li class="d-flex mb-3"> <li class="d-flex mb-3">
<span class="mr-3">👻</span <span class="mr-3">👻</span
><span>valuing privacy and data ownership</span> ><span i18n>valuing privacy and data ownership</span>
</li> </li>
<li class="d-flex mb-3"> <li class="d-flex mb-3">
<span class="mr-3">🧘</span><span>into minimalism</span> <span class="mr-3">🧘</span><span i18n>into minimalism</span>
</li> </li>
<li class="d-flex mb-3"> <li class="d-flex mb-3">
<span class="mr-3">🧺</span <span class="mr-3">🧺</span
><span>caring about diversifying your financial resources</span> ><span i18n>caring about diversifying your financial resources</span>
</li> </li>
<li class="d-flex mb-3"> <li class="d-flex mb-3">
<span class="mr-3">🆓</span <span class="mr-3">🆓</span
><span>interested in financial independence</span> ><span i18n>interested in financial independence</span>
</li> </li>
<li class="d-flex mb-3"> <li class="d-flex mb-3">
<span class="mr-3">🙅</span <span class="mr-3">🙅</span
><span>saying no to spreadsheets in {{ currentYear }}</span> ><span i18n>saying no to spreadsheets in {{ currentYear }}</span>
</li> </li>
<li class="d-flex mb-3"> <li class="d-flex mb-3">
<span class="mr-3">😎</span><span>still reading this list</span> <span class="mr-3">😎</span><span i18n>still reading this list</span>
</li> </li>
</ul> </ul>
<div class="mt-4 text-center"> <div class="mt-4 text-center">
<a mat-stroked-button [routerLink]="['/about']" <a i18n mat-stroked-button [routerLink]="routerLinkAbout"
>Learn more about Ghostfolio</a >Learn more about Ghostfolio</a
> >
</div> </div>
@ -289,7 +304,7 @@
<div class="row my-5"> <div class="row my-5">
<div class="col-12"> <div class="col-12">
<h2 class="h4 mb-1 text-center"> <h2 class="h4 mb-1 text-center" i18n>
What our <strong>users</strong> are saying What our <strong>users</strong> are saying
</h2> </h2>
</div> </div>
@ -319,7 +334,7 @@
<div *ngIf="hasPermissionForSubscription" class="row my-5"> <div *ngIf="hasPermissionForSubscription" class="row my-5">
<div class="col-12"> <div class="col-12">
<h2 class="h4 text-center"> <h2 class="h4 text-center" i18n>
Members from around the globe are using Members from around the globe are using
<a href="pricing"><strong>Ghostfolio Premium</strong></a> <a href="pricing"><strong>Ghostfolio Premium</strong></a>
</h2> </h2>
@ -334,17 +349,17 @@
<div *ngIf="hasPermissionForSubscription" class="row my-3"> <div *ngIf="hasPermissionForSubscription" class="row my-3">
<div class="col-12"> <div class="col-12">
<h2 class="h4 mb-1 text-center"> <h2 class="h4 mb-1 text-center" i18n>
How does <strong>Ghostfolio</strong> work? How does <strong>Ghostfolio</strong> work?
</h2> </h2>
<p class="lead mb-3 text-center">Get started in only 3 steps</p> <p class="lead mb-3 text-center" i18n>Get started in only 3 steps</p>
</div> </div>
<div class="col-md-4 my-2"> <div class="col-md-4 my-2">
<mat-card appearance="outlined" class="h-100"> <mat-card appearance="outlined" class="h-100">
<mat-card-content class="d-flex flex-row"> <mat-card-content class="d-flex flex-row">
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="font-weight-bold">Sign up anonymously*</div> <div class="font-weight-bold" i18n>Sign up anonymously*</div>
<div class="text-muted"> <div class="text-muted" i18n>
<small>* no e-mail address nor credit card required</small> <small>* no e-mail address nor credit card required</small>
</div> </div>
</div> </div>
@ -356,7 +371,7 @@
<mat-card appearance="outlined" class="h-100"> <mat-card appearance="outlined" class="h-100">
<mat-card-content class="d-flex flex-row"> <mat-card-content class="d-flex flex-row">
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="font-weight-bold"> <div class="font-weight-bold" i18n>
Add any of your historical transactions Add any of your historical transactions
</div> </div>
</div> </div>
@ -368,7 +383,7 @@
<mat-card appearance="outlined" class="h-100"> <mat-card appearance="outlined" class="h-100">
<mat-card-content class="d-flex flex-row"> <mat-card-content class="d-flex flex-row">
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="font-weight-bold"> <div class="font-weight-bold" i18n>
Get valuable insights of your portfolio composition Get valuable insights of your portfolio composition
</div> </div>
</div> </div>
@ -380,19 +395,24 @@
<div *ngIf="hasPermissionToCreateUser" class="row my-5"> <div *ngIf="hasPermissionToCreateUser" class="row my-5">
<div class="col"> <div class="col">
<h2 class="h4 mb-1 text-center">Are <strong>you</strong> ready?</h2> <h2 class="h4 mb-1 text-center" i18n>Are <strong>you</strong> ready?</h2>
<p class="lead mb-3 text-center"> <p class="lead mb-3 text-center" i18n>
Join now<ng-container *ngIf="hasPermissionForDemo"> Join now<ng-container *ngIf="hasPermissionForDemo">
or check out the example account</ng-container or check out the example account</ng-container
> >
</p> </p>
<div class="align-items-center d-flex justify-content-center py-2"> <div class="align-items-center d-flex justify-content-center py-2">
<a color="primary" mat-flat-button [routerLink]="['/register']"> <a
color="primary"
i18n
mat-flat-button
[routerLink]="routerLinkRegister"
>
Get Started Get Started
</a> </a>
<ng-container *ngIf="hasPermissionForDemo"> <ng-container *ngIf="hasPermissionForDemo">
<div class="mx-3 text-muted">or</div> <div class="mx-3 text-muted" i18n>or</div>
<a mat-stroked-button [routerLink]="['/demo']"> Live Demo </a> <a i18n mat-stroked-button [routerLink]="['/demo']">Live Demo</a>
</ng-container> </ng-container>
</div> </div>
</div> </div>

4
apps/client/src/app/pages/open/open-page.html

@ -1,8 +1,8 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3 class="d-none d-sm-block mb-3 text-center">Open Startup</h3> <h1 class="d-none d-sm-block h3 mb-4 text-center">Open Startup</h1>
<div class="intro-container"> <div class="intro-container mb-4">
<p i18n> <p i18n>
At Ghostfolio, transparency is at the core of our values. We publish At Ghostfolio, transparency is at the core of our values. We publish
the source code as the source code as

2
apps/client/src/app/pages/portfolio/activities/activities-page.html

@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<div class="row mb-3"> <div class="row mb-3">
<div class="col"> <div class="col">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Activities</h3> <h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Activities</h1>
<gf-activities-table <gf-activities-table
[activities]="activities" [activities]="activities"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"

10
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -72,7 +72,13 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
public positions: { public positions: {
[symbol: string]: Pick< [symbol: string]: Pick<
PortfolioPosition, PortfolioPosition,
'assetClass' | 'assetSubClass' | 'currency' | 'exchange' | 'name' | 'assetClass'
| 'assetClassLabel'
| 'assetSubClass'
| 'assetSubClassLabel'
| 'currency'
| 'exchange'
| 'name'
> & { etfProvider: string; value: number }; > & { etfProvider: string; value: number };
}; };
public sectors: { public sectors: {
@ -341,7 +347,9 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.positions[symbol] = { this.positions[symbol] = {
value, value,
assetClass: position.assetClass, assetClass: position.assetClass,
assetClassLabel: translate(position.assetClass),
assetSubClass: position.assetSubClass, assetSubClass: position.assetSubClass,
assetSubClassLabel: translate(position.assetSubClass),
currency: position.currency, currency: position.currency,
etfProvider: this.extractEtfProvider({ etfProvider: this.extractEtfProvider({
assetSubClass: position.assetSubClass, assetSubClass: position.assetSubClass,

40
apps/client/src/app/pages/portfolio/allocations/allocations-page.html

@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Allocations</h3> <h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Allocations</h1>
<gf-activities-filter <gf-activities-filter
[allFilters]="allFilters" [allFilters]="allFilters"
[isLoading]="isLoading" [isLoading]="isLoading"
@ -93,7 +93,7 @@
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[colorScheme]="user?.settings?.colorScheme" [colorScheme]="user?.settings?.colorScheme"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[keys]="['assetClass', 'assetSubClass']" [keys]="['assetClassLabel', 'assetSubClassLabel']"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="positions" [positions]="positions"
></gf-portfolio-proportion-chart> ></gf-portfolio-proportion-chart>
@ -263,23 +263,18 @@
<div class="col-md-4"> <div class="col-md-4">
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-header class="overflow-hidden w-100"> <mat-card-header class="overflow-hidden w-100">
<mat-card-title class="align-items-center d-flex text-truncate" <mat-card-title class="text-truncate" i18n>By Account</mat-card-title>
><span i18n>By Country</span
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
></gf-premium-indicator
></mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
cursor="pointer"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[colorScheme]="user?.settings?.colorScheme" [colorScheme]="user?.settings?.colorScheme"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[keys]="['name']" [keys]="['id']"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[maxItems]="10" [positions]="accounts"
[positions]="countries" (proportionChartClicked)="onAccountChartClicked($event)"
></gf-portfolio-proportion-chart> ></gf-portfolio-proportion-chart>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
@ -287,18 +282,22 @@
<div class="col-md-4"> <div class="col-md-4">
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-header class="overflow-hidden w-100"> <mat-card-header class="overflow-hidden w-100">
<mat-card-title class="text-truncate" i18n>By Account</mat-card-title> <mat-card-title class="align-items-center d-flex text-truncate"
><span i18n>By ETF Provider</span
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
></gf-premium-indicator
></mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
cursor="pointer"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[colorScheme]="user?.settings?.colorScheme" [colorScheme]="user?.settings?.colorScheme"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[keys]="['id']" [keys]="['etfProvider']"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="accounts" [positions]="positions"
(proportionChartClicked)="onAccountChartClicked($event)"
></gf-portfolio-proportion-chart> ></gf-portfolio-proportion-chart>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
@ -307,7 +306,7 @@
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-header class="overflow-hidden w-100"> <mat-card-header class="overflow-hidden w-100">
<mat-card-title class="align-items-center d-flex text-truncate" <mat-card-title class="align-items-center d-flex text-truncate"
><span i18n>By ETF Provider</span ><span i18n>By Country</span
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
@ -319,9 +318,10 @@
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[colorScheme]="user?.settings?.colorScheme" [colorScheme]="user?.settings?.colorScheme"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[keys]="['etfProvider']" [keys]="['name']"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="positions" [maxItems]="10"
[positions]="countries"
></gf-portfolio-proportion-chart> ></gf-portfolio-proportion-chart>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>

2
apps/client/src/app/pages/portfolio/analysis/analysis-page.html

@ -1,5 +1,5 @@
<div class="container"> <div class="container">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Analysis</h3> <h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Analysis</h1>
<div *ngIf="user?.settings?.viewMode !== 'ZEN'" class="my-4 text-center"> <div *ngIf="user?.settings?.viewMode !== 'ZEN'" class="my-4 text-center">
<gf-toggle <gf-toggle
[defaultValue]="user?.settings?.dateRange" [defaultValue]="user?.settings?.dateRange"

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

@ -44,7 +44,7 @@ export class FirePageComponent implements OnDestroy, OnInit {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.dataService this.dataService
.fetchPortfolioDetails({}) .fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ summary }) => { .subscribe(({ summary }) => {
if (summary.cash === null || summary.currentValue === null) { if (summary.cash === null || summary.currentValue === null) {

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

@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<div class="row mb-5"> <div class="row mb-5">
<div class="col-lg"> <div class="col-lg">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>FIRE</h3> <h2 class="d-none d-sm-block h3 mb-3 text-center" i18n>FIRE</h2>
<div> <div>
<h4 class="align-items-center d-flex mb-3"> <h4 class="align-items-center d-flex mb-3">
<span i18n>Calculator</span <span i18n>Calculator</span
@ -94,7 +94,7 @@
<div class="container mt-5"> <div class="container mt-5">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3 class="mb-3 text-center">X-ray</h3> <h2 class="h3 mb-3 text-center">X-ray</h2>
<p class="mb-4"> <p class="mb-4">
Ghostfolio X-ray uses static analysis to identify potential issues and Ghostfolio X-ray uses static analysis to identify potential issues and
risks in your portfolio. risks in your portfolio.

6
apps/client/src/app/pages/portfolio/holdings/holdings-page.component.ts

@ -164,7 +164,11 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
for (const [symbol, position] of Object.entries( for (const [symbol, position] of Object.entries(
this.portfolioDetails.holdings this.portfolioDetails.holdings
)) { )) {
this.positionsArray.push(position); this.positionsArray.push({
...position,
assetClassLabel: translate(position.assetClass),
assetSubClassLabel: translate(position.assetSubClass)
});
} }
} }

2
apps/client/src/app/pages/portfolio/holdings/holdings-page.html

@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Holdings</h3> <h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Holdings</h1>
<gf-activities-filter <gf-activities-filter
[allFilters]="allFilters" [allFilters]="allFilters"
[isLoading]="isLoading" [isLoading]="isLoading"

2
apps/client/src/app/pages/pricing/pricing-page.component.ts

@ -29,6 +29,8 @@ export class PricingPageComponent implements OnDestroy, OnInit {
public isLoggedIn: boolean; public isLoggedIn: boolean;
public price: number; public price: number;
public priceId: string; public priceId: string;
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkRegister = ['/' + $localize`register`];
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();

10
apps/client/src/app/pages/pricing/pricing-page.html

@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Pricing Plans</h3> <h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Pricing Plans</h1>
<div class="mb-4"> <div class="mb-4">
<p i18n> <p i18n>
Our official Ghostfolio Premium cloud offering is the easiest way to Our official Ghostfolio Premium cloud offering is the easiest way to
@ -21,7 +21,7 @@
<a href="mailto:hi@ghostfol.io?Subject=Student Discount">here</a> with <a href="mailto:hi@ghostfol.io?Subject=Student Discount">here</a> with
your university e-mail address. your university e-mail address.
</p> </p>
<p> <p i18n>
If you prefer to run Ghostfolio on your own infrastructure, please If you prefer to run Ghostfolio on your own infrastructure, please
find the source code and further instructions on find the source code and further instructions on
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>. <a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
@ -106,7 +106,7 @@
class="mr-1" class="mr-1"
name="checkmark-circle-outline" name="checkmark-circle-outline"
></ion-icon> ></ion-icon>
<a i18n [routerLink]="['/features']" <a i18n [routerLink]="routerLinkFeatures"
>and more Features...</a >and more Features...</a
> >
</li> </li>
@ -302,7 +302,7 @@
class="mr-1" class="mr-1"
name="checkmark-circle-outline" name="checkmark-circle-outline"
></ion-icon> ></ion-icon>
<a i18n [routerLink]="['/features']" <a i18n [routerLink]="routerLinkFeatures"
>and more Features...</a >and more Features...</a
> >
</li> </li>
@ -360,7 +360,7 @@
</div> </div>
<div *ngIf="!user" class="row"> <div *ngIf="!user" class="row">
<div class="col mt-3 text-center"> <div class="col mt-3 text-center">
<a color="primary" i18n mat-flat-button [routerLink]="['/register']"> <a color="primary" i18n mat-flat-button [routerLink]="routerLinkRegister">
Get Started Get Started
</a> </a>
<p class="m-0 text-muted"><small i18n>It’s free.</small></p> <p class="m-0 text-muted"><small i18n>It’s free.</small></p>

4
apps/client/src/app/pages/public/public-page.html

@ -1,10 +1,10 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3 class="h4 mb-3 text-center" i18n> <h1 class="h4 mb-3 text-center" i18n>
Hello, {{ portfolioPublicDetails?.alias ?? 'someone' }} has shared a Hello, {{ portfolioPublicDetails?.alias ?? 'someone' }} has shared a
<strong>Portfolio</strong> with you! <strong>Portfolio</strong> with you!
</h3> </h1>
</div> </div>
</div> </div>
<div class="proportion-charts row"> <div class="proportion-charts row">

2
apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page-routing.module.ts

@ -22,7 +22,7 @@ const routes: Routes = [
path: `open-source-alternative-to-${key}`, path: `open-source-alternative-to-${key}`,
loadComponent: () => loadComponent: () =>
import(`./products/${key}-page.component`).then(() => component), import(`./products/${key}-page.component`).then(() => component),
title: `Open Source Alternative to ${name}` title: $localize`Open Source Alternative to ${name}`
}; };
}) })
]; ];

2
apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.component.ts

@ -10,9 +10,11 @@ import { products } from './products';
templateUrl: './personal-finance-tools-page.html' templateUrl: './personal-finance-tools-page.html'
}) })
export class PersonalFinanceToolsPageComponent implements OnDestroy { export class PersonalFinanceToolsPageComponent implements OnDestroy {
public pathResources = '/' + $localize`resources`;
public products = products.filter(({ key }) => { public products = products.filter(({ key }) => {
return key !== 'ghostfolio'; return key !== 'ghostfolio';
}); });
public routerLinkAbout = ['/' + $localize`about`];
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();

14
apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html

@ -1,19 +1,19 @@
<div class="container"> <div class="container">
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<h3 class="d-none d-sm-block mb-3 text-center" i18n> <h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>
Discover Open Source Alternatives for Personal Finance Tools Discover Open Source Alternatives for Personal Finance Tools
</h3> </h1>
<div class="introduction mb-4"> <div class="introduction mb-4">
<p> <p i18n>
This overview page features a curated collection of personal finance This overview page features a curated collection of personal finance
tools compared to the open source alternative tools compared to the open source alternative
<a [routerLink]="['/about']">Ghostfolio</a>. If you value <a [routerLink]="routerLinkAbout">Ghostfolio</a>. If you value
transparency, data privacy, and community collaboration, Ghostfolio transparency, data privacy, and community collaboration, Ghostfolio
provides an excellent opportunity to take control of your financial provides an excellent opportunity to take control of your financial
management. management.
</p> </p>
<p> <p i18n>
Explore the links below to compare a variety of personal finance tools Explore the links below to compare a variety of personal finance tools
with Ghostfolio. with Ghostfolio.
</p> </p>
@ -29,10 +29,10 @@
<a <a
class="d-flex overflow-hidden w-100" class="d-flex overflow-hidden w-100"
title="Compare Ghostfolio to {{ product.name }}" title="Compare Ghostfolio to {{ product.name }}"
[routerLink]="['/resources', 'personal-finance-tools', 'open-source-alternative-to-' + product.key]" [routerLink]="[pathResources, 'personal-finance-tools', 'open-source-alternative-to-' + product.key]"
> >
<div class="flex-grow-1 overflow-hidden"> <div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate"> <div class="h6 m-0 text-truncate" i18n>
Open Source Alternative to {{ product.name }} Open Source Alternative to {{ product.name }}
</div> </div>
</div> </div>

13
apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html

@ -4,19 +4,22 @@
<article> <article>
<div class="mb-4 text-center"> <div class="mb-4 text-center">
<h1 class="mb-1"> <h1 class="mb-1">
<strong>Ghostfolio</strong>: The Open Source Alternative to <strong>Ghostfolio</strong>:
<strong>{{ product2.name }}</strong> <ng-container i18n>The Open Source Alternative to</ng-container
>&nbsp;<strong>{{ product2.name }}</strong>
</h1> </h1>
</div> </div>
<section class="mb-4"> <section class="mb-4">
<p> <p>
Are you looking for an open source alternative to {{ product2.name Are you looking for an open source alternative to {{ product2.name
}}? <a [routerLink]="['/about']">Ghostfolio</a> is a powerful }}? <a [routerLink]="routerLinkAbout">Ghostfolio</a> is a powerful
portfolio management tool that provides individuals with a portfolio management tool that provides individuals with a
comprehensive platform to track, analyze, and optimize their comprehensive platform to track, analyze, and optimize their
investments. Whether you are an experienced investor or just investments. Whether you are an experienced investor or just
starting out, Ghostfolio offers an intuitive user interface and a starting out, Ghostfolio offers an intuitive user interface and a
<a [routerLink]="['/features']">wide range of functionalities</a> <a [routerLink]="routerLinkFeatures"
>wide range of functionalities</a
>
to help you make informed decisions and take control of your to help you make informed decisions and take control of your
financial future. financial future.
</p> </p>
@ -280,7 +283,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a i18n [routerLink]="['/resources', 'personal-finance-tools']" <a i18n [routerLink]="routerLinkResourcesPersonalFinanceTools"
>Personal Finance Tools</a >Personal Finance Tools</a
> >
</li> </li>

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

@ -21,4 +21,11 @@ export class AltooPageComponent {
public product2 = products.find(({ key }) => { public product2 = products.find(({ key }) => {
return key === 'altoo'; return key === 'altoo';
}); });
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
} }

7
apps/client/src/app/pages/resources/personal-finance-tools/products/copilot-money-page.component.ts

@ -21,4 +21,11 @@ export class CopilotMoneyPageComponent {
public product2 = products.find(({ key }) => { public product2 = products.find(({ key }) => {
return key === 'copilot-money'; return key === 'copilot-money';
}); });
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
} }

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

@ -21,4 +21,11 @@ export class DeltaPageComponent {
public product2 = products.find(({ key }) => { public product2 = products.find(({ key }) => {
return key === 'delta'; return key === 'delta';
}); });
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
} }

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

@ -21,4 +21,11 @@ export class DivvyDiaryPageComponent {
public product2 = products.find(({ key }) => { public product2 = products.find(({ key }) => {
return key === 'divvydiary'; return key === 'divvydiary';
}); });
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
} }

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

@ -21,4 +21,11 @@ export class ExirioPageComponent {
public product2 = products.find(({ key }) => { public product2 = products.find(({ key }) => {
return key === 'exirio'; return key === 'exirio';
}); });
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
} }

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

@ -21,4 +21,11 @@ export class FolisharePageComponent {
public product2 = products.find(({ key }) => { public product2 = products.find(({ key }) => {
return key === 'folishare'; return key === 'folishare';
}); });
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
} }

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

@ -21,4 +21,11 @@ export class GetquinPageComponent {
public product2 = products.find(({ key }) => { public product2 = products.find(({ key }) => {
return key === 'getquin'; return key === 'getquin';
}); });
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
} }

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

@ -21,4 +21,11 @@ export class GoSpatzPageComponent {
public product2 = products.find(({ key }) => { public product2 = products.find(({ key }) => {
return key === 'gospatz'; return key === 'gospatz';
}); });
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
} }

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

@ -21,4 +21,11 @@ export class JustEtfPageComponent {
public product2 = products.find(({ key }) => { public product2 = products.find(({ key }) => {
return key === 'justetf'; return key === 'justetf';
}); });
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
} }

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

@ -21,4 +21,11 @@ export class KuberaPageComponent {
public product2 = products.find(({ key }) => { public product2 = products.find(({ key }) => {
return key === 'kubera'; return key === 'kubera';
}); });
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
} }

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

Loading…
Cancel
Save