Browse Source

Merge remote-tracking branch 'upstream/main'

pull/1134/head
Anders Sparrevohn 3 years ago
parent
commit
1dfe42b5db
  1. 64
      CHANGELOG.md
  2. 2
      Dockerfile
  3. 18
      README.md
  4. 50
      angular.json
  5. 11
      apps/api/src/app/app.module.ts
  6. 13
      apps/api/src/app/auth/auth.controller.ts
  7. 6
      apps/api/src/app/benchmark/benchmark.service.ts
  8. 81
      apps/api/src/app/frontend.middleware.ts
  9. 7
      apps/api/src/app/order/order.service.ts
  10. 11
      apps/api/src/app/subscription/subscription.controller.ts
  11. 5
      apps/api/src/app/subscription/subscription.service.ts
  12. 4
      apps/api/src/app/user/update-user-setting.dto.ts
  13. 13
      apps/api/src/services/symbol-profile.service.ts
  14. 27
      apps/client/src/app/app-routing.module.ts
  15. 4
      apps/client/src/app/app.component.html
  16. 10
      apps/client/src/app/components/access-table/access-table.component.html
  17. 4
      apps/client/src/app/components/access-table/access-table.component.ts
  18. 12
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  19. 27
      apps/client/src/app/components/accounts-table/accounts-table.component.html
  20. 4
      apps/client/src/app/components/accounts-table/accounts-table.component.ts
  21. 13
      apps/client/src/app/components/admin-jobs/admin-jobs.html
  22. 4
      apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html
  23. 9
      apps/client/src/app/components/admin-market-data/admin-market-data.html
  24. 16
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  25. 4
      apps/client/src/app/components/admin-overview/admin-overview.html
  26. 4
      apps/client/src/app/components/admin-users/admin-users.component.ts
  27. 16
      apps/client/src/app/components/admin-users/admin-users.html
  28. 7
      apps/client/src/app/components/header/header.component.html
  29. 4
      apps/client/src/app/components/header/header.component.ts
  30. 4
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  31. 1
      apps/client/src/app/components/home-market/home-market.html
  32. 4
      apps/client/src/app/components/home-overview/home-overview.component.ts
  33. 2
      apps/client/src/app/components/home-overview/home-overview.scss
  34. 2
      apps/client/src/app/components/investment-chart/investment-chart.component.ts
  35. 9
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html
  36. 2
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts
  37. 70
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html
  38. 20
      apps/client/src/app/components/positions-table/positions-table.component.html
  39. 8
      apps/client/src/app/components/toggle/toggle.component.ts
  40. 8
      apps/client/src/app/core/auth.guard.ts
  41. 16
      apps/client/src/app/core/http-response.interceptor.ts
  42. 2
      apps/client/src/app/pages/about/about-page-routing.module.ts
  43. 40
      apps/client/src/app/pages/about/about-page.html
  44. 2
      apps/client/src/app/pages/about/changelog/changelog-page-routing.module.ts
  45. 4
      apps/client/src/app/pages/about/changelog/changelog-page.html
  46. 2
      apps/client/src/app/pages/about/privacy-policy/privacy-policy-page-routing.module.ts
  47. 2
      apps/client/src/app/pages/about/privacy-policy/privacy-policy-page.html
  48. 2
      apps/client/src/app/pages/account/account-page-routing.module.ts
  49. 25
      apps/client/src/app/pages/account/account-page.component.ts
  50. 34
      apps/client/src/app/pages/account/account-page.html
  51. 3
      apps/client/src/app/pages/account/create-or-update-access-dialog/create-or-update-access-dialog.html
  52. 2
      apps/client/src/app/pages/accounts/accounts-page-routing.module.ts
  53. 3
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html
  54. 2
      apps/client/src/app/pages/admin/admin-page-routing.module.ts
  55. 1
      apps/client/src/app/pages/auth/auth-page.component.ts
  56. 2
      apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.html
  57. 2
      apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.html
  58. 4
      apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.html
  59. 4
      apps/client/src/app/pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.html
  60. 20
      apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page-routing.module.ts
  61. 9
      apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.component.ts
  62. 195
      apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.html
  63. 13
      apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.module.ts
  64. 3
      apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.scss
  65. 2
      apps/client/src/app/pages/blog/blog-page-routing.module.ts
  66. 34
      apps/client/src/app/pages/blog/blog-page.html
  67. 2
      apps/client/src/app/pages/demo/demo-page.component.ts
  68. 2
      apps/client/src/app/pages/faq/faq-page-routing.module.ts
  69. 58
      apps/client/src/app/pages/faq/faq-page.html
  70. 2
      apps/client/src/app/pages/features/features-page-routing.module.ts
  71. 40
      apps/client/src/app/pages/features/features-page.html
  72. 2
      apps/client/src/app/pages/home/home-page-routing.module.ts
  73. 29
      apps/client/src/app/pages/landing/landing-page.html
  74. 2
      apps/client/src/app/pages/markets/markets-page-routing.module.ts
  75. 2
      apps/client/src/app/pages/portfolio/allocations/allocations-page-routing.module.ts
  76. 4
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  77. 19
      apps/client/src/app/pages/portfolio/allocations/allocations-page.html
  78. 2
      apps/client/src/app/pages/portfolio/analysis/analysis-page-routing.module.ts
  79. 4
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  80. 4
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  81. 2
      apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts
  82. 2
      apps/client/src/app/pages/portfolio/holdings/holdings-page-routing.module.ts
  83. 2
      apps/client/src/app/pages/portfolio/portfolio-page.html
  84. 10
      apps/client/src/app/pages/portfolio/report/report-page.html
  85. 5
      apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html
  86. 2
      apps/client/src/app/pages/portfolio/transactions/transactions-page-routing.module.ts
  87. 4
      apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts
  88. 2
      apps/client/src/app/pages/pricing/pricing-page-routing.module.ts
  89. 14
      apps/client/src/app/pages/pricing/pricing-page.html
  90. 2
      apps/client/src/app/pages/public/public-page-routing.module.ts
  91. 19
      apps/client/src/app/pages/public/public-page.html
  92. 2
      apps/client/src/app/pages/register/register-page-routing.module.ts
  93. 9
      apps/client/src/app/pages/register/register-page.html
  94. 2
      apps/client/src/app/pages/resources/resources-page-routing.module.ts
  95. 3
      apps/client/src/app/pages/resources/resources-page.html
  96. 2
      apps/client/src/app/pages/webauthn/webauthn-page-routing.module.ts
  97. 13
      apps/client/src/app/pages/webauthn/webauthn-page.html
  98. 2
      apps/client/src/app/pages/zen/zen-page-routing.module.ts
  99. BIN
      apps/client/src/assets/images/blog/500-stars-on-github.jpg
  100. 4
      apps/client/src/assets/robots.txt

64
CHANGELOG.md

@ -9,7 +9,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the language localization for German (`de`)
## 1.181.2 - 21.08.2022
### Added
- Added a language selector to the account page
- Added support for translated labels in the value component
### Changed
- Integrated the commands `database:setup` and `database:migrate` into the container start
### Fixed
- Fixed a division by zero error in the benchmarks calculation
### Todo
- Apply manual data migration (`yarn database:migrate`) is not needed anymore
## 1.180.1 - 18.08.2022
### Added
- Set up `ng-extract-i18n-merge` to improve the i18n extraction and merge workflow
- Set up language localization for German (`de`)
- Resolved the feature graphic of the blog post
### Changed
- Tagged template literal strings in components for localization with `$localize`
### Fixed
- Fixed the license component in the about page
- Fixed the links to the blog posts
## 1.179.5 - 15.08.2022
### Added
- Set up i18n support
- Added a blog post: _500 Stars on GitHub_
### Changed
- Reduced the maximum width of the performance chart on the home page
## 1.178.0 - 09.08.2022
### Added
- Added `url` to the symbol profile overrides model for manual adjustments
- Added default values for `countries` and `sectors` of the symbol profile overrides model
### Changed
- Simplified the initialization of the exchange rate service - Simplified the initialization of the exchange rate service
- Improved the orders query for `assetClass` with symbol profile overrides
- Improved the styling of the benchmarks in the markets overview
### Todo
- Apply data migration (`yarn database:migrate`)
## 1.177.0 - 04.08.2022 ## 1.177.0 - 04.08.2022

2
Dockerfile

@ -58,4 +58,4 @@ RUN apt update && apt install -y \
COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps
WORKDIR /ghostfolio/apps/api WORKDIR /ghostfolio/apps/api
EXPOSE 3333 EXPOSE 3333
CMD [ "node", "main" ] CMD [ "yarn", "start:prod" ]

18
README.md

@ -114,14 +114,6 @@ Run the following command to start the Docker images from [Docker Hub](https://h
docker-compose --env-file ./.env -f docker/docker-compose.yml up -d docker-compose --env-file ./.env -f docker/docker-compose.yml up -d
``` ```
##### Setup Database
Run the following command to setup the database once Ghostfolio is running:
```bash
docker-compose --env-file ./.env -f docker/docker-compose.yml exec ghostfolio yarn database:setup
```
#### b. Build and run environment #### b. Build and run environment
Run the following commands to build and start the Docker images: Run the following commands to build and start the Docker images:
@ -131,14 +123,6 @@ docker-compose --env-file ./.env -f docker/docker-compose.build.yml build
docker-compose --env-file ./.env -f docker/docker-compose.build.yml up -d docker-compose --env-file ./.env -f docker/docker-compose.build.yml up -d
``` ```
##### Setup Database
Run the following command to setup the database once Ghostfolio is running:
```bash
docker-compose --env-file ./.env -f docker/docker-compose.build.yml exec ghostfolio yarn database:setup
```
#### Fetch Historical Data #### Fetch Historical Data
Open http://localhost:3333 in your browser and accomplish these steps: Open http://localhost:3333 in your browser and accomplish these steps:
@ -151,7 +135,7 @@ Open http://localhost:3333 in your browser and accomplish these steps:
1. Increase the version of the `ghostfolio/ghostfolio` Docker image in `docker/docker-compose.yml` 1. Increase the version of the `ghostfolio/ghostfolio` Docker image in `docker/docker-compose.yml`
1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d` 1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d`
1. Then, run the following command to keep your database schema in sync: `docker-compose --env-file ./.env -f docker/docker-compose.yml exec ghostfolio yarn database:migrate` At each start, the container will automatically apply the database schema migrations if needed.
### Run with _Unraid_ (Community) ### Run with _Unraid_ (Community)

50
angular.json

@ -77,41 +77,45 @@
"polyfills": "apps/client/src/polyfills.ts", "polyfills": "apps/client/src/polyfills.ts",
"tsConfig": "apps/client/tsconfig.app.json", "tsConfig": "apps/client/tsconfig.app.json",
"assets": [ "assets": [
"apps/client/src/assets",
{ {
"glob": "assetlinks.json", "glob": "assetlinks.json",
"input": "apps/client/src/assets", "input": "apps/client/src/assets",
"output": "./.well-known" "output": "./../.well-known"
}, },
{ {
"glob": "CHANGELOG.md", "glob": "CHANGELOG.md",
"input": "", "input": "",
"output": "./assets" "output": "./../assets"
}, },
{ {
"glob": "LICENSE", "glob": "LICENSE",
"input": "", "input": "",
"output": "./assets" "output": "./../assets"
}, },
{ {
"glob": "robots.txt", "glob": "robots.txt",
"input": "apps/client/src/assets", "input": "apps/client/src/assets",
"output": "./" "output": "./../"
}, },
{ {
"glob": "sitemap.xml", "glob": "sitemap.xml",
"input": "apps/client/src/assets", "input": "apps/client/src/assets",
"output": "./" "output": "./../"
}, },
{ {
"glob": "**/*", "glob": "**/*",
"input": "node_modules/ionicons/dist/ionicons", "input": "node_modules/ionicons/dist/ionicons",
"output": "./ionicons" "output": "./../ionicons"
}, },
{ {
"glob": "**/*.js", "glob": "**/*.js",
"input": "node_modules/ionicons/dist/", "input": "node_modules/ionicons/dist/",
"output": "./" "output": "./../"
},
{
"glob": "**/*",
"input": "apps/client/src/assets",
"output": "./../assets/"
} }
], ],
"styles": ["apps/client/src/styles.scss"], "styles": ["apps/client/src/styles.scss"],
@ -124,6 +128,14 @@
"namedChunks": true "namedChunks": true
}, },
"configurations": { "configurations": {
"development-de": {
"baseHref": "/de/",
"localize": ["de"]
},
"development-en": {
"baseHref": "/en/",
"localize": ["en"]
},
"production": { "production": {
"fileReplacements": [ "fileReplacements": [
{ {
@ -162,15 +174,24 @@
"proxyConfig": "apps/client/proxy.conf.json" "proxyConfig": "apps/client/proxy.conf.json"
}, },
"configurations": { "configurations": {
"development-de": {
"browserTarget": "client:build:development-de"
},
"development-en": {
"browserTarget": "client:build:development-en"
},
"production": { "production": {
"browserTarget": "client:build:production" "browserTarget": "client:build:production"
} }
} }
}, },
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "ng-extract-i18n-merge:ng-extract-i18n-merge",
"options": { "options": {
"browserTarget": "client:build" "browserTarget": "client:build",
"includeContext": true,
"outputPath": "src/locales",
"targetFiles": ["messages.de.xlf"]
} }
}, },
"lint": { "lint": {
@ -188,6 +209,15 @@
"outputs": ["coverage/apps/client"] "outputs": ["coverage/apps/client"]
} }
}, },
"i18n": {
"locales": {
"de": {
"baseHref": "/de/",
"translation": "apps/client/src/locales/messages.de.xlf"
}
},
"sourceLocale": "en"
},
"tags": [] "tags": []
}, },
"client-e2e": { "client-e2e": {

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

@ -10,7 +10,7 @@ import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-d
import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module'; import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module';
import { BullModule } from '@nestjs/bull'; import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common'; import { MiddlewareConsumer, Module, RequestMethod } 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';
@ -23,6 +23,7 @@ import { AuthModule } from './auth/auth.module';
import { BenchmarkModule } from './benchmark/benchmark.module'; import { BenchmarkModule } from './benchmark/benchmark.module';
import { CacheModule } from './cache/cache.module'; import { CacheModule } from './cache/cache.module';
import { ExportModule } from './export/export.module'; import { ExportModule } from './export/export.module';
import { FrontendMiddleware } from './frontend.middleware';
import { ImportModule } from './import/import.module'; import { ImportModule } from './import/import.module';
import { InfoModule } from './info/info.module'; import { InfoModule } from './info/info.module';
import { OrderModule } from './order/order.module'; import { OrderModule } from './order/order.module';
@ -82,4 +83,10 @@ 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 });
}
}

13
apps/api/src/app/auth/auth.controller.ts

@ -1,5 +1,6 @@
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service'; import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { OAuthResponse } from '@ghostfolio/common/interfaces'; import { OAuthResponse } from '@ghostfolio/common/interfaces';
import { import {
Body, Body,
@ -62,9 +63,17 @@ export class AuthController {
const jwt: string = req.user.jwt; const jwt: string = req.user.jwt;
if (jwt) { if (jwt) {
res.redirect(`${this.configurationService.get('ROOT_URL')}/auth/${jwt}`); res.redirect(
`${this.configurationService.get(
'ROOT_URL'
)}/${DEFAULT_LANGUAGE_CODE}/auth/${jwt}`
);
} else { } else {
res.redirect(`${this.configurationService.get('ROOT_URL')}/auth`); res.redirect(
`${this.configurationService.get(
'ROOT_URL'
)}/${DEFAULT_LANGUAGE_CODE}/auth`
);
} }
} }

6
apps/api/src/app/benchmark/benchmark.service.ts

@ -48,9 +48,13 @@ export class BenchmarkService {
benchmarks = allTimeHighs.map((allTimeHigh, index) => { benchmarks = allTimeHighs.map((allTimeHigh, index) => {
const { marketPrice } = quotes[benchmarkAssets[index].symbol]; const { marketPrice } = quotes[benchmarkAssets[index].symbol];
const performancePercentFromAllTimeHigh = new Big(marketPrice) let performancePercentFromAllTimeHigh = new Big(0);
if (allTimeHigh) {
performancePercentFromAllTimeHigh = new Big(marketPrice)
.div(allTimeHigh) .div(allTimeHigh)
.minus(1); .minus(1);
}
return { return {
marketCondition: this.getMarketCondition( marketCondition: this.getMarketCondition(

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

@ -0,0 +1,81 @@
import * as fs from 'fs';
import * as path from 'path';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class FrontendMiddleware implements NestMiddleware {
public indexHtmlDe = fs.readFileSync(
this.getPathOfIndexHtmlFile('de'),
'utf8'
);
public indexHtmlEn = fs.readFileSync(
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
'utf8'
);
public constructor(
private readonly configurationService: ConfigurationService
) {}
public use(req: Request, res: Response, next: NextFunction) {
let featureGraphicPath = 'assets/cover.png';
if (
req.path === '/en/blog/2022/08/500-stars-on-github' ||
req.path === '/en/blog/2022/08/500-stars-on-github/'
) {
featureGraphicPath = 'assets/images/blog/500-stars-on-github.jpg';
}
if (req.path.startsWith('/api/') || this.isFileRequest(req.url)) {
// Skip
next();
} else if (req.path === '/de' || req.path.startsWith('/de/')) {
res.send(
this.interpolate(this.indexHtmlDe, {
featureGraphicPath,
languageCode: 'de',
path: req.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else {
res.send(
this.interpolate(this.indexHtmlEn, {
featureGraphicPath,
languageCode: DEFAULT_LANGUAGE_CODE,
path: req.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
}
}
private getPathOfIndexHtmlFile(aLocale: string) {
return path.join(__dirname, '..', 'client', aLocale, 'index.html');
}
private interpolate(template: string, context: any) {
return template.replace(/[$]{([^}]+)}/g, (_, objectPath) => {
const properties = objectPath.split('.');
return properties.reduce(
(previous, current) => previous?.[current],
context
);
});
}
private isFileRequest(filename: string) {
if (filename === '/assets/LICENSE') {
return true;
} else if (filename.includes('auth/ey')) {
return false;
}
return filename.split('.').pop() !== filename;
}
}

7
apps/api/src/app/order/order.service.ts

@ -230,9 +230,10 @@ export class OrderService {
}) })
}, },
{ {
SymbolProfileOverrides: { OR: [
is: null { SymbolProfileOverrides: { is: null } },
} { SymbolProfileOverrides: { assetClass: null } }
]
} }
] ]
}, },

11
apps/api/src/app/subscription/subscription.controller.ts

@ -1,6 +1,9 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { PROPERTY_COUPONS } from '@ghostfolio/common/config'; import {
DEFAULT_LANGUAGE_CODE,
PROPERTY_COUPONS
} from '@ghostfolio/common/config';
import { Coupon } from '@ghostfolio/common/interfaces'; import { Coupon } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
@ -93,7 +96,11 @@ export class SubscriptionController {
'SubscriptionController' 'SubscriptionController'
); );
res.redirect(`${this.configurationService.get('ROOT_URL')}/account`); res.redirect(
`${this.configurationService.get(
'ROOT_URL'
)}/${DEFAULT_LANGUAGE_CODE}/account`
);
} }
@Post('stripe/checkout-session') @Post('stripe/checkout-session')

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

@ -1,5 +1,6 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type'; import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { Subscription } from '@prisma/client'; import { Subscription } from '@prisma/client';
@ -33,7 +34,9 @@ export class SubscriptionService {
userId: string; userId: string;
}) { }) {
const checkoutSessionCreateParams: Stripe.Checkout.SessionCreateParams = { const checkoutSessionCreateParams: Stripe.Checkout.SessionCreateParams = {
cancel_url: `${this.configurationService.get('ROOT_URL')}/account`, cancel_url: `${this.configurationService.get(
'ROOT_URL'
)}/${DEFAULT_LANGUAGE_CODE}/account`,
client_reference_id: userId, client_reference_id: userId,
line_items: [ line_items: [
{ {

4
apps/api/src/app/user/update-user-setting.dto.ts

@ -9,6 +9,10 @@ export class UpdateUserSettingDto {
@IsOptional() @IsOptional()
isRestrictedView?: boolean; isRestrictedView?: boolean;
@IsString()
@IsOptional()
language?: string;
@IsString() @IsString()
@IsOptional() @IsOptional()
locale?: string; locale?: string;

13
apps/api/src/services/symbol-profile.service.ts

@ -115,9 +115,16 @@ export class SymbolProfileService {
} }
item.name = item.SymbolProfileOverrides?.name ?? item.name; item.name = item.SymbolProfileOverrides?.name ?? item.name;
item.sectors =
(item.SymbolProfileOverrides.sectors as unknown as Sector[]) ?? if (
item.sectors; (item.SymbolProfileOverrides.sectors as unknown as Sector[])?.length >
0
) {
item.sectors = item.SymbolProfileOverrides
.sectors as unknown as Sector[];
}
item.url = item.SymbolProfileOverrides?.url ?? item.url;
delete item.SymbolProfileOverrides; delete item.SymbolProfileOverrides;
} }

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

@ -54,45 +54,52 @@ const routes: Routes = [
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule) import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
}, },
{ {
path: 'de/blog/2021/07/hallo-ghostfolio', path: 'blog/2021/07/hallo-ghostfolio',
loadChildren: () => loadChildren: () =>
import( import(
'./pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.module' './pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.module'
).then((m) => m.HalloGhostfolioPageModule) ).then((m) => m.HalloGhostfolioPageModule)
}, },
{ {
path: 'demo', path: 'blog/2021/07/hello-ghostfolio',
loadChildren: () =>
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
},
{
path: 'en/blog/2021/07/hello-ghostfolio',
loadChildren: () => loadChildren: () =>
import( import(
'./pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.module' './pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.module'
).then((m) => m.HelloGhostfolioPageModule) ).then((m) => m.HelloGhostfolioPageModule)
}, },
{ {
path: 'en/blog/2022/01/ghostfolio-first-months-in-open-source', path: 'blog/2022/01/ghostfolio-first-months-in-open-source',
loadChildren: () => loadChildren: () =>
import( import(
'./pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module' './pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module'
).then((m) => m.FirstMonthsInOpenSourcePageModule) ).then((m) => m.FirstMonthsInOpenSourcePageModule)
}, },
{ {
path: 'en/blog/2022/07/ghostfolio-meets-internet-identity', path: 'blog/2022/07/ghostfolio-meets-internet-identity',
loadChildren: () => loadChildren: () =>
import( import(
'./pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.module' './pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.module'
).then((m) => m.GhostfolioMeetsInternetIdentityPageModule) ).then((m) => m.GhostfolioMeetsInternetIdentityPageModule)
}, },
{ {
path: 'en/blog/2022/07/how-do-i-get-my-finances-in-order', path: 'blog/2022/07/how-do-i-get-my-finances-in-order',
loadChildren: () => loadChildren: () =>
import( import(
'./pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.module' './pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.module'
).then((m) => m.HowDoIGetMyFinancesInOrderPageModule) ).then((m) => m.HowDoIGetMyFinancesInOrderPageModule)
}, },
{
path: 'blog/2022/08/500-stars-on-github',
loadChildren: () =>
import(
'./pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.module'
).then((m) => m.FiveHundredStarsOnGitHubPageModule)
},
{
path: 'demo',
loadChildren: () =>
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
},
{ {
path: 'faq', path: 'faq',
loadChildren: () => loadChildren: () =>

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

@ -24,8 +24,8 @@
class="cursor-pointer d-inline-block info-message px-3 py-2" class="cursor-pointer d-inline-block info-message px-3 py-2"
(click)="onCreateAccount()" (click)="onCreateAccount()"
> >
<span i18n>You are using the Live Demo.</span> <span>You are using the Live Demo.</span>
<span class="a ml-2" i18n>Create Account</span> <span class="a ml-2">Create Account</span>
</div></a </div></a
> >
<div <div

10
apps/client/src/app/components/access-table/access-table.component.html

@ -21,8 +21,10 @@
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell> <td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
<ng-container *ngIf="element.type === 'PUBLIC'"> <ng-container *ngIf="element.type === 'PUBLIC'">
<ion-icon class="mr-1" name="link-outline"></ion-icon> <ion-icon class="mr-1" name="link-outline"></ion-icon>
<a href="{{ baseUrl }}/p/{{ element.id }}" target="_blank" <a
>{{ baseUrl }}/p/{{ element.id }}</a href="{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}"
target="_blank"
>{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}</a
> >
</ng-container> </ng-container>
</td> </td>
@ -41,8 +43,8 @@
<ion-icon name="ellipsis-vertical"></ion-icon> <ion-icon name="ellipsis-vertical"></ion-icon>
</button> </button>
<mat-menu #transactionMenu="matMenu" xPosition="before"> <mat-menu #transactionMenu="matMenu" xPosition="before">
<button i18n mat-menu-item (click)="onDeleteAccess(element.id)"> <button mat-menu-item (click)="onDeleteAccess(element.id)">
Revoke <ng-container i18n>Revoke</ng-container>
</button> </button>
</mat-menu> </mat-menu>
</td> </td>

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

@ -8,6 +8,7 @@ import {
Output Output
} from '@angular/core'; } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { Access } from '@ghostfolio/common/interfaces'; import { Access } from '@ghostfolio/common/interfaces';
@Component({ @Component({
@ -24,6 +25,7 @@ export class AccessTableComponent implements OnChanges, OnInit {
public baseUrl = window.location.origin; public baseUrl = window.location.origin;
public dataSource: MatTableDataSource<Access>; public dataSource: MatTableDataSource<Access>;
public defaultLanguageCode = DEFAULT_LANGUAGE_CODE;
public displayedColumns = []; public displayedColumns = [];
public constructor() {} public constructor() {}
@ -44,7 +46,7 @@ export class AccessTableComponent implements OnChanges, OnInit {
public onDeleteAccess(aId: string) { public onDeleteAccess(aId: string) {
const confirmation = confirm( const confirmation = confirm(
'Do you really want to revoke this granted access?' $localize`Do you really want to revoke this granted access?`
); );
if (confirmation) { if (confirmation) {

12
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -21,18 +21,10 @@
<div class="row"> <div class="row">
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value size="medium" [value]="accountType">Account Type</gf-value>
label="Account Type"
size="medium"
[value]="accountType"
></gf-value>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value size="medium" [value]="platformName">Platform</gf-value>
label="Platform"
size="medium"
[value]="platformName"
></gf-value>
</div> </div>
</div> </div>

27
apps/client/src/app/components/accounts-table/accounts-table.component.html

@ -19,13 +19,8 @@
</ng-container> </ng-container>
<ng-container matColumnDef="currency"> <ng-container matColumnDef="currency">
<th <th *matHeaderCellDef class="d-none d-lg-table-cell px-1" mat-header-cell>
*matHeaderCellDef <ng-container i18n>Currency</ng-container>
class="d-none d-lg-table-cell px-1"
i18n
mat-header-cell
>
Currency
</th> </th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell> <td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
{{ element.currency }} {{ element.currency }}
@ -36,13 +31,8 @@
</ng-container> </ng-container>
<ng-container matColumnDef="platform"> <ng-container matColumnDef="platform">
<th <th *matHeaderCellDef class="d-none d-lg-table-cell px-1" mat-header-cell>
*matHeaderCellDef <ng-container i18n>Platform</ng-container>
class="d-none d-lg-table-cell px-1"
i18n
mat-header-cell
>
Platform
</th> </th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell> <td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
<div class="d-flex"> <div class="d-flex">
@ -81,10 +71,9 @@
<th <th
*matHeaderCellDef *matHeaderCellDef
class="d-none d-lg-table-cell px-1 text-right" class="d-none d-lg-table-cell px-1 text-right"
i18n
mat-header-cell mat-header-cell
> >
Cash Balance <ng-container i18n>Cash Balance</ng-container>
</th> </th>
<td <td
*matCellDef="let element" *matCellDef="let element"
@ -116,10 +105,9 @@
<th <th
*matHeaderCellDef *matHeaderCellDef
class="d-none d-lg-table-cell px-1 text-right" class="d-none d-lg-table-cell px-1 text-right"
i18n
mat-header-cell mat-header-cell
> >
Value <ng-container i18n>Value</ng-container>
</th> </th>
<td <td
*matCellDef="let element" *matCellDef="let element"
@ -151,10 +139,9 @@
<th <th
*matHeaderCellDef *matHeaderCellDef
class="d-lg-none d-xl-none px-1 text-right" class="d-lg-none d-xl-none px-1 text-right"
i18n
mat-header-cell mat-header-cell
> >
Value <ng-container i18n>Value</ng-container>
</th> </th>
<td <td
*matCellDef="let element" *matCellDef="let element"

4
apps/client/src/app/components/accounts-table/accounts-table.component.ts

@ -69,7 +69,9 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
} }
public onDeleteAccount(aId: string) { public onDeleteAccount(aId: string) {
const confirmation = confirm('Do you really want to delete this account?'); const confirmation = confirm(
$localize`Do you really want to delete this account?`
);
if (confirmation) { if (confirmation) {
this.accountDeleted.emit(aId); this.accountDeleted.emit(aId);

13
apps/client/src/app/components/admin-jobs/admin-jobs.html

@ -24,7 +24,7 @@
<table class="gf-table w-100"> <table class="gf-table w-100">
<thead> <thead>
<tr class="mat-header-row"> <tr class="mat-header-row">
<th class="mat-header-cell px-1 py-2 text-right" i18n>#</th> <th class="mat-header-cell px-1 py-2 text-right">#</th>
<th class="mat-header-cell px-1 py-2" i18n>Type</th> <th class="mat-header-cell px-1 py-2" i18n>Type</th>
<th class="mat-header-cell px-1 py-2" i18n>Symbol</th> <th class="mat-header-cell px-1 py-2" i18n>Symbol</th>
<th class="mat-header-cell px-1 py-2" i18n>Data Source</th> <th class="mat-header-cell px-1 py-2" i18n>Data Source</th>
@ -105,19 +105,18 @@
<ion-icon name="ellipsis-vertical"></ion-icon> <ion-icon name="ellipsis-vertical"></ion-icon>
</button> </button>
<mat-menu #accountMenu="matMenu" xPosition="before"> <mat-menu #accountMenu="matMenu" xPosition="before">
<button i18n mat-menu-item (click)="onViewData(job.data)"> <button mat-menu-item (click)="onViewData(job.data)">
View Data <ng-container i18n>View Data</ng-container>
</button> </button>
<button <button
i18n
mat-menu-item mat-menu-item
[disabled]="job.stacktrace?.length <= 0" [disabled]="job.stacktrace?.length <= 0"
(click)="onViewStacktrace(job.stacktrace)" (click)="onViewStacktrace(job.stacktrace)"
> >
View Stacktrace <ng-container i18n>View Stacktrace</ng-container>
</button> </button>
<button i18n mat-menu-item (click)="onDeleteJob(job.id)"> <button mat-menu-item (click)="onDeleteJob(job.id)">
Delete Job <ng-container i18n>Delete Job</ng-container>
</button> </button>
</mat-menu> </mat-menu>
</td> </td>

4
apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html

@ -43,8 +43,8 @@
</div> </div>
<div class="justify-content-end" mat-dialog-actions> <div class="justify-content-end" mat-dialog-actions>
<button i18n mat-button (click)="onCancel()">Cancel</button> <button i18n mat-button (click)="onCancel()">Cancel</button>
<button color="primary" i18n mat-flat-button (click)="onUpdate()"> <button color="primary" mat-flat-button (click)="onUpdate()">
Save <ng-container i18n>Save</ng-container>
</button> </button>
</div> </div>
</form> </form>

9
apps/client/src/app/components/admin-market-data/admin-market-data.html

@ -36,26 +36,23 @@
</button> </button>
<mat-menu #accountMenu="matMenu" xPosition="before"> <mat-menu #accountMenu="matMenu" xPosition="before">
<button <button
i18n
mat-menu-item mat-menu-item
(click)="onGatherSymbol({dataSource: item.dataSource, symbol: item.symbol})" (click)="onGatherSymbol({dataSource: item.dataSource, symbol: item.symbol})"
> >
Gather Data <ng-container i18n>Gather Data</ng-container>
</button> </button>
<button <button
i18n
mat-menu-item mat-menu-item
(click)="onGatherProfileDataBySymbol({dataSource: item.dataSource, symbol: item.symbol})" (click)="onGatherProfileDataBySymbol({dataSource: item.dataSource, symbol: item.symbol})"
> >
Gather Profile Data <ng-container i18n>Gather Profile Data</ng-container>
</button> </button>
<button <button
i18n
mat-menu-item mat-menu-item
[disabled]="item.activityCount !== 0" [disabled]="item.activityCount !== 0"
(click)="onDeleteProfileData({dataSource: item.dataSource, symbol: item.symbol})" (click)="onDeleteProfileData({dataSource: item.dataSource, symbol: item.symbol})"
> >
Delete Profile Data <ng-container i18n>Delete</ng-container>
</button> </button>
</mat-menu> </mat-menu>
</td> </td>

16
apps/client/src/app/components/admin-overview/admin-overview.component.ts

@ -103,7 +103,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
} }
public onAddCurrency() { public onAddCurrency() {
const currency = prompt('Please add a currency:'); const currency = prompt($localize`Please add a currency:`);
if (currency) { if (currency) {
const currencies = uniq([...this.customCurrencies, currency]); const currencies = uniq([...this.customCurrencies, currency]);
@ -116,7 +116,9 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
} }
public onDeleteCoupon(aCouponCode: string) { public onDeleteCoupon(aCouponCode: string) {
const confirmation = confirm('Do you really want to delete this coupon?'); const confirmation = confirm(
$localize`Do you really want to delete this coupon?`
);
if (confirmation === true) { if (confirmation === true) {
const coupons = this.coupons.filter((coupon) => { const coupons = this.coupons.filter((coupon) => {
@ -127,7 +129,9 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
} }
public onDeleteCurrency(aCurrency: string) { public onDeleteCurrency(aCurrency: string) {
const confirmation = confirm('Do you really want to delete this currency?'); const confirmation = confirm(
$localize`Do you really want to delete this currency?`
);
if (confirmation === true) { if (confirmation === true) {
const currencies = this.customCurrencies.filter((currency) => { const currencies = this.customCurrencies.filter((currency) => {
@ -142,7 +146,9 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
} }
public onFlushCache() { public onFlushCache() {
const confirmation = confirm('Do you really want to flush the cache?'); const confirmation = confirm(
$localize`Do you really want to flush the cache?`
);
if (confirmation === true) { if (confirmation === true) {
this.cacheService this.cacheService
@ -190,7 +196,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
} }
public onSetSystemMessage() { public onSetSystemMessage() {
const systemMessage = prompt('Please set your system message:'); const systemMessage = prompt($localize`Please set your system message:`);
if (systemMessage) { if (systemMessage) {
this.putSystemMessage(systemMessage); this.putSystemMessage(systemMessage);

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

@ -8,7 +8,7 @@
<div class="w-50">{{ userCount }}</div> <div class="w-50">{{ userCount }}</div>
</div> </div>
<div class="d-flex my-3"> <div class="d-flex my-3">
<div class="w-50" i18n>Transaction Count</div> <div class="w-50" i18n>Activity Count</div>
<div class="w-50"> <div class="w-50">
<ng-container *ngIf="transactionCount"> <ng-container *ngIf="transactionCount">
{{ transactionCount }} ({{ transactionCount / userCount | number {{ transactionCount }} ({{ transactionCount / userCount | number
@ -17,7 +17,7 @@
</div> </div>
</div> </div>
<div class="d-flex my-3"> <div class="d-flex my-3">
<div class="w-50" i18n>Data Gathering</div> <div class="w-50" i18n>Data Management</div>
<div class="w-50"> <div class="w-50">
<div class="overflow-hidden"> <div class="overflow-hidden">
<div class="mb-2"> <div class="mb-2">

4
apps/client/src/app/components/admin-users/admin-users.component.ts

@ -55,7 +55,9 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
} }
public onDeleteUser(aId: string) { public onDeleteUser(aId: string) {
const confirmation = confirm('Do you really want to delete this user?'); const confirmation = confirm(
$localize`Do you really want to delete this user?`
);
if (confirmation) { if (confirmation) {
this.dataService this.dataService

16
apps/client/src/app/components/admin-users/admin-users.html

@ -7,17 +7,17 @@
<tr class="mat-header-row"> <tr class="mat-header-row">
<th class="mat-header-cell px-1 py-2 text-right">#</th> <th class="mat-header-cell px-1 py-2 text-right">#</th>
<th class="mat-header-cell px-1 py-2" i18n>User</th> <th class="mat-header-cell px-1 py-2" i18n>User</th>
<th class="mat-header-cell px-1 py-2 text-right" i18n> <th class="mat-header-cell px-1 py-2 text-right">
Registration <ng-container i18n>Registration</ng-container>
</th> </th>
<th class="mat-header-cell px-1 py-2 text-right" i18n> <th class="mat-header-cell px-1 py-2 text-right">
Accounts <ng-container i18n>Accounts</ng-container>
</th> </th>
<th class="mat-header-cell px-1 py-2 text-right" i18n> <th class="mat-header-cell px-1 py-2 text-right">
Activities <ng-container i18n>Activities</ng-container>
</th> </th>
<th class="mat-header-cell px-1 py-2 text-right" i18n> <th class="mat-header-cell px-1 py-2 text-right">
Engagement per Day <ng-container i18n>Engagement per Day</ng-container>
</th> </th>
<th class="mat-header-cell px-1 py-2" i18n>Last Request</th> <th class="mat-header-cell px-1 py-2" i18n>Last Request</th>
<th class="mat-header-cell px-1 py-2"></th> <th class="mat-header-cell px-1 py-2"></th>

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

@ -285,17 +285,16 @@
mat-flat-button mat-flat-button
><ion-icon name="logo-github"></ion-icon ><ion-icon name="logo-github"></ion-icon
></a> ></a>
<button class="mx-1" i18n mat-flat-button (click)="openLoginDialog()"> <button class="mx-1" mat-flat-button (click)="openLoginDialog()">
Sign In <ng-container i18n>Sign in</ng-container>
</button> </button>
<a <a
*ngIf="currentRoute !== 'register' && !info?.isReadOnlyMode" *ngIf="currentRoute !== 'register' && !info?.isReadOnlyMode"
class="d-none d-sm-block" class="d-none d-sm-block"
color="primary" color="primary"
i18n
mat-flat-button mat-flat-button
[routerLink]="['/register']" [routerLink]="['/register']"
>Get Started ><ng-container i18n>Get started</ng-container>
</a> </a>
</ng-container> </ng-container>
</mat-toolbar> </mat-toolbar>

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

@ -109,7 +109,7 @@ export class HeaderComponent implements OnChanges {
data: { data: {
accessToken: '', accessToken: '',
hasPermissionToUseSocialLogin: this.hasPermissionForSocialLogin, hasPermissionToUseSocialLogin: this.hasPermissionForSocialLogin,
title: 'Sign in' title: $localize`Sign in`
}, },
width: '30rem' width: '30rem'
}); });
@ -123,7 +123,7 @@ export class HeaderComponent implements OnChanges {
.loginAnonymous(data?.accessToken) .loginAnonymous(data?.accessToken)
.pipe( .pipe(
catchError(() => { catchError(() => {
alert('Oops! Incorrect Security Token.'); alert($localize`Oops! Incorrect Security Token.`);
return EMPTY; return EMPTY;
}), }),

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

@ -2,6 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { import {
@ -9,7 +10,6 @@ import {
SettingsStorageService SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service'; } from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { defaultDateRangeOptions } from '@ghostfolio/common/config';
import { Position, User } from '@ghostfolio/common/interfaces'; import { Position, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
@ -27,7 +27,7 @@ import { PositionDetailDialogParams } from '../position/position-detail-dialog/i
}) })
export class HomeHoldingsComponent implements OnDestroy, OnInit { export class HomeHoldingsComponent implements OnDestroy, OnInit {
public dateRange: DateRange; public dateRange: DateRange;
public dateRangeOptions = defaultDateRangeOptions; public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
public deviceType: string; public deviceType: string;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public hasPermissionToCreateOrder: boolean; public hasPermissionToCreateOrder: boolean;

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

@ -34,6 +34,7 @@
<ngx-skeleton-loader <ngx-skeleton-loader
*ngIf="isLoading" *ngIf="isLoading"
animation="pulse" animation="pulse"
class="px-2 py-3"
[theme]="{ [theme]="{
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'

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

@ -1,4 +1,5 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { import {
@ -6,7 +7,6 @@ import {
SettingsStorageService SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service'; } from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { defaultDateRangeOptions } from '@ghostfolio/common/config';
import { import {
PortfolioPerformance, PortfolioPerformance,
UniqueAsset, UniqueAsset,
@ -26,7 +26,7 @@ import { takeUntil } from 'rxjs/operators';
}) })
export class HomeOverviewComponent implements OnDestroy, OnInit { export class HomeOverviewComponent implements OnDestroy, OnInit {
public dateRange: DateRange; public dateRange: DateRange;
public dateRangeOptions = defaultDateRangeOptions; public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
public deviceType: string; public deviceType: string;
public errors: UniqueAsset[]; public errors: UniqueAsset[];
public hasError: boolean; public hasError: boolean;

2
apps/client/src/app/components/home-overview/home-overview.scss

@ -6,7 +6,7 @@
.chart-container { .chart-container {
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;
height: auto; height: auto;
max-width: 67rem; max-width: 50rem;
// Fallback for aspect-ratio (using padding hack) // Fallback for aspect-ratio (using padding hack)
@supports not (aspect-ratio: 16 / 9) { @supports not (aspect-ratio: 16 / 9) {

2
apps/client/src/app/components/investment-chart/investment-chart.component.ts

@ -122,7 +122,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
data: this.investments.map((position) => { data: this.investments.map((position) => {
return position.investment; return position.investment;
}), }),
label: 'Investment', label: $localize`Deposit`,
segment: { segment: {
borderColor: (context: unknown) => borderColor: (context: unknown) =>
this.isInFuture( this.isInFuture(

9
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html

@ -25,14 +25,14 @@
> >
<img <img
class="mr-2" class="mr-2"
src="./assets/icons/internet-computer.svg" src="../assets/icons/internet-computer.svg"
style="height: 0.75rem" style="height: 0.75rem"
/><span i18n>Sign in with Internet Identity</span> /><span i18n>Sign in with Internet Identity</span>
</button> </button>
<a href="/api/v1/auth/google" mat-stroked-button <a href="../api/v1/auth/google" mat-stroked-button
><img ><img
class="mr-2" class="mr-2"
src="./assets/icons/google.svg" src="../assets/icons/google.svg"
style="height: 1rem" style="height: 1rem"
/><span i18n>Sign in with Google</span></a /><span i18n>Sign in with Google</span></a
> >
@ -49,12 +49,11 @@
<div> <div>
<button <button
color="primary" color="primary"
i18n
mat-flat-button mat-flat-button
[disabled]="!data.accessToken" [disabled]="!data.accessToken"
[mat-dialog-close]="data" [mat-dialog-close]="data"
> >
Sign in <ng-container i18n>Sign in</ng-container>
</button> </button>
</div> </div>
</div> </div>

2
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts

@ -45,7 +45,7 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
public onEditEmergencyFund() { public onEditEmergencyFund() {
const emergencyFundInput = prompt( const emergencyFundInput = prompt(
'Please enter the amount of your emergency fund:', $localize`Please enter the amount of your emergency fund:`,
this.summary.emergencyFund.toString() this.summary.emergencyFund.toString()
); );
const emergencyFund = parseFloat(emergencyFundInput?.trim()); const emergencyFund = parseFloat(emergencyFundInput?.trim());

70
apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html

@ -35,112 +35,124 @@
<div class="row"> <div class="row">
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
label="Change" i18n
size="medium" size="medium"
[colorizeSign]="true" [colorizeSign]="true"
[currency]="data.baseCurrency" [currency]="data.baseCurrency"
[locale]="data.locale" [locale]="data.locale"
[value]="netPerformance" [value]="netPerformance"
></gf-value> >Change</gf-value
>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
label="Performance" i18n
size="medium" size="medium"
[colorizeSign]="true" [colorizeSign]="true"
[isPercent]="true" [isPercent]="true"
[locale]="data.locale" [locale]="data.locale"
[value]="netPerformancePercent" [value]="netPerformancePercent"
></gf-value> >Performance</gf-value
>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
label="Average Unit Price" i18n
size="medium" size="medium"
[currency]="SymbolProfile?.currency" [currency]="SymbolProfile?.currency"
[locale]="data.locale" [locale]="data.locale"
[value]="averagePrice" [value]="averagePrice"
></gf-value> >Average Unit Price</gf-value
>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
label="Market Price" i18n
size="medium" size="medium"
[currency]="SymbolProfile?.currency" [currency]="SymbolProfile?.currency"
[locale]="data.locale" [locale]="data.locale"
[value]="marketPrice" [value]="marketPrice"
></gf-value> >Market Price</gf-value
>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
label="Minimum Price" i18n
size="medium" size="medium"
[currency]="SymbolProfile?.currency" [currency]="SymbolProfile?.currency"
[locale]="data.locale" [locale]="data.locale"
[ngClass]="{ 'text-danger': minPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }" [ngClass]="{ 'text-danger': minPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }"
[value]="minPrice" [value]="minPrice"
></gf-value> >Minimum Price</gf-value
>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
label="Maximum Price" i18n
size="medium" size="medium"
[currency]="SymbolProfile?.currency" [currency]="SymbolProfile?.currency"
[locale]="data.locale" [locale]="data.locale"
[ngClass]="{ 'text-success': maxPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }" [ngClass]="{ 'text-success': maxPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }"
[value]="maxPrice" [value]="maxPrice"
></gf-value> >Maximum Price</gf-value
>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
label="Quantity" i18n
size="medium" size="medium"
[locale]="data.locale" [locale]="data.locale"
[precision]="quantityPrecision" [precision]="quantityPrecision"
[value]="quantity" [value]="quantity"
></gf-value> >Quantity</gf-value
>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
label="Investment" i18n
size="medium" size="medium"
[currency]="data.baseCurrency" [currency]="data.baseCurrency"
[locale]="data.locale" [locale]="data.locale"
[value]="investment" [value]="investment"
></gf-value> >Investment</gf-value
>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
label="First Buy Date" i18n
size="medium" size="medium"
[isDate]="true" [isDate]="true"
[locale]="data.locale" [locale]="data.locale"
[value]="firstBuyDate" [value]="firstBuyDate"
></gf-value> >First Buy Date</gf-value
>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
i18n
size="medium" size="medium"
[label]="transactionCount === 1 ? 'Transaction' : 'Transactions'"
[locale]="data.locale" [locale]="data.locale"
[value]="transactionCount" [value]="transactionCount"
></gf-value> >Transactions</gf-value
>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
label="Asset Class" i18n
size="medium" size="medium"
[hidden]="!SymbolProfile?.assetClass" [hidden]="!SymbolProfile?.assetClass"
[value]="SymbolProfile?.assetClass" [value]="SymbolProfile?.assetClass"
></gf-value> >Asset Class</gf-value
>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
label="Asset Sub Class" i18n
size="medium" size="medium"
[hidden]="!SymbolProfile?.assetSubClass" [hidden]="!SymbolProfile?.assetSubClass"
[value]="SymbolProfile?.assetSubClass" [value]="SymbolProfile?.assetSubClass"
></gf-value> >Asset Sub Class</gf-value
>
</div> </div>
<ng-container <ng-container
*ngIf="SymbolProfile?.countries?.length > 0 || SymbolProfile?.sectors?.length > 0" *ngIf="SymbolProfile?.countries?.length > 0 || SymbolProfile?.sectors?.length > 0"
@ -150,22 +162,24 @@
> >
<div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3"> <div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3">
<gf-value <gf-value
label="Sector" i18n
size="medium" size="medium"
[locale]="data.locale" [locale]="data.locale"
[value]="SymbolProfile.sectors[0].name" [value]="SymbolProfile.sectors[0].name"
></gf-value> >Sector</gf-value
>
</div> </div>
<div <div
*ngIf="SymbolProfile?.countries?.length === 1" *ngIf="SymbolProfile?.countries?.length === 1"
class="col-6 mb-3" class="col-6 mb-3"
> >
<gf-value <gf-value
label="Country" i18n
size="medium" size="medium"
[locale]="data.locale" [locale]="data.locale"
[value]="SymbolProfile.countries[0].name" [value]="SymbolProfile.countries[0].name"
></gf-value> >Country</gf-value
>
</div> </div>
</ng-container> </ng-container>
<ng-template #charts> <ng-template #charts>

20
apps/client/src/app/components/positions-table/positions-table.component.html

@ -18,8 +18,8 @@
</ng-container> </ng-container>
<ng-container matColumnDef="symbol"> <ng-container matColumnDef="symbol">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header> <th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
Symbol <ng-container i18n>Symbol</ng-container>
</th> </th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<span [title]="element.name">{{ element.symbol | gfSymbol }}</span> <span [title]="element.name">{{ element.symbol | gfSymbol }}</span>
@ -30,11 +30,10 @@
<th <th
*matHeaderCellDef *matHeaderCellDef
class="d-none d-lg-table-cell px-1" class="d-none d-lg-table-cell px-1"
i18n
mat-header-cell mat-header-cell
mat-sort-header mat-sort-header
> >
Name <ng-container i18n>Name</ng-container>
</th> </th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell> <td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
<ng-container *ngIf="element.name !== element.symbol">{{ <ng-container *ngIf="element.name !== element.symbol">{{
@ -47,11 +46,10 @@
<th <th
*matHeaderCellDef *matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1" class="d-none d-lg-table-cell justify-content-end px-1"
i18n
mat-header-cell mat-header-cell
mat-sort-header mat-sort-header
> >
Value <ng-container i18n>Value</ng-container>
</th> </th>
<td class="d-none d-lg-table-cell px-1" mat-cell *matCellDef="let element"> <td class="d-none d-lg-table-cell px-1" mat-cell *matCellDef="let element">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
@ -68,11 +66,10 @@
<th <th
*matHeaderCellDef *matHeaderCellDef
class="justify-content-end px-1" class="justify-content-end px-1"
i18n
mat-header-cell mat-header-cell
mat-sort-header mat-sort-header
> >
Allocation <ng-container i18n>Allocation</ng-container>
</th> </th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
@ -89,10 +86,9 @@
<th <th
*matHeaderCellDef *matHeaderCellDef
class="d-none d-lg-table-cell px-1 text-right" class="d-none d-lg-table-cell px-1 text-right"
i18n
mat-header-cell mat-header-cell
> >
Performance <ng-container i18n>Performance</ng-container>
</th> </th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell> <td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
@ -137,8 +133,8 @@
*ngIf="dataSource.data.length > pageSize && !isLoading" *ngIf="dataSource.data.length > pageSize && !isLoading"
class="my-3 text-center" class="my-3 text-center"
> >
<button i18n mat-stroked-button (click)="onShowAllPositions()"> <button mat-stroked-button (click)="onShowAllPositions()">
Show all <ng-container i18n>Show all</ng-container>
</button> </button>
</div> </div>

8
apps/client/src/app/components/toggle/toggle.component.ts

@ -17,6 +17,14 @@ import { ToggleOption } from '@ghostfolio/common/types';
styleUrls: ['./toggle.component.scss'] styleUrls: ['./toggle.component.scss']
}) })
export class ToggleComponent implements OnChanges, OnInit { export class ToggleComponent implements OnChanges, OnInit {
public static DEFAULT_DATE_RANGE_OPTIONS: ToggleOption[] = [
{ label: $localize`Today`, value: '1d' },
{ label: $localize`YTD`, value: 'ytd' },
{ label: $localize`1Y`, value: '1y' },
{ label: $localize`5Y`, value: '5y' },
{ label: $localize`Max`, value: 'max' }
];
@Input() defaultValue: string; @Input() defaultValue: string;
@Input() isLoading: boolean; @Input() isLoading: boolean;
@Input() options: ToggleOption[]; @Input() options: ToggleOption[];

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

@ -72,7 +72,13 @@ export class AuthGuard implements CanActivate {
}) })
) )
.subscribe((user) => { .subscribe((user) => {
if ( const userLanguage = user?.settings?.language;
if (userLanguage && document.documentElement.lang !== userLanguage) {
window.location.href = `../${userLanguage}`;
resolve(false);
return;
} else if (
state.url.startsWith('/home') && state.url.startsWith('/home') &&
user.settings.viewMode === ViewMode.ZEN user.settings.viewMode === ViewMode.ZEN
) { ) {

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

@ -56,14 +56,18 @@ export class HttpResponseInterceptor implements HttpInterceptor {
if (!this.snackBarRef) { if (!this.snackBarRef) {
if (this.info.isReadOnlyMode) { if (this.info.isReadOnlyMode) {
this.snackBarRef = this.snackBar.open( this.snackBarRef = this.snackBar.open(
'This feature is currently unavailable. Please try again later.', $localize`This feature is currently unavailable.` +
' ' +
$localize`Please try again later.`,
undefined, undefined,
{ duration: 6000 } { duration: 6000 }
); );
} else { } else {
this.snackBarRef = this.snackBar.open( this.snackBarRef = this.snackBar.open(
'This feature requires a subscription.', $localize`This feature requires a subscription.`,
this.hasPermissionForSubscription ? 'Upgrade Plan' : undefined, this.hasPermissionForSubscription
? $localize`Upgrade Plan`
: undefined,
{ duration: 6000 } { duration: 6000 }
); );
} }
@ -79,8 +83,10 @@ export class HttpResponseInterceptor implements HttpInterceptor {
} else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) { } else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) {
if (!this.snackBarRef) { if (!this.snackBarRef) {
this.snackBarRef = this.snackBar.open( this.snackBarRef = this.snackBar.open(
'Oops! Something went wrong. Please try again later.', $localize`Oops! Something went wrong.` +
'Okay', ' ' +
$localize`Please try again later.`,
$localize`Okay`,
{ duration: 6000 } { duration: 6000 }
); );

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

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: AboutPageComponent, component: AboutPageComponent,
path: '', path: '',
title: 'About' title: $localize`About`
} }
]; ];

40
apps/client/src/app/pages/about/about-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-flex justify-content-center mb-3" i18n>About Ghostfolio</h3> <h3 class="d-flex justify-content-center mb-3">About Ghostfolio</h3>
<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
@ -21,7 +21,7 @@
<ng-container *ngIf="version"> <ng-container *ngIf="version">
This instance is running Ghostfolio {{ version }}. This instance is running Ghostfolio {{ version }}.
</ng-container> </ng-container>
<ng-container *ngIf="hasPermissionForStatistics" i18n <ng-container *ngIf="hasPermissionForStatistics"
>Check the system status at >Check the system status at
<a href="https://status.ghostfol.io" title="Ghostfolio status" <a href="https://status.ghostfol.io" title="Ghostfolio status"
>status.ghostfol.io</a >status.ghostfol.io</a
@ -102,33 +102,36 @@
<div *ngIf="hasPermissionForStatistics" class="mb-5 row"> <div *ngIf="hasPermissionForStatistics" class="mb-5 row">
<div class="col"> <div class="col">
<h3 class="mb-3 text-center" i18n>Ghostfolio in Numbers</h3> <h3 class="mb-3 text-center">Ghostfolio in Numbers</h3>
<mat-card> <mat-card>
<mat-card-content> <mat-card-content>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
<gf-value <gf-value
label="Active Users" i18n
size="large" size="large"
subLabel="(Last 24 hours)" subLabel="(Last 24 hours)"
[value]="statistics?.activeUsers1d ?? '-'" [value]="statistics?.activeUsers1d ?? '-'"
></gf-value> >Active Users</gf-value
>
</div> </div>
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
<gf-value <gf-value
label="New Users" i18n
size="large" size="large"
subLabel="(Last 30 days)" subLabel="(Last 30 days)"
[value]="statistics?.newUsers30d ?? '-'" [value]="statistics?.newUsers30d ?? '-'"
></gf-value> >New Users</gf-value
>
</div> </div>
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
<gf-value <gf-value
label="Active Users" i18n
size="large" size="large"
subLabel="(Last 30 days)" subLabel="(Last 30 days)"
[value]="statistics?.activeUsers30d ?? '-'" [value]="statistics?.activeUsers30d ?? '-'"
></gf-value> >Active Users</gf-value
>
</div> </div>
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
<a <a
@ -136,10 +139,11 @@
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"
> >
<gf-value <gf-value
label="Users in Slack community" i18n
size="large" size="large"
[value]="statistics?.slackCommunityUsers ?? '-'" [value]="statistics?.slackCommunityUsers ?? '-'"
></gf-value> >Users in Slack community</gf-value
>
</a> </a>
</div> </div>
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
@ -148,10 +152,11 @@
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors" href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
> >
<gf-value <gf-value
label="Contributors on GitHub" i18n
size="large" size="large"
[value]="statistics?.gitHubContributors ?? '-'" [value]="statistics?.gitHubContributors ?? '-'"
></gf-value> >Contributors on GitHub</gf-value
>
</a> </a>
</div> </div>
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
@ -160,10 +165,11 @@
href="https://github.com/ghostfolio/ghostfolio/stargazers" href="https://github.com/ghostfolio/ghostfolio/stargazers"
> >
<gf-value <gf-value
label="Stars on GitHub" i18n
size="large" size="large"
[value]="statistics?.gitHubStargazers ?? '-'" [value]="statistics?.gitHubStargazers ?? '-'"
></gf-value> >Stars on GitHub</gf-value
>
</a> </a>
</div> </div>
</div> </div>
@ -177,7 +183,6 @@
<a <a
class="py-2 w-100" class="py-2 w-100"
color="primary" color="primary"
i18n
mat-stroked-button mat-stroked-button
[routerLink]="['/faq']" [routerLink]="['/faq']"
>FAQ</a >FAQ</a
@ -190,7 +195,6 @@
<a <a
class="py-2 w-100" class="py-2 w-100"
color="primary" color="primary"
i18n
mat-stroked-button mat-stroked-button
[routerLink]="['/about', 'changelog']" [routerLink]="['/about', 'changelog']"
>Changelog & License</a >Changelog & License</a
@ -200,7 +204,6 @@
<a <a
class="py-2 w-100" class="py-2 w-100"
color="primary" color="primary"
i18n
mat-stroked-button mat-stroked-button
[routerLink]="['/about', 'privacy-policy']" [routerLink]="['/about', 'privacy-policy']"
>Privacy Policy</a >Privacy Policy</a
@ -210,7 +213,6 @@
<a <a
class="py-2 w-100" class="py-2 w-100"
color="primary" color="primary"
i18n
mat-flat-button mat-flat-button
[routerLink]="['/blog']" [routerLink]="['/blog']"
>Blog</a >Blog</a

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

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: ChangelogPageComponent, component: ChangelogPageComponent,
path: '', path: '',
title: 'Changelog & License' title: $localize`Changelog & License`
} }
]; ];

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

@ -4,7 +4,7 @@
<h3 class="mb-3 text-center" i18n>Changelog</h3> <h3 class="mb-3 text-center" i18n>Changelog</h3>
<mat-card class="changelog"> <mat-card class="changelog">
<mat-card-content> <mat-card-content>
<markdown [src]="'assets/CHANGELOG.md'"></markdown> <markdown [src]="'../assets/CHANGELOG.md'"></markdown>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -15,7 +15,7 @@
<h3 class="mb-3 text-center" i18n>License</h3> <h3 class="mb-3 text-center" i18n>License</h3>
<mat-card> <mat-card>
<mat-card-content> <mat-card-content>
<markdown [src]="'assets/LICENSE'"></markdown> <markdown [src]="'../assets/LICENSE'"></markdown>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>

2
apps/client/src/app/pages/about/privacy-policy/privacy-policy-page-routing.module.ts

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: PrivacyPolicyPageComponent, component: PrivacyPolicyPageComponent,
path: '', path: '',
title: 'Privacy Policy' title: $localize`Privacy Policy`
} }
]; ];

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

@ -2,7 +2,7 @@
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<h3 class="mb-3 text-center" i18n>Privacy Policy</h3> <h3 class="mb-3 text-center" i18n>Privacy Policy</h3>
<markdown [src]="'assets/privacy-policy.md'"></markdown> <markdown [src]="'../assets/privacy-policy.md'"></markdown>
</div> </div>
</div> </div>
</div> </div>

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

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: AccountPageComponent, component: AccountPageComponent,
path: '', path: '',
title: 'My Ghostfolio' title: $localize`My Ghostfolio`
} }
]; ];

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

@ -53,6 +53,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
public hasPermissionToDeleteAccess: boolean; public hasPermissionToDeleteAccess: boolean;
public hasPermissionToUpdateViewMode: boolean; public hasPermissionToUpdateViewMode: boolean;
public hasPermissionToUpdateUserSettings: boolean; public hasPermissionToUpdateUserSettings: boolean;
public language = document.documentElement.lang;
public locales = ['de', 'de-CH', 'en-GB', 'en-US']; public locales = ['de', 'de-CH', 'en-GB', 'en-US'];
public price: number; public price: number;
public priceId: string; public priceId: string;
@ -162,6 +163,14 @@ export class AccountPageComponent implements OnDestroy, OnInit {
this.user = user; this.user = user;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
if (aKey === 'language') {
if (aValue) {
window.location.href = `../${aValue}/account`;
} else {
window.location.href = `../`;
}
}
}); });
}); });
} }
@ -218,7 +227,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
} }
public onRedeemCoupon() { public onRedeemCoupon() {
let couponCode = prompt('Please enter your coupon code:'); let couponCode = prompt($localize`Please enter your coupon code:`);
couponCode = couponCode?.trim(); couponCode = couponCode?.trim();
if (couponCode) { if (couponCode) {
@ -227,17 +236,21 @@ export class AccountPageComponent implements OnDestroy, OnInit {
.pipe( .pipe(
takeUntil(this.unsubscribeSubject), takeUntil(this.unsubscribeSubject),
catchError(() => { catchError(() => {
this.snackBar.open('😞 Could not redeem coupon code', undefined, { this.snackBar.open(
'😞 ' + $localize`Could not redeem coupon code`,
undefined,
{
duration: 3000 duration: 3000
}); }
);
return EMPTY; return EMPTY;
}) })
) )
.subscribe(() => { .subscribe(() => {
this.snackBarRef = this.snackBar.open( this.snackBarRef = this.snackBar.open(
'✅ Coupon code has been redeemed', '✅' + $localize`Coupon code has been redeemed`,
'Reload', $localize`Reload`,
{ {
duration: 3000 duration: 3000
} }
@ -283,7 +296,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
this.registerDevice(); this.registerDevice();
} else { } else {
const confirmation = confirm( const confirmation = confirm(
'Do you really want to remove this sign in method?' $localize`Do you really want to remove this sign in method?`
); );
if (confirmation) { if (confirmation) {

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

@ -31,11 +31,10 @@
<ng-container *ngIf="hasPermissionForSubscription"> <ng-container *ngIf="hasPermissionForSubscription">
<button <button
color="primary" color="primary"
i18n
mat-flat-button mat-flat-button
(click)="onCheckout(priceId)" (click)="onCheckout(priceId)"
> >
Upgrade <ng-container i18n>Upgrade</ng-container>
</button> </button>
<div *ngIf="price" class="mt-1"> <div *ngIf="price" class="mt-1">
<ng-container *ngIf="coupon" <ng-container *ngIf="coupon"
@ -91,8 +90,8 @@
<div class="d-flex mt-4 py-1"> <div class="d-flex mt-4 py-1">
<form #changeUserSettingsForm="ngForm" class="w-100"> <form #changeUserSettingsForm="ngForm" class="w-100">
<div class="d-flex mb-2"> <div class="d-flex mb-2">
<div class="align-items-center d-flex pt-1 pt-1 w-50" i18n> <div class="align-items-center d-flex pt-1 pt-1 w-50">
Base Currency <ng-container i18n>Base Currency</ng-container>
</div> </div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
@ -111,11 +110,30 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div class="align-items-center d-flex mb-2">
<div class="pr-1 w-50">
<div i18n>Language</div>
<div class="hint-text text-muted" i18n>Beta</div>
</div>
<div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100">
<mat-select
name="language"
[value]="language"
(selectionChange)="onChangeUserSetting('language', $event.value)"
>
<mat-option [value]="null"></mat-option>
<mat-option value="de">Deutsch</mat-option>
<mat-option value="en">English</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="align-items-center d-flex mb-2"> <div class="align-items-center d-flex mb-2">
<div class="pr-1 w-50"> <div class="pr-1 w-50">
<div i18n>Locale</div> <div i18n>Locale</div>
<div class="hint-text text-muted" i18n> <div class="hint-text text-muted">
Date and number format <ng-container i18n>Date and number format</ng-container>
</div> </div>
</div> </div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
@ -137,8 +155,8 @@
</div> </div>
</div> </div>
<div class="d-flex"> <div class="d-flex">
<div class="align-items-center d-flex pr-1 pt-1 w-50" i18n> <div class="align-items-center d-flex pr-1 pt-1 w-50">
View Mode <ng-container i18n>View Mode</ng-container>
</div> </div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
<div class="align-items-center d-flex overflow-hidden"> <div class="align-items-center d-flex overflow-hidden">

3
apps/client/src/app/pages/account/create-or-update-access-dialog/create-or-update-access-dialog.html

@ -14,12 +14,11 @@
<button i18n mat-button (click)="onCancel()">Cancel</button> <button i18n mat-button (click)="onCancel()">Cancel</button>
<button <button
color="primary" color="primary"
i18n
mat-flat-button mat-flat-button
[disabled]="!addAccessForm.form.valid" [disabled]="!addAccessForm.form.valid"
[mat-dialog-close]="data" [mat-dialog-close]="data"
> >
Save <ng-container i18n>Save</ng-container>
</button> </button>
</div> </div>
</form> </form>

2
apps/client/src/app/pages/accounts/accounts-page-routing.module.ts

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: AccountsPageComponent, component: AccountsPageComponent,
path: '', path: '',
title: 'Accounts' title: $localize`Accounts`
} }
]; ];

3
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html

@ -66,12 +66,11 @@
<button i18n mat-button (click)="onCancel()">Cancel</button> <button i18n mat-button (click)="onCancel()">Cancel</button>
<button <button
color="primary" color="primary"
i18n
mat-flat-button mat-flat-button
[disabled]="!addAccountForm.form.valid" [disabled]="!addAccountForm.form.valid"
[mat-dialog-close]="data" [mat-dialog-close]="data"
> >
Save <ng-container i18n>Save</ng-container>
</button> </button>
</div> </div>
</form> </form>

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

@ -20,7 +20,7 @@ const routes: Routes = [
], ],
component: AdminPageComponent, component: AdminPageComponent,
path: '', path: '',
title: 'Admin Control' title: $localize`Admin Control`
} }
]; ];

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

@ -28,6 +28,7 @@ export class AuthPageComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => { .subscribe((params) => {
const jwt = params['jwt']; const jwt = params['jwt'];
this.tokenStorageService.saveToken( this.tokenStorageService.saveToken(
jwt, jwt,
this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true' this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true'

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

@ -68,7 +68,7 @@
<p class="my-5 text-center"> <p class="my-5 text-center">
<img <img
alt="Ghostfol.io Screenshot" alt="Ghostfol.io Screenshot"
src="./assets/images/screenshot.png" src="../assets/images/screenshot.png"
style="max-width: 100%; width: 20rem" style="max-width: 100%; width: 20rem"
title="Ghostfol.io Screenshot" title="Ghostfol.io Screenshot"
/> />

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

@ -66,7 +66,7 @@
<p class="my-5 text-center"> <p class="my-5 text-center">
<img <img
alt="Ghostfol.io Screenshot" alt="Ghostfol.io Screenshot"
src="./assets/images/screenshot.png" src="../assets/images/screenshot.png"
style="max-width: 100%; width: 20rem" style="max-width: 100%; width: 20rem"
title="Ghostfol.io Screenshot" title="Ghostfol.io Screenshot"
/> />

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

@ -20,9 +20,7 @@
<h2 class="h4">From 1* to 100 stars on GitHub</h2> <h2 class="h4">From 1* to 100 stars on GitHub</h2>
<p> <p>
When I decided to When I decided to
<a [routerLink]="['/en', 'blog', '2021', '07', 'hello-ghostfolio']" <a href="../en/blog/2021/07/hello-ghostfolio">publish</a>
>publish</a
>
the project as the project as
<a href="https://github.com/ghostfolio/ghostfolio" <a href="https://github.com/ghostfolio/ghostfolio"
>open source software</a >open source software</a

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

@ -7,8 +7,8 @@
<div class="mb-3 text-muted"><small>2022-07-23</small></div> <div class="mb-3 text-muted"><small>2022-07-23</small></div>
<img <img
alt="Ghostfolio meets Internet Identity Teaser" alt="Ghostfolio meets Internet Identity Teaser"
class="w-100" class="rounded w-100"
src="./assets/images/blog/ghostfolio-meets-internet-identity.png" src="../assets/images/blog/ghostfolio-meets-internet-identity.png"
title="Ghostfolio meets Internet Identity" title="Ghostfolio meets Internet Identity"
/> />
</div> </div>

20
apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-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 { FiveHundredStarsOnGitHubPageComponent } from './500-stars-on-github-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: FiveHundredStarsOnGitHubPageComponent,
path: '',
title: '500 Stars on GitHub'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class FiveHundredStarsOnGitHubRoutingModule {}

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

@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
host: { class: 'page' },
selector: 'gf-500-stars-on-github-page',
styleUrls: ['./500-stars-on-github-page.scss'],
templateUrl: './500-stars-on-github-page.html'
})
export class FiveHundredStarsOnGitHubPageComponent {}

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

@ -0,0 +1,195 @@
<div class="blog container">
<div class="row">
<div class="col-md-8 offset-md-2">
<article>
<div class="mb-4 text-center">
<h1 class="mb-1">500 Stars</h1>
<div class="mb-3 text-muted"><small>2022-08-18</small></div>
<img
alt="500 Stars on GitHub Teaser"
class="rounded w-100"
src="../assets/images/blog/500-stars-on-github.jpg"
title="500 Stars on GitHub"
/>
</div>
<section class="mb-4">
<p>
<a href="https://ghostfol.io">Ghostfolio</a>, the web-based personal
finance management software, is celebrating 500 stars on
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>. This
is a major milestone for this open source project and a good time
for another
<a href="../en/blog/2022/01/ghostfolio-first-months-in-open-source"
>recap</a
>.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Growing Community</h2>
<p>
The Ghostfolio community is growing on various platforms and has
recently passed 100 members on
<a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
>Slack</a
>
as well as 100 followers on
<a href="https://twitter.com/ghostfolio_">Twitter</a>. If you have
not joined yet, this is a good time to make sure you do not miss out
on any future updates.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Message Queue: Asynchronous Processing</h2>
<p>
Overall
<a href="https://status.ghostfol.io">stability and robustness</a>
has increased significantly since the introduction of a
<a href="https://github.com/OptimalBits/bull">message queue</a>. The
workers of this robust queue system process jobs, namely gathering
historical market data, asynchronously in the background to not
bother the main service.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Ready for Web 3.0</h2>
<p>
The
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
>recent integration of Internet Identity</a
>, a blockchain authentication system, makes Ghostfolio ready for
Web3. This third iteration of the World Wide Web is the vision of a
new and better Internet based on decentralized blockchains to give
power back to the users. <i>Internet Identity</i> created by the
<a href="https://dfinity.org">Dfinity Foundation</a> enables you to
sign in securely and anonymously to Ghostfolio without an email
address, username, or a password. All you need is your device with
built-in biometric authentication.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Break-even Point</h2>
<p>
Despite the complicated
<a [routerLink]="['/markets']">economic situation</a> at this time,
the goal set at the beginning of the year to build a sustainable
business and reach break-even with the SaaS offering (<a
[routerLink]="['/markets']"
>Ghostfolio Premium</a
>) has been achieved. We will continue to leverage the revenue to
further improve the fully managed cloud offering for our paying
customers. A new goal we have set for ourselves is to become
profitable.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Outlook</h2>
<p>
Besides all the positive accomplishments during the last months,
there is still a lot of room for improvement. It would be great to
onboard more contributors who are actively involved in software
engineering to realize the full potential of open source software.
If you are a web developer and interested in personal finance,
please get in touch by email via
<a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a> or on Twitter
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a>. We are
happy to discuss ideas.
</p>
<p>
We would like to say thank you for all your feedback and support
since the beginning of this project.
</p>
<p>
Off to the next 500 stars!<br />
Thomas from Ghostfolio
</p>
</section>
<section class="mb-4">
<ul class="list-inline">
<li class="list-inline-item">
<span class="badge badge-light">Blockchain</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">BuildInPublic</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Cloud</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Community</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Finance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Fintech</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Future</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Goal</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Internet Identity</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Investment</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Message Queue</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">OpenSaaS</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Open Source</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">OSS</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Personal Finance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Planning</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Portfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Portfolio Tracker</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Progress</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">SaaS</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Software</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">User Feedback</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Wealth</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Wealth Management</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Web3</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Web 3.0</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Worker</span>
</li>
</ul>
</section>
</article>
</div>
</div>
</div>

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

@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { FiveHundredStarsOnGitHubRoutingModule } from './500-stars-on-github-page-routing.module';
import { FiveHundredStarsOnGitHubPageComponent } from './500-stars-on-github-page.component';
@NgModule({
declarations: [FiveHundredStarsOnGitHubPageComponent],
imports: [CommonModule, FiveHundredStarsOnGitHubRoutingModule, RouterModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class FiveHundredStarsOnGitHubPageModule {}

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

@ -0,0 +1,3 @@
:host {
display: block;
}

2
apps/client/src/app/pages/blog/blog-page-routing.module.ts

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: BlogPageComponent, component: BlogPageComponent,
path: '', path: '',
title: 'Blog' title: $localize`Blog`
} }
]; ];

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

@ -8,7 +8,31 @@
<div class="flex-nowrap no-gutters row"> <div class="flex-nowrap no-gutters row">
<a <a
class="d-flex w-100" class="d-flex w-100"
[routerLink]="['/en', 'blog', '2022', '07', 'ghostfolio-meets-internet-identity']" href="../en/blog/2022/08/500-stars-on-github"
>
<div class="flex-grow-1">
<div class="h6 m-0 text-truncate">500 Stars on GitHub</div>
<div class="d-flex text-muted">2022-08-18</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
></ion-icon>
</div>
</a>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
> >
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="h6 m-0 text-truncate"> <div class="h6 m-0 text-truncate">
@ -34,7 +58,7 @@
<div class="flex-nowrap no-gutters row"> <div class="flex-nowrap no-gutters row">
<a <a
class="d-flex w-100" class="d-flex w-100"
[routerLink]="['/en', 'blog', '2022', '07', 'how-do-i-get-my-finances-in-order']" href="../en/blog/2022/07/how-do-i-get-my-finances-in-order"
> >
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="h6 m-0 text-truncate"> <div class="h6 m-0 text-truncate">
@ -60,7 +84,7 @@
<div class="flex-nowrap no-gutters row"> <div class="flex-nowrap no-gutters row">
<a <a
class="d-flex w-100" class="d-flex w-100"
[routerLink]="['/en', 'blog', '2022', '01', 'ghostfolio-first-months-in-open-source']" href="'../en/blog/2022/01/ghostfolio-first-months-in-open-source"
> >
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="h6 m-0 text-truncate"> <div class="h6 m-0 text-truncate">
@ -86,7 +110,7 @@
<div class="flex-nowrap no-gutters row"> <div class="flex-nowrap no-gutters row">
<a <a
class="d-flex w-100" class="d-flex w-100"
[routerLink]="['/en', 'blog', '2021', '07', 'hello-ghostfolio']" href="../en/blog/2021/07/hello-ghostfolio"
> >
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="h6 m-0 text-truncate">Hello Ghostfolio</div> <div class="h6 m-0 text-truncate">Hello Ghostfolio</div>
@ -110,7 +134,7 @@
<div class="flex-nowrap no-gutters row"> <div class="flex-nowrap no-gutters row">
<a <a
class="d-flex w-100" class="d-flex w-100"
[routerLink]="['/de', 'blog', '2021', '07', 'hallo-ghostfolio']" href="../de/blog/2021/07/hallo-ghostfolio"
> >
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="h6 m-0 text-truncate">Hallo Ghostfolio</div> <div class="h6 m-0 text-truncate">Hallo Ghostfolio</div>

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

@ -28,7 +28,7 @@ export class DemoPageComponent implements OnDestroy {
if (hasToken) { if (hasToken) {
alert( alert(
'As you are already logged in, you cannot access the demo account.' $localize`As you are already logged in, you cannot access the demo account.`
); );
} else { } else {
this.tokenStorageService.saveToken(this.info.demoAuthToken, true); this.tokenStorageService.saveToken(this.info.demoAuthToken, true);

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

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: FaqPageComponent, component: FaqPageComponent,
path: '', path: '',
title: 'FAQ' title: $localize`FAQ`
} }
]; ];

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

@ -1,61 +1,57 @@
<div class="container"> <div class="container">
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<h3 class="mb-3 text-center" i18n>Frequently Asked Questions (FAQ)</h3> <h3 class="mb-3 text-center">Frequently Asked Questions (FAQ)</h3>
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-title i18n>What is Ghostfolio?</mat-card-title> <mat-card-title>What is Ghostfolio?</mat-card-title>
<mat-card-content i18n> <mat-card-content>
Ghostfolio is a lightweight, open source wealth management application Ghostfolio is a lightweight, open source wealth management application
for individuals to keep track of their net worth. The software for individuals to keep track of their net worth. The software
empowers you to make solid, data-driven investment decisions. empowers you to make solid, data-driven investment decisions.
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-title i18n <mat-card-title
>What assets can I track with Ghostfolio?</mat-card-title >What assets can I track with Ghostfolio?</mat-card-title
> >
<mat-card-content i18n> <mat-card-content>
With Ghostfolio, you can keep track of various assets like stocks, With Ghostfolio, you can keep track of various assets like stocks,
ETFs or cryptocurrencies. ETFs or cryptocurrencies.
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-title i18n <mat-card-title>What else is included in Ghostfolio?</mat-card-title>
>What else is included in Ghostfolio?</mat-card-title <mat-card-content>
>
<mat-card-content i18n>
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]="['/features']">here</a>.
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-title i18n>How do I start?</mat-card-title> <mat-card-title>How do I start?</mat-card-title>
<mat-card-content i18n> <mat-card-content>
You can sign up via the “<a [routerLink]="['/register']" You can sign up via the “<a [routerLink]="['/register']"
>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
<a <a href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
[routerLink]="['/en', 'blog', '2022', '07', 'ghostfolio-meets-internet-identity']"
>Internet Identity</a >Internet Identity</a
> >
or <i>Google Sign</i>. We will guide you to set up your portfolio. or <i>Google Sign</i>. We will guide you to set up your portfolio.
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-title i18n>Can I use Ghostfolio anonymously?</mat-card-title> <mat-card-title>Can I use Ghostfolio anonymously?</mat-card-title>
<mat-card-content i18n> <mat-card-content>
Yes, the authentication systems (via security token or Yes, the authentication systems (via security token or
<a <a href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
[routerLink]="['/en', 'blog', '2022', '07', 'ghostfolio-meets-internet-identity']"
>Internet Identity</a >Internet Identity</a
>) enable you to sign in securely and anonymously to Ghostfolio. There >) enable you to sign in securely and anonymously to Ghostfolio. There
is no need for an email address, phone number, or a username. is no need for an email address, phone number, or a username.
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-title i18n>How can Ghostfolio be free?</mat-card-title> <mat-card-title>How can Ghostfolio be free?</mat-card-title>
<mat-card-content i18n <mat-card-content
>This project is driven by the efforts of contributors from around the >This project is driven by the efforts of contributors from around the
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
@ -66,16 +62,16 @@
> >
</mat-card> </mat-card>
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-title i18n>Is it really free?</mat-card-title> <mat-card-title>Is it really free?</mat-card-title>
<mat-card-content i18n <mat-card-content
>Yes, it is! Our >Yes, it is! Our
<a [routerLink]="['/pricing']">pricing page</a> details everything you <a [routerLink]="['/pricing']">pricing page</a> details everything you
get for free.</mat-card-content get for free.</mat-card-content
> >
</mat-card> </mat-card>
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-title i18n>What is Ghostfolio Premium?</mat-card-title> <mat-card-title>What is Ghostfolio Premium?</mat-card-title>
<mat-card-content i18n <mat-card-content
><a [routerLink]="['/pricing']">Ghostfolio Premium</a> is a fully ><a [routerLink]="['/pricing']">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. It is the Open Source is used to cover the hosting infrastructure. It is the Open Source
@ -83,8 +79,8 @@
> >
</mat-card> </mat-card>
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-title i18n>Can I start with a trial version?</mat-card-title> <mat-card-title>Can I start with a trial version?</mat-card-title>
<mat-card-content i18n <mat-card-content
>Yes, you can try >Yes, you can try
<a [routerLink]="['/pricing']">Ghostfolio Premium</a> by signing up <a [routerLink]="['/pricing']">Ghostfolio Premium</a> by signing up
for Ghostfolio and applying for a trial (see “My Ghostfolio”). It’s for Ghostfolio and applying for a trial (see “My Ghostfolio”). It’s
@ -93,8 +89,8 @@
> >
</mat-card> </mat-card>
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-title i18n>Which devices are supported?</mat-card-title> <mat-card-title>Which devices are supported?</mat-card-title>
<mat-card-content i18n <mat-card-content
>Ghostfolio works in every modern web browser on smartphones, tablets >Ghostfolio works in every modern web browser on smartphones, tablets
and desktop computers (where you have even more analysis options and and desktop computers (where you have even more analysis options and
statistics). For Android users, there is a dedicated Ghostfolio app statistics). For Android users, there is a dedicated Ghostfolio app
@ -106,10 +102,10 @@
> >
</mat-card> </mat-card>
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-title i18n <mat-card-title
>Ghostfolio sounds cool, how can I get involved?</mat-card-title >Ghostfolio sounds cool, how can I get involved?</mat-card-title
> >
<mat-card-content i18n <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]="['/pricing']">Ghostfolio Premium</a> subscription to
finance the hosting, a positive rating in the finance the hosting, a positive rating in the
@ -126,8 +122,8 @@
> >
</mat-card> </mat-card>
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-title i18n>Got any other questions?</mat-card-title> <mat-card-title>Got any other questions?</mat-card-title>
<mat-card-content i18n <mat-card-content
>Join the Ghostfolio >Join the 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"

2
apps/client/src/app/pages/features/features-page-routing.module.ts

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: FeaturesPageComponent, component: FeaturesPageComponent,
path: '', path: '',
title: 'Features' title: $localize`Features`
} }
]; ];

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

@ -1,9 +1,7 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3 class="d-flex justify-content-center mb-3 text-center" i18n> <h3 class="d-flex justify-content-center mb-3 text-center">Features</h3>
Features
</h3>
<div class="mb-4"> <div class="mb-4">
<p> <p>
Check out the numerous features of <strong>Ghostfolio</strong> to Check out the numerous features of <strong>Ghostfolio</strong> to
@ -14,7 +12,7 @@
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 i18n>Stocks</h4> <h4>Stocks</h4>
<p class="m-0">Keep track of your stock purchases and sales.</p> <p class="m-0">Keep track of your stock purchases and sales.</p>
</div> </div>
</mat-card> </mat-card>
@ -22,7 +20,7 @@
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 i18n>ETFs</h4> <h4>ETFs</h4>
<p class="m-0"> <p class="m-0">
Are you into ETFs (Exchange Traded Funds)? Track your ETF Are you into ETFs (Exchange Traded Funds)? Track your ETF
investments. investments.
@ -33,7 +31,7 @@
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 i18n>Bonds</h4> <h4>Bonds</h4>
<p class="m-0"> <p class="m-0">
Manage your investment in bonds and other assets with fixed Manage your investment in bonds and other assets with fixed
income. income.
@ -44,7 +42,7 @@
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 i18n>Cryptocurrencies</h4> <h4>Cryptocurrencies</h4>
<p class="m-0"> <p class="m-0">
Keep track of your Bitcoin and Altcoin holdings. Keep track of your Bitcoin and Altcoin holdings.
</p> </p>
@ -54,7 +52,7 @@
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 i18n>Dividend</h4> <h4>Dividend</h4>
<p class="m-0"> <p class="m-0">
Are you building a dividend portfolio? Track your dividend in Are you building a dividend portfolio? Track your dividend in
Ghostfolio. Ghostfolio.
@ -65,7 +63,7 @@
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 class="align-items-center d-flex" i18n>Wealth Items</h4> <h4 class="align-items-center d-flex">Wealth Items</h4>
<p class="m-0"> <p class="m-0">
Track all your treasuries, be it your luxury watch or rare Track all your treasuries, be it your luxury watch or rare
trading cards. trading cards.
@ -76,7 +74,7 @@
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 class="align-items-center d-flex" i18n>Emergency Fund</h4> <h4 class="align-items-center d-flex">Emergency Fund</h4>
<p class="m-0"> <p class="m-0">
Define your emergency fund you are comfortable with for Define your emergency fund you are comfortable with for
difficult times. difficult times.
@ -87,7 +85,7 @@
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 class="align-items-center d-flex" i18n>Import and Export</h4> <h4 class="align-items-center d-flex">Import and Export</h4>
<p class="m-0">Import and export your investment activities.</p> <p class="m-0">Import and export your investment activities.</p>
</div> </div>
</mat-card> </mat-card>
@ -95,7 +93,7 @@
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 i18n>Multi-Accounts</h4> <h4>Multi-Accounts</h4>
<p class="m-0"> <p class="m-0">
Keep an eye on all your accounts across multiple platforms Keep an eye on all your accounts across multiple platforms
(multi-banking). (multi-banking).
@ -107,7 +105,7 @@
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 class="align-items-center d-flex"> <h4 class="align-items-center d-flex">
<span i18n>Portfolio Calculations</span> <span>Portfolio Calculations</span>
<gf-premium-indicator <gf-premium-indicator
*ngIf="hasPermissionForSubscription" *ngIf="hasPermissionForSubscription"
class="ml-1" class="ml-1"
@ -125,7 +123,7 @@
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 class="align-items-center d-flex"> <h4 class="align-items-center d-flex">
<span i18n>Portfolio Allocations</span> <span>Portfolio Allocations</span>
<gf-premium-indicator <gf-premium-indicator
*ngIf="hasPermissionForSubscription" *ngIf="hasPermissionForSubscription"
class="ml-1" class="ml-1"
@ -141,7 +139,7 @@
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 class="align-items-center d-flex" i18n>Dark Mode</h4> <h4 class="align-items-center d-flex">Dark Mode</h4>
<p class="m-0"> <p class="m-0">
Ghostfolio automatically switches to a dark color theme based on Ghostfolio automatically switches to a dark color theme based on
your operating system's preferences. your operating system's preferences.
@ -152,7 +150,7 @@
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 class="align-items-center d-flex" i18n>Zen Mode</h4> <h4 class="align-items-center d-flex">Zen Mode</h4>
<p class="m-0"> <p class="m-0">
Keep calm and activate Zen Mode if the markets are going crazy. Keep calm and activate Zen Mode if the markets are going crazy.
</p> </p>
@ -166,7 +164,7 @@
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 class="align-items-center d-flex"> <h4 class="align-items-center d-flex">
<span i18n>Market Mood</span> <span>Market Mood</span>
<gf-premium-indicator class="ml-1"></gf-premium-indicator> <gf-premium-indicator class="ml-1"></gf-premium-indicator>
</h4> </h4>
<p class="m-0"> <p class="m-0">
@ -181,7 +179,7 @@
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 class="align-items-center d-flex"> <h4 class="align-items-center d-flex">
<span i18n>Static Analysis</span> <span>Static Analysis</span>
<gf-premium-indicator <gf-premium-indicator
*ngIf="hasPermissionForSubscription" *ngIf="hasPermissionForSubscription"
class="ml-1" class="ml-1"
@ -197,7 +195,7 @@
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 i18n>Community</h4> <h4>Community</h4>
<p class="m-0"> <p class="m-0">
Join the Ghostfolio Join the Ghostfolio
<a <a
@ -214,7 +212,7 @@
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 i18n>Open Source Software</h4> <h4>Open Source Software</h4>
<p class="m-0"> <p class="m-0">
The source code is fully available as The source code is fully available as
<a <a
@ -232,7 +230,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" mat-flat-button [routerLink]="['/register']">
Get Started Get Started
</a> </a>
</div> </div>

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

@ -20,7 +20,7 @@ const routes: Routes = [
], ],
component: HomePageComponent, component: HomePageComponent,
path: '', path: '',
title: 'Overview' title: $localize`Overview`
} }
]; ];

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

@ -1,7 +1,7 @@
<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 my-5" i18n> <h1 class="font-weight-bold intro my-5">
Manage your wealth like a boss Manage your wealth like a boss
</h1> </h1>
<div> <div>
@ -13,7 +13,7 @@
<img <img
alt="Ghostfol.io Trailer" alt="Ghostfol.io Trailer"
class="rounded video" class="rounded video"
src="./assets/images/video-preview.jpg" src="../assets/images/video-preview.jpg"
style="max-width: 100%; width: 40rem" style="max-width: 100%; width: 40rem"
/> />
</a> </a>
@ -29,19 +29,13 @@
<a <a
class="d-inline-block" class="d-inline-block"
color="primary" color="primary"
i18n
mat-flat-button mat-flat-button
[routerLink]="['/register']" [routerLink]="['/register']"
> >
Get Started Get Started
</a> </a>
<div class="d-inline-block mx-3 text-muted" i18n>or</div> <div class="d-inline-block mx-3 text-muted">or</div>
<a <a class="d-inline-block" mat-stroked-button [routerLink]="['/demo']">
class="d-inline-block"
i18n
mat-stroked-button
[routerLink]="['/demo']"
>
Live Demo Live Demo
</a> </a>
</div> </div>
@ -107,7 +101,7 @@
</li> </li>
</ul> </ul>
<div class="mt-4 text-center"> <div class="mt-4 text-center">
<a [routerLink]="['/about']" i18n mat-stroked-button <a [routerLink]="['/about']" mat-stroked-button
>Learn more about Ghostfolio</a >Learn more about Ghostfolio</a
> >
</div> </div>
@ -162,16 +156,11 @@
Join now or check out the example account Join now or check out the example account
</p> </p>
<div class="py-2 text-center"> <div class="py-2 text-center">
<a color="primary" i18n mat-flat-button [routerLink]="['/register']"> <a color="primary" mat-flat-button [routerLink]="['/register']">
Get Started Get Started
</a> </a>
<div class="d-inline-block mx-3 text-muted" i18n>or</div> <div class="d-inline-block mx-3 text-muted">or</div>
<a <a class="d-inline-block" mat-stroked-button [routerLink]="['/demo']">
class="d-inline-block"
i18n
mat-stroked-button
[routerLink]="['/demo']"
>
Live Demo Live Demo
</a> </a>
</div> </div>
@ -183,7 +172,7 @@
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"
title="Get Ghostfolio on Google Play" title="Get Ghostfolio on Google Play"
> >
<img alt="Google Play Badge" src="assets/badge-en-google-play.png" /> <img alt="Google Play Badge" src="../assets/badge-en-google-play.png" />
</a> </a>
</div> </div>
</div> </div>

2
apps/client/src/app/pages/markets/markets-page-routing.module.ts

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: MarketsPageComponent, component: MarketsPageComponent,
path: '', path: '',
title: 'Markets' title: $localize`Markets`
} }
]; ];

2
apps/client/src/app/pages/portfolio/allocations/allocations-page-routing.module.ts

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: AllocationsPageComponent, component: AllocationsPageComponent,
path: '', path: '',
title: 'Allocations' title: $localize`Allocations`
} }
]; ];

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

@ -54,8 +54,8 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
}; };
public period = 'current'; public period = 'current';
public periodOptions: ToggleOption[] = [ public periodOptions: ToggleOption[] = [
{ label: 'Initial', value: 'original' }, { label: $localize`Initial`, value: 'original' },
{ label: 'Current', value: 'current' } { label: $localize`Current`, value: 'current' }
]; ];
public placeholder = ''; public placeholder = '';
public portfolioDetails: PortfolioDetails; public portfolioDetails: PortfolioDetails;

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

@ -94,8 +94,8 @@
<div class="col-md-12 allocations-by-symbol"> <div class="col-md-12 allocations-by-symbol">
<mat-card class="mb-3"> <mat-card 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" i18n> <mat-card-title class="align-items-center d-flex text-truncate">
By Holding</mat-card-title <ng-container i18n>By Holding</ng-container></mat-card-title
> >
<gf-toggle <gf-toggle
[defaultValue]="period" [defaultValue]="period"
@ -233,27 +233,30 @@
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
<gf-value <gf-value
label="Developed Markets" i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.developedMarkets?.value" [value]="markets?.developedMarkets?.value"
></gf-value> >Developed Markets</gf-value
>
</div> </div>
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
<gf-value <gf-value
label="Emerging Markets" i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.emergingMarkets?.value" [value]="markets?.emergingMarkets?.value"
></gf-value> >Emerging Markets</gf-value
>
</div> </div>
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
<gf-value <gf-value
label="Other Markets" i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.otherMarkets?.value" [value]="markets?.otherMarkets?.value"
></gf-value> >Other Markets</gf-value
>
</div> </div>
</div> </div>
</mat-card-content> </mat-card-content>

2
apps/client/src/app/pages/portfolio/analysis/analysis-page-routing.module.ts

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: AnalysisPageComponent, component: AnalysisPageComponent,
path: '', path: '',
title: 'Analysis' title: $localize`Analysis`
} }
]; ];

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

@ -26,8 +26,8 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public investmentsByMonth: InvestmentItem[]; public investmentsByMonth: InvestmentItem[];
public mode: GroupBy; public mode: GroupBy;
public modeOptions: ToggleOption[] = [ public modeOptions: ToggleOption[] = [
{ label: 'Monthly', value: 'month' }, { label: $localize`Monthly`, value: 'month' },
{ label: 'Accumulating', value: undefined } { label: $localize`Accumulating`, value: undefined }
]; ];
public top3: Position[]; public top3: Position[];
public user: User; public user: User;

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

@ -52,7 +52,7 @@
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-header> <mat-card-header>
<mat-card-title class="align-items-center d-flex" i18n <mat-card-title class="align-items-center d-flex" i18n
>Top 3</mat-card-title >Top</mat-card-title
> >
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
@ -88,7 +88,7 @@
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-header> <mat-card-header>
<mat-card-title class="align-items-center d-flex" i18n <mat-card-title class="align-items-center d-flex" i18n
>Bottom 3</mat-card-title >Bottom</mat-card-title
> >
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>

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

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: FirePageComponent, component: FirePageComponent,
path: '', path: '',
title: 'FIRE' title: $localize`FIRE`
} }
]; ];

2
apps/client/src/app/pages/portfolio/holdings/holdings-page-routing.module.ts

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: HoldingsPageComponent, component: HoldingsPageComponent,
path: '', path: '',
title: 'Holdings' title: $localize`Holdings`
} }
]; ];

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

@ -91,7 +91,7 @@
<div class="col-xs-12 col-md-6 mb-3"> <div class="col-xs-12 col-md-6 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<h4 class="align-items-center d-flex"> <h4 class="align-items-center d-flex">
<span i18n>X-ray</span> <span>X-ray</span>
<gf-premium-indicator <gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"

10
apps/client/src/app/pages/portfolio/report/report-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="align-items-center d-flex justify-content-center mb-3" i18n> <h3 class="align-items-center d-flex justify-content-center mb-3">
X-ray X-ray
</h3> </h3>
<p class="mb-4" i18n> <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.
<span class="d-none" <span class="d-none"
@ -14,21 +14,21 @@
> >
</p> </p>
<div class="mb-4"> <div class="mb-4">
<h4 class="m-0" i18n>Currency Cluster Risks</h4> <h4 class="m-0">Currency Cluster Risks</h4>
<gf-rules <gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder" [hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
[rules]="currencyClusterRiskRules" [rules]="currencyClusterRiskRules"
></gf-rules> ></gf-rules>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<h4 class="m-0" i18n>Account Cluster Risks</h4> <h4 class="m-0">Account Cluster Risks</h4>
<gf-rules <gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder" [hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
[rules]="accountClusterRiskRules" [rules]="accountClusterRiskRules"
></gf-rules> ></gf-rules>
</div> </div>
<div> <div>
<h4 class="m-0" i18n>Fees</h4> <h4 class="m-0">Fees</h4>
<gf-rules <gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder" [hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
[rules]="feeRules" [rules]="feeRules"

5
apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html

@ -166,7 +166,7 @@
[ngClass]="{ 'd-none': activityForm.controls['type']?.value !== 'ITEM' }" [ngClass]="{ 'd-none': activityForm.controls['type']?.value !== 'ITEM' }"
> >
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Asset Sub-Class</mat-label> <mat-label i18n>Asset Sub Class</mat-label>
<mat-select formControlName="assetSubClass"> <mat-select formControlName="assetSubClass">
<mat-option [value]="null"></mat-option> <mat-option [value]="null"></mat-option>
<mat-option <mat-option
@ -201,12 +201,11 @@
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button> <button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
<button <button
color="primary" color="primary"
i18n
mat-flat-button mat-flat-button
type="submit" type="submit"
[disabled]="!activityForm.valid" [disabled]="!activityForm.valid"
> >
Save <ng-container i18n>Save</ng-container>
</button> </button>
</div> </div>
</div> </div>

2
apps/client/src/app/pages/portfolio/transactions/transactions-page-routing.module.ts

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: TransactionsPageComponent, component: TransactionsPageComponent,
path: '', path: '',
title: 'Activities' title: $localize`Activities`
} }
]; ];

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

@ -188,7 +188,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
input.type = 'file'; input.type = 'file';
input.onchange = (event) => { input.onchange = (event) => {
this.snackBar.open('⏳ Importing data...'); this.snackBar.open('⏳' + $localize`Importing data...`);
// Getting the file reference // Getting the file reference
const file = (event.target as HTMLInputElement).files[0]; const file = (event.target as HTMLInputElement).files[0];
@ -334,7 +334,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
private handleImportSuccess() { private handleImportSuccess() {
this.fetchActivities(); this.fetchActivities();
this.snackBar.open('✅ Import has been completed', undefined, { this.snackBar.open('✅' + $localize`Import has been completed`, undefined, {
duration: 3000 duration: 3000
}); });
} }

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

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: PricingPageComponent, component: PricingPageComponent,
path: '', path: '',
title: 'Pricing' title: $localize`Pricing`
} }
]; ];

14
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-flex justify-content-center mb-3 text-center" i18n> <h3 class="d-flex justify-content-center mb-3 text-center">
Pricing Plans Pricing Plans
</h3> </h3>
<div class="mb-4"> <div class="mb-4">
@ -20,7 +20,7 @@
<div class="col-xs-12 col-md-4 mb-3"> <div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100"> <mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 i18n>Open Source</h4> <h4>Open Source</h4>
<p> <p>
For tech-savvy investors who prefer to run For tech-savvy investors who prefer to run
<strong>Ghostfolio</strong> on their own infrastructure. <strong>Ghostfolio</strong> on their own infrastructure.
@ -73,7 +73,7 @@
[ngClass]="{ 'active': user?.subscription?.type === 'Basic' }" [ngClass]="{ 'active': user?.subscription?.type === 'Basic' }"
> >
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 class="align-items-center d-flex" i18n>Basic</h4> <h4 class="align-items-center d-flex">Basic</h4>
<p> <p>
For new investors who are just getting started with trading. For new investors who are just getting started with trading.
</p> </p>
@ -124,7 +124,7 @@
> >
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 class="align-items-center d-flex"> <h4 class="align-items-center d-flex">
<span i18n>Premium</span> <span>Premium</span>
<gf-premium-indicator <gf-premium-indicator
class="ml-1" class="ml-1"
[enableLink]="false" [enableLink]="false"
@ -186,7 +186,7 @@
>{{ baseCurrency }}&nbsp;<strong >{{ baseCurrency }}&nbsp;<strong
>{{ price }}</strong >{{ price }}</strong
></ng-container ></ng-container
>&nbsp;<span i18n>per year</span></span >&nbsp;<span>per year</span></span
> >
</p> </p>
</mat-card> </mat-card>
@ -196,14 +196,14 @@
</div> </div>
<div *ngIf="user?.subscription?.type === 'Basic'" class="row"> <div *ngIf="user?.subscription?.type === 'Basic'" class="row">
<div class="col mt-3 text-center"> <div class="col mt-3 text-center">
<a color="primary" i18n mat-flat-button [routerLink]="['/account']"> <a color="primary" mat-flat-button [routerLink]="['/account']">
Upgrade Plan Upgrade Plan
</a> </a>
</div> </div>
</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" mat-flat-button [routerLink]="['/register']">
Get Started Get Started
</a> </a>
<p class="text-muted"><small>It's free</small></p> <p class="text-muted"><small>It's free</small></p>

2
apps/client/src/app/pages/public/public-page-routing.module.ts

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: PublicPageComponent, component: PublicPageComponent,
path: ':id', path: ':id',
title: 'Portfolio' title: $localize`Portfolio`
} }
]; ];

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

@ -82,27 +82,30 @@
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
<gf-value <gf-value
label="Developed Markets" i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.developedMarkets?.value" [value]="markets?.developedMarkets?.value"
></gf-value> >Developed Markets</gf-value
>
</div> </div>
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
<gf-value <gf-value
label="Emerging Markets" i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.emergingMarkets?.value" [value]="markets?.emergingMarkets?.value"
></gf-value> >Emerging Markets</gf-value
>
</div> </div>
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
<gf-value <gf-value
label="Other Markets" i18n
size="large" size="large"
[isPercent]="true" [isPercent]="true"
[value]="markets?.otherMarkets?.value" [value]="markets?.otherMarkets?.value"
></gf-value> >Other Markets</gf-value
>
</div> </div>
</div> </div>
</mat-card-content> </mat-card-content>
@ -129,8 +132,8 @@
Ghostfolio empowers you to keep track of your wealth. Ghostfolio empowers you to keep track of your wealth.
</p> </p>
<div class="py-2 text-center"> <div class="py-2 text-center">
<a color="primary" href="https://ghostfol.io" i18n mat-flat-button> <a color="primary" href="https://ghostfol.io" mat-flat-button>
Get Started <ng-container i18n>Get Started</ng-container>
</a> </a>
</div> </div>
</div> </div>

2
apps/client/src/app/pages/register/register-page-routing.module.ts

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: RegisterPageComponent, component: RegisterPageComponent,
path: '', path: '',
title: 'Registration' title: $localize`Registration`
} }
]; ];

9
apps/client/src/app/pages/register/register-page.html

@ -20,12 +20,11 @@
<button <button
class="d-inline-block" class="d-inline-block"
color="primary" color="primary"
i18n
mat-flat-button mat-flat-button
[disabled]="!demoAuthToken || info?.isReadOnlyMode" [disabled]="!demoAuthToken || info?.isReadOnlyMode"
(click)="createAccount()" (click)="createAccount()"
> >
Create Account <ng-container i18n>Create Account</ng-container>
</button> </button>
<ng-container *ngIf="hasPermissionForSocialLogin"> <ng-container *ngIf="hasPermissionForSocialLogin">
<div class="my-3 text-muted" i18n>or</div> <div class="my-3 text-muted" i18n>or</div>
@ -36,15 +35,15 @@
> >
<img <img
class="mr-2" class="mr-2"
src="./assets/icons/internet-computer.svg" src="../assets/icons/internet-computer.svg"
style="height: 0.75rem" style="height: 0.75rem"
/> />
<span i18n>Continue with Internet Identity</span> <span i18n>Continue with Internet Identity</span>
</button> </button>
<a class="d-block" href="/api/v1/auth/google" mat-stroked-button <a class="d-block" href="../api/v1/auth/google" mat-stroked-button
><img ><img
class="mr-2" class="mr-2"
src="./assets/icons/google.svg" src="../assets/icons/google.svg"
style="height: 1rem" style="height: 1rem"
/><span i18n>Continue with Google</span></a /><span i18n>Continue with Google</span></a
> >

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

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
component: ResourcesPageComponent, component: ResourcesPageComponent,
path: '', path: '',
title: 'Resources' title: $localize`Resources`
} }
]; ];

3
apps/client/src/app/pages/resources/resources-page.html

@ -29,8 +29,7 @@
easier and faster in this guide. easier and faster in this guide.
</div> </div>
<div> <div>
<a <a href="../en/blog/2022/07/how-do-i-get-my-finances-in-order"
[routerLink]="['/en', 'blog', '2022', '07', 'how-do-i-get-my-finances-in-order']"
>How do I get my finances in order? →</a >How do I get my finances in order? →</a
> >
</div> </div>

2
apps/client/src/app/pages/webauthn/webauthn-page-routing.module.ts

@ -3,7 +3,7 @@ import { RouterModule, Routes } from '@angular/router';
import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component'; import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component';
const routes: Routes = [ const routes: Routes = [
{ component: WebauthnPageComponent, path: '', title: 'Login' } { component: WebauthnPageComponent, path: '', title: $localize`Sign in` }
]; ];
@NgModule({ @NgModule({

13
apps/client/src/app/pages/webauthn/webauthn-page.html

@ -14,21 +14,20 @@
*ngIf="hasError" *ngIf="hasError"
class="align-items-center col d-flex flex-column justify-content-center" class="align-items-center col d-flex flex-column justify-content-center"
> >
<h1 class="d-flex h5 justify-content-center mb-0 text-center" i18n> <h1 class="d-flex h5 justify-content-center mb-0 text-center">
Oops, authentication has failed. <ng-container i18n>Oops, authentication has failed.</ng-container>
</h1> </h1>
<button <button
class="mb-3 mt-4" class="mb-3 mt-4"
color="primary" color="primary"
i18n
mat-flat-button mat-flat-button
(click)="signIn()" (click)="signIn()"
> >
Try again <ng-container i18n>Try again</ng-container>
</button> </button>
<div class="text-muted" i18n>or</div> <div class="text-muted"><ng-container i18n>or</ng-container></div>
<button class="mt-1" i18n mat-flat-button (click)="deregisterDevice()"> <button class="mt-1" mat-flat-button (click)="deregisterDevice()">
Go back to Home Page <ng-container i18n>Go back to Home Page</ng-container>
</button> </button>
</div> </div>
</div> </div>

2
apps/client/src/app/pages/zen/zen-page-routing.module.ts

@ -16,7 +16,7 @@ const routes: Routes = [
], ],
component: ZenPageComponent, component: ZenPageComponent,
path: '', path: '',
title: 'Overview' title: $localize`Overview`
} }
]; ];

BIN
apps/client/src/assets/images/blog/500-stars-on-github.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

4
apps/client/src/assets/robots.txt

@ -1,6 +1,6 @@
User-agent: * User-agent: *
Allow: / Allow: /
Disallow: /about/privacy-policy Disallow: /en/about/privacy-policy
Disallow: /p/* Disallow: /en/p/*
Sitemap: https://ghostfol.io/sitemap.xml Sitemap: https://ghostfol.io/sitemap.xml

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

Loading…
Cancel
Save