Compare commits
55 Commits
Author | SHA1 | Date | |
---|---|---|---|
20195b2b1a | |||
7fa4e6ebd2 | |||
d8531ddfcb | |||
70d670b711 | |||
27b0663a80 | |||
874dfb0235 | |||
072db0d558 | |||
12e692429a | |||
e22b8b78b8 | |||
dc5052f7dc | |||
335553e891 | |||
d480ad1023 | |||
7320751056 | |||
108c0c13c4 | |||
053a5cc5b5 | |||
c456a8bcfe | |||
6fcecb5bc6 | |||
e4e0a7d9f0 | |||
c7173761a3 | |||
185e130d9f | |||
81245635af | |||
55182ac1af | |||
0b446a30ae | |||
c5e6602102 | |||
573038f407 | |||
dbc38e705e | |||
f127e7c61a | |||
4ccabde251 | |||
86ae88f90f | |||
69bc1d67e1 | |||
03942aecda | |||
7ec9170c0d | |||
51431a7fb2 | |||
4adda6783d | |||
d5cd4c0dea | |||
34be10d755 | |||
51f586e160 | |||
ff64a00196 | |||
148f6f8762 | |||
bf2c4d1e9e | |||
eee1f1c722 | |||
9f2a49a1c7 | |||
44058b2d7a | |||
23634f3404 | |||
f93dab6086 | |||
207859cc22 | |||
77181aaaff | |||
412039badf | |||
7619442895 | |||
61ecd66e0f | |||
81217b35ef | |||
678f1f0051 | |||
71c7e37b5a | |||
80459371f3 | |||
35f1f348a8 |
@ -2,7 +2,7 @@ language: node_js
|
||||
git:
|
||||
depth: false
|
||||
node_js:
|
||||
- 14
|
||||
- 16
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
114
CHANGELOG.md
114
CHANGELOG.md
@ -5,6 +5,120 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 1.180.0 - 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
|
||||
- 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
|
||||
|
||||
### Added
|
||||
|
||||
- Added `GHOSTFOLIO` as a default to `DATA_SOURCES`
|
||||
- Added the `AGPLv3` logo to the landing page
|
||||
|
||||
### Changed
|
||||
|
||||
- Refactored the initialization of the exchange rate service
|
||||
- Upgraded `angular` from version `14.0.2` to `14.1.0`
|
||||
- Upgraded `nestjs` from version `8.4.7` to `9.0.7`
|
||||
- Upgraded `Nx` from version `14.3.5` to `14.5.1`
|
||||
- Upgraded `prisma` from version `3.15.2` to `4.1.1`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Handled database connection errors (do not exit process)
|
||||
|
||||
## 1.176.2 - 31.07.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added page titles
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the performance of data provider requests by introducing a maximum number of symbols per request (chunk size)
|
||||
- Changed the log level settings
|
||||
- Refactored the access of the environment variables in the bootstrap function (api)
|
||||
- Upgraded `Node.js` from version `14` to `16` (`Dockerfile`)
|
||||
|
||||
### Todo
|
||||
|
||||
- Upgrade to `Node.js` 16+
|
||||
|
||||
## 1.175.0 - 29.07.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Set up a Frequently Asked Questions (FAQ) page
|
||||
- Added the savings rate to the investment timeline grouped by month
|
||||
|
||||
### Fixed
|
||||
|
||||
- Added the symbols to the activities in the account detail dialog
|
||||
|
||||
## 1.174.0 - 27.07.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Support a note for activities
|
||||
|
||||
### Todo
|
||||
|
||||
- Apply data migration (`yarn database:migrate`)
|
||||
|
||||
## 1.173.0 - 23.07.2022
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue with the currency inconsistency in the _Yahoo Finance_ service (convert from `USX` to `USD`)
|
||||
|
||||
## 1.172.0 - 23.07.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added a blog post: _Ghostfolio meets Internet Identity_
|
||||
|
||||
## 1.171.0 - 22.07.2022
|
||||
|
||||
### Added
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM node:14-alpine as builder
|
||||
FROM node:16-alpine as builder
|
||||
|
||||
# Build application and add additional files
|
||||
|
||||
@ -45,7 +45,7 @@ COPY package.json /ghostfolio/dist/apps/api
|
||||
RUN yarn database:generate-typings
|
||||
|
||||
# Image to run, copy everything needed from builder
|
||||
FROM node:14-alpine
|
||||
FROM node:16-alpine
|
||||
COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps
|
||||
WORKDIR /ghostfolio/apps/api
|
||||
EXPOSE 3333
|
||||
|
21
README.md
21
README.md
@ -12,7 +12,7 @@
|
||||
<strong>Open Source Wealth Management Software</strong>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://ghostfol.io"><strong>Ghostfol.io</strong></a> | <a href="https://ghostfol.io/demo"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/blog"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
|
||||
<a href="https://ghostfol.io"><strong>Ghostfol.io</strong></a> | <a href="https://ghostfol.io/demo"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/faq"><strong>FAQ</strong></a> | <a href="https://ghostfol.io/blog"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="#contributing">
|
||||
@ -81,6 +81,23 @@ The frontend is built with [Angular](https://angular.io) and uses [Angular Mater
|
||||
|
||||
## Self-hosting
|
||||
|
||||
### Supported Environment Variables
|
||||
|
||||
| Name | Default Value | Description |
|
||||
| ------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ACCESS_TOKEN_SALT` | | A random string used as salt for access tokens |
|
||||
| `BASE_CURRENCY` | `USD` | The base currency of the Ghostfolio application. Caution: This cannot be changed later! |
|
||||
| `DATABASE_URL` | | The database connection URL, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer` |
|
||||
| `HOST` | `0.0.0.0` | The host where the Ghostfolio application will run on |
|
||||
| `JWT_SECRET_KEY` | | A random string used for _JSON Web Tokens_ (JWT) |
|
||||
| `PORT` | `3333` | The port where the Ghostfolio application will run on |
|
||||
| `POSTGRES_DB` | | The name of the _PostgreSQL_ database |
|
||||
| `POSTGRES_PASSWORD` | | The password of the _PostgreSQL_ database |
|
||||
| `POSTGRES_USER` | | The user of the _PostgreSQL_ database |
|
||||
| `REDIS_HOST` | `localhost` | The host where _Redis_ is running |
|
||||
| `REDIS_PASSWORD` | | The password of _Redis_ |
|
||||
| `REDIS_PORT` | `6379` | The port where _Redis_ is running |
|
||||
|
||||
### Run with Docker Compose
|
||||
|
||||
#### Prerequisites
|
||||
@ -145,7 +162,7 @@ Please follow the instructions of the Ghostfolio [Unraid Community App](https://
|
||||
### Prerequisites
|
||||
|
||||
- [Docker](https://www.docker.com/products/docker-desktop)
|
||||
- [Node.js](https://nodejs.org/en/download) (version 14+)
|
||||
- [Node.js](https://nodejs.org/en/download) (version 16+)
|
||||
- [Yarn](https://yarnpkg.com/en/docs/install)
|
||||
- A local copy of this Git repository (clone)
|
||||
|
||||
|
43
angular.json
43
angular.json
@ -77,41 +77,45 @@
|
||||
"polyfills": "apps/client/src/polyfills.ts",
|
||||
"tsConfig": "apps/client/tsconfig.app.json",
|
||||
"assets": [
|
||||
"apps/client/src/assets",
|
||||
{
|
||||
"glob": "assetlinks.json",
|
||||
"input": "apps/client/src/assets",
|
||||
"output": "./.well-known"
|
||||
"output": "./../.well-known"
|
||||
},
|
||||
{
|
||||
"glob": "CHANGELOG.md",
|
||||
"input": "",
|
||||
"output": "./assets"
|
||||
"output": "./../assets"
|
||||
},
|
||||
{
|
||||
"glob": "LICENSE",
|
||||
"input": "",
|
||||
"output": "./assets"
|
||||
"output": "./../assets"
|
||||
},
|
||||
{
|
||||
"glob": "robots.txt",
|
||||
"input": "apps/client/src/assets",
|
||||
"output": "./"
|
||||
"output": "./../"
|
||||
},
|
||||
{
|
||||
"glob": "sitemap.xml",
|
||||
"input": "apps/client/src/assets",
|
||||
"output": "./"
|
||||
"output": "./../"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/ionicons/dist/ionicons",
|
||||
"output": "./ionicons"
|
||||
"output": "./../ionicons"
|
||||
},
|
||||
{
|
||||
"glob": "**/*.js",
|
||||
"input": "node_modules/ionicons/dist/",
|
||||
"output": "./"
|
||||
"output": "./../"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/client/src/assets",
|
||||
"output": "./../assets/"
|
||||
}
|
||||
],
|
||||
"styles": ["apps/client/src/styles.scss"],
|
||||
@ -124,6 +128,10 @@
|
||||
"namedChunks": true
|
||||
},
|
||||
"configurations": {
|
||||
"development-en": {
|
||||
"baseHref": "/en/",
|
||||
"localize": ["en"]
|
||||
},
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
@ -162,15 +170,21 @@
|
||||
"proxyConfig": "apps/client/proxy.conf.json"
|
||||
},
|
||||
"configurations": {
|
||||
"development-en": {
|
||||
"browserTarget": "client:build:development-en"
|
||||
},
|
||||
"production": {
|
||||
"browserTarget": "client:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"builder": "ng-extract-i18n-merge:ng-extract-i18n-merge",
|
||||
"options": {
|
||||
"browserTarget": "client:build"
|
||||
"browserTarget": "client:build",
|
||||
"includeContext": true,
|
||||
"outputPath": "src/locales",
|
||||
"targetFiles": ["messages.de.xlf"]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
@ -188,6 +202,15 @@
|
||||
"outputs": ["coverage/apps/client"]
|
||||
}
|
||||
},
|
||||
"i18n": {
|
||||
"locales": {
|
||||
"de": {
|
||||
"baseHref": "/de/",
|
||||
"translation": "apps/client/src/locales/messages.de.xlf"
|
||||
}
|
||||
},
|
||||
"sourceLocale": "en"
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
"client-e2e": {
|
||||
|
@ -1,6 +1,17 @@
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { Controller } from '@nestjs/common';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
public constructor() {}
|
||||
public constructor(
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService
|
||||
) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private async initialize() {
|
||||
try {
|
||||
await this.exchangeRateDataService.initialize();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-d
|
||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||
import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module';
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
@ -23,6 +23,7 @@ import { AuthModule } from './auth/auth.module';
|
||||
import { BenchmarkModule } from './benchmark/benchmark.module';
|
||||
import { CacheModule } from './cache/cache.module';
|
||||
import { ExportModule } from './export/export.module';
|
||||
import { FrontendMiddleware } from './frontend.middleware';
|
||||
import { ImportModule } from './import/import.module';
|
||||
import { InfoModule } from './info/info.module';
|
||||
import { OrderModule } from './order/order.module';
|
||||
@ -82,4 +83,10 @@ import { UserModule } from './user/user.module';
|
||||
controllers: [AppController],
|
||||
providers: [CronService]
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer
|
||||
.apply(FrontendMiddleware)
|
||||
.forRoutes({ path: '*', method: RequestMethod.ALL });
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
||||
import { OAuthResponse } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
Body,
|
||||
@ -62,9 +63,17 @@ export class AuthController {
|
||||
const jwt: string = req.user.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 {
|
||||
res.redirect(`${this.configurationService.get('ROOT_URL')}/auth`);
|
||||
res.redirect(
|
||||
`${this.configurationService.get(
|
||||
'ROOT_URL'
|
||||
)}/${DEFAULT_LANGUAGE_CODE}/auth`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ export class ExportService {
|
||||
orderBy: { date: 'desc' },
|
||||
select: {
|
||||
accountId: true,
|
||||
comment: true,
|
||||
date: true,
|
||||
fee: true,
|
||||
id: true,
|
||||
@ -40,6 +41,7 @@ export class ExportService {
|
||||
activities: activities.map(
|
||||
({
|
||||
accountId,
|
||||
comment,
|
||||
date,
|
||||
fee,
|
||||
id,
|
||||
@ -50,6 +52,7 @@ export class ExportService {
|
||||
}) => {
|
||||
return {
|
||||
accountId,
|
||||
comment,
|
||||
fee,
|
||||
id,
|
||||
quantity,
|
||||
|
53
apps/api/src/app/frontend.middleware.ts
Normal file
53
apps/api/src/app/frontend.middleware.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import * as path from 'path';
|
||||
|
||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
||||
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class FrontendMiddleware implements NestMiddleware {
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
if (req.url.includes('cover.png')) {
|
||||
Logger.log(`Referer: ${req.headers.referer}`, 'FrontendMiddleware');
|
||||
|
||||
// Resolve feature graphic for blog post
|
||||
if (req.headers.referer?.includes('500-stars-on-github')) {
|
||||
res.sendFile(
|
||||
path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'client',
|
||||
'assets',
|
||||
'images',
|
||||
'blog',
|
||||
'500-stars-on-github.jpg'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Skip
|
||||
next();
|
||||
}
|
||||
} else if (req.path.startsWith('/api/') || this.isFileRequest(req.url)) {
|
||||
// Skip
|
||||
next();
|
||||
} else if (req.path.startsWith('/de/')) {
|
||||
res.sendFile(this.getPathOfIndexHtmlFile('de'));
|
||||
} else {
|
||||
res.sendFile(this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE));
|
||||
}
|
||||
}
|
||||
|
||||
private getPathOfIndexHtmlFile(aLocale: string) {
|
||||
return path.join(__dirname, '..', 'client', aLocale, 'index.html');
|
||||
}
|
||||
|
||||
private isFileRequest(filename: string) {
|
||||
if (filename === '/assets/LICENSE') {
|
||||
return true;
|
||||
} else if (filename.includes('auth/ey')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return filename.split('.').pop() !== filename;
|
||||
}
|
||||
}
|
@ -48,6 +48,7 @@ export class ImportService {
|
||||
|
||||
for (const {
|
||||
accountId,
|
||||
comment,
|
||||
currency,
|
||||
dataSource,
|
||||
date,
|
||||
@ -58,6 +59,7 @@ export class ImportService {
|
||||
unitPrice
|
||||
} of activities) {
|
||||
await this.orderService.createOrder({
|
||||
comment,
|
||||
fee,
|
||||
quantity,
|
||||
type,
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
Tag,
|
||||
Type
|
||||
} from '@prisma/client';
|
||||
import { Transform, TransformFnParams } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
@ -13,25 +14,33 @@ import {
|
||||
IsOptional,
|
||||
IsString
|
||||
} from 'class-validator';
|
||||
import { isString } from 'lodash';
|
||||
|
||||
export class CreateOrderDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
accountId?: string;
|
||||
|
||||
@IsEnum(AssetClass, { each: true })
|
||||
@IsOptional()
|
||||
@IsEnum(AssetClass, { each: true })
|
||||
assetClass?: AssetClass;
|
||||
|
||||
@IsEnum(AssetSubClass, { each: true })
|
||||
@IsOptional()
|
||||
@IsEnum(AssetSubClass, { each: true })
|
||||
assetSubClass?: AssetSubClass;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }: TransformFnParams) =>
|
||||
isString(value) ? value.trim() : value
|
||||
)
|
||||
comment?: string;
|
||||
|
||||
@IsString()
|
||||
currency: string;
|
||||
|
||||
@IsEnum(DataSource, { each: true })
|
||||
@IsOptional()
|
||||
@IsEnum(DataSource, { each: true })
|
||||
dataSource?: DataSource;
|
||||
|
||||
@IsISO8601()
|
||||
|
@ -143,6 +143,11 @@ export class OrderService {
|
||||
delete data.accountId;
|
||||
delete data.assetClass;
|
||||
delete data.assetSubClass;
|
||||
|
||||
if (!data.comment) {
|
||||
delete data.comment;
|
||||
}
|
||||
|
||||
delete data.currency;
|
||||
delete data.dataSource;
|
||||
delete data.symbol;
|
||||
@ -225,9 +230,10 @@ export class OrderService {
|
||||
})
|
||||
},
|
||||
{
|
||||
SymbolProfileOverrides: {
|
||||
is: null
|
||||
}
|
||||
OR: [
|
||||
{ SymbolProfileOverrides: { is: null } },
|
||||
{ SymbolProfileOverrides: { assetClass: null } }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -316,6 +322,10 @@ export class OrderService {
|
||||
delete data.Account;
|
||||
}
|
||||
|
||||
if (!data.comment) {
|
||||
data.comment = null;
|
||||
}
|
||||
|
||||
const tags = data.tags ?? [];
|
||||
|
||||
let isDraft = false;
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
Tag,
|
||||
Type
|
||||
} from '@prisma/client';
|
||||
import { Transform, TransformFnParams } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
@ -13,6 +14,7 @@ import {
|
||||
IsOptional,
|
||||
IsString
|
||||
} from 'class-validator';
|
||||
import { isString } from 'lodash';
|
||||
|
||||
export class UpdateOrderDto {
|
||||
@IsOptional()
|
||||
@ -27,6 +29,13 @@ export class UpdateOrderDto {
|
||||
@IsOptional()
|
||||
assetSubClass?: AssetSubClass;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }: TransformFnParams) =>
|
||||
isString(value) ? value.trim() : value
|
||||
)
|
||||
comment?: string;
|
||||
|
||||
@IsString()
|
||||
currency: string;
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.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 type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
@ -93,7 +96,11 @@ export class 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')
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.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 { Injectable, Logger } from '@nestjs/common';
|
||||
import { Subscription } from '@prisma/client';
|
||||
@ -33,7 +34,9 @@ export class SubscriptionService {
|
||||
userId: string;
|
||||
}) {
|
||||
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,
|
||||
line_items: [
|
||||
{
|
||||
|
@ -1,11 +1,24 @@
|
||||
import { Logger, ValidationPipe, VersioningType } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
const configApp = await NestFactory.create(AppModule);
|
||||
const configService = configApp.get<ConfigService>(ConfigService);
|
||||
|
||||
const NODE_ENV =
|
||||
configService.get<'development' | 'production'>('NODE_ENV') ??
|
||||
'development';
|
||||
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger:
|
||||
NODE_ENV === 'production'
|
||||
? ['error', 'log', 'warn']
|
||||
: ['debug', 'error', 'log', 'verbose', 'warn']
|
||||
});
|
||||
app.enableCors();
|
||||
app.enableVersioning({
|
||||
defaultVersion: '1',
|
||||
@ -20,11 +33,11 @@ async function bootstrap() {
|
||||
})
|
||||
);
|
||||
|
||||
const host = process.env.HOST || '0.0.0.0';
|
||||
const port = process.env.PORT || 3333;
|
||||
await app.listen(port, host, () => {
|
||||
const HOST = configService.get<string>('HOST') || '0.0.0.0';
|
||||
const PORT = configService.get<number>('PORT') || 3333;
|
||||
await app.listen(PORT, HOST, () => {
|
||||
logLogo();
|
||||
Logger.log(`Listening at http://${host}:${port}`);
|
||||
Logger.log(`Listening at http://${HOST}:${PORT}`);
|
||||
Logger.log('');
|
||||
});
|
||||
}
|
||||
|
@ -15,7 +15,9 @@ export class ConfigurationService {
|
||||
BASE_CURRENCY: str({ default: 'USD' }),
|
||||
CACHE_TTL: num({ default: 1 }),
|
||||
DATA_SOURCE_PRIMARY: str({ default: DataSource.YAHOO }),
|
||||
DATA_SOURCES: json({ default: [DataSource.YAHOO] }),
|
||||
DATA_SOURCES: json({
|
||||
default: [DataSource.GHOSTFOLIO, DataSource.YAHOO]
|
||||
}),
|
||||
ENABLE_FEATURE_BLOG: bool({ default: false }),
|
||||
ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }),
|
||||
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),
|
||||
|
@ -168,6 +168,7 @@ export class DataProviderService {
|
||||
const response: {
|
||||
[symbol: string]: IDataProviderResponse;
|
||||
} = {};
|
||||
const startTimeTotal = performance.now();
|
||||
|
||||
const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource);
|
||||
|
||||
@ -176,25 +177,59 @@ export class DataProviderService {
|
||||
for (const [dataSource, dataGatheringItems] of Object.entries(
|
||||
itemsGroupedByDataSource
|
||||
)) {
|
||||
const dataProvider = this.getDataProvider(DataSource[dataSource]);
|
||||
|
||||
const symbols = dataGatheringItems.map((dataGatheringItem) => {
|
||||
return dataGatheringItem.symbol;
|
||||
});
|
||||
|
||||
const promise = Promise.resolve(
|
||||
this.getDataProvider(DataSource[dataSource]).getQuotes(symbols)
|
||||
);
|
||||
const maximumNumberOfSymbolsPerRequest =
|
||||
dataProvider.getMaxNumberOfSymbolsPerRequest?.() ??
|
||||
Number.MAX_SAFE_INTEGER;
|
||||
for (
|
||||
let i = 0;
|
||||
i < symbols.length;
|
||||
i += maximumNumberOfSymbolsPerRequest
|
||||
) {
|
||||
const startTimeDataSource = performance.now();
|
||||
|
||||
promises.push(
|
||||
promise.then((result) => {
|
||||
for (const [symbol, dataProviderResponse] of Object.entries(result)) {
|
||||
response[symbol] = dataProviderResponse;
|
||||
}
|
||||
})
|
||||
);
|
||||
const symbolsChunk = symbols.slice(
|
||||
i,
|
||||
i + maximumNumberOfSymbolsPerRequest
|
||||
);
|
||||
|
||||
const promise = Promise.resolve(dataProvider.getQuotes(symbolsChunk));
|
||||
|
||||
promises.push(
|
||||
promise.then((result) => {
|
||||
for (const [symbol, dataProviderResponse] of Object.entries(
|
||||
result
|
||||
)) {
|
||||
response[symbol] = dataProviderResponse;
|
||||
}
|
||||
|
||||
Logger.debug(
|
||||
`Fetched ${symbolsChunk.length} quotes from ${dataSource} in ${(
|
||||
(performance.now() - startTimeDataSource) /
|
||||
1000
|
||||
).toFixed(3)} seconds`
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
Logger.debug('------------------------------------------------');
|
||||
Logger.debug(
|
||||
`Fetched ${items.length} quotes in ${(
|
||||
(performance.now() - startTimeTotal) /
|
||||
1000
|
||||
).toFixed(3)} seconds`
|
||||
);
|
||||
Logger.debug('================================================');
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
@ -81,6 +81,12 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
}
|
||||
}
|
||||
|
||||
public getMaxNumberOfSymbolsPerRequest() {
|
||||
// It is not recommended using more than 15-20 tickers per request
|
||||
// https://eodhistoricaldata.com/financial-apis/live-realtime-stocks-api
|
||||
return 20;
|
||||
}
|
||||
|
||||
public getName(): DataSource {
|
||||
return DataSource.EOD_HISTORICAL_DATA;
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ export interface DataProviderInterface {
|
||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||
}>; // TODO: Return only one symbol
|
||||
|
||||
getMaxNumberOfSymbolsPerRequest?(): number;
|
||||
|
||||
getName(): DataSource;
|
||||
|
||||
getQuotes(
|
||||
|
@ -208,6 +208,10 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
}
|
||||
}
|
||||
|
||||
public getMaxNumberOfSymbolsPerRequest() {
|
||||
return 50;
|
||||
}
|
||||
|
||||
public getName(): DataSource {
|
||||
return DataSource.YAHOO;
|
||||
}
|
||||
@ -266,6 +270,16 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
}
|
||||
}
|
||||
|
||||
if (yahooFinanceSymbols.includes('USDUSX=X')) {
|
||||
// Convert USD to USX (cent)
|
||||
response['USDUSX'] = {
|
||||
currency: 'USX',
|
||||
dataSource: this.getName(),
|
||||
marketPrice: new Big(1).mul(100).toNumber(),
|
||||
marketState: 'open'
|
||||
};
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
Logger.error(error, 'YahooFinanceService');
|
||||
|
@ -22,9 +22,7 @@ export class ExchangeRateDataService {
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly propertyService: PropertyService
|
||||
) {
|
||||
this.initialize();
|
||||
}
|
||||
) {}
|
||||
|
||||
public getCurrencies() {
|
||||
return this.currencies?.length > 0 ? this.currencies : [this.baseCurrency];
|
||||
@ -122,15 +120,6 @@ export class ExchangeRateDataService {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const hasNaN = Object.values(this.exchangeRates).some((exchangeRate) => {
|
||||
return isNaN(exchangeRate);
|
||||
});
|
||||
|
||||
if (hasNaN) {
|
||||
// Reinitialize if data is not loaded correctly
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
let factor = 1;
|
||||
|
||||
if (aFromCurrency !== aToCurrency) {
|
||||
|
@ -1,15 +1,25 @@
|
||||
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
OnModuleDestroy,
|
||||
OnModuleInit
|
||||
} from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService
|
||||
extends PrismaClient
|
||||
implements OnModuleInit, OnModuleDestroy {
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
implements OnModuleInit, OnModuleDestroy
|
||||
{
|
||||
public async onModuleInit() {
|
||||
try {
|
||||
await this.$connect();
|
||||
} catch (error) {
|
||||
Logger.error(error, 'PrismaService');
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
public async onModuleDestroy() {
|
||||
await this.$disconnect();
|
||||
}
|
||||
}
|
||||
|
@ -115,9 +115,16 @@ export class SymbolProfileService {
|
||||
}
|
||||
|
||||
item.name = item.SymbolProfileOverrides?.name ?? item.name;
|
||||
item.sectors =
|
||||
(item.SymbolProfileOverrides.sectors as unknown as Sector[]) ??
|
||||
item.sectors;
|
||||
|
||||
if (
|
||||
(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;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { RouterModule, Routes, TitleStrategy } from '@angular/router';
|
||||
import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strategy';
|
||||
|
||||
import { ModulePreloadService } from './core/module-preload.service';
|
||||
|
||||
@ -53,37 +54,56 @@ const routes: Routes = [
|
||||
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
|
||||
},
|
||||
{
|
||||
path: 'de/blog/2021/07/hallo-ghostfolio',
|
||||
path: 'blog/2021/07/hallo-ghostfolio',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.module'
|
||||
).then((m) => m.HalloGhostfolioPageModule)
|
||||
},
|
||||
{
|
||||
path: 'blog/2021/07/hello-ghostfolio',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.module'
|
||||
).then((m) => m.HelloGhostfolioPageModule)
|
||||
},
|
||||
{
|
||||
path: 'blog/2022/01/ghostfolio-first-months-in-open-source',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module'
|
||||
).then((m) => m.FirstMonthsInOpenSourcePageModule)
|
||||
},
|
||||
{
|
||||
path: 'blog/2022/07/ghostfolio-meets-internet-identity',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.module'
|
||||
).then((m) => m.GhostfolioMeetsInternetIdentityPageModule)
|
||||
},
|
||||
{
|
||||
path: 'blog/2022/07/how-do-i-get-my-finances-in-order',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./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)
|
||||
},
|
||||
{
|
||||
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: 'en/blog/2021/07/hello-ghostfolio',
|
||||
path: 'faq',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.module'
|
||||
).then((m) => m.HelloGhostfolioPageModule)
|
||||
},
|
||||
{
|
||||
path: 'en/blog/2022/01/ghostfolio-first-months-in-open-source',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module'
|
||||
).then((m) => m.FirstMonthsInOpenSourcePageModule)
|
||||
},
|
||||
{
|
||||
path: 'en/blog/2022/07/how-do-i-get-my-finances-in-order',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./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)
|
||||
import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule)
|
||||
},
|
||||
{
|
||||
path: 'features',
|
||||
@ -222,7 +242,10 @@ const routes: Routes = [
|
||||
}
|
||||
)
|
||||
],
|
||||
providers: [ModulePreloadService],
|
||||
providers: [
|
||||
ModulePreloadService,
|
||||
{ provide: TitleStrategy, useClass: PageTitleStrategy }
|
||||
],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
|
@ -21,8 +21,10 @@
|
||||
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
||||
<ng-container *ngIf="element.type === 'PUBLIC'">
|
||||
<ion-icon class="mr-1" name="link-outline"></ion-icon>
|
||||
<a href="{{ baseUrl }}/p/{{ element.id }}" target="_blank"
|
||||
>{{ baseUrl }}/p/{{ element.id }}</a
|
||||
<a
|
||||
href="{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}"
|
||||
target="_blank"
|
||||
>{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}</a
|
||||
>
|
||||
</ng-container>
|
||||
</td>
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
||||
import { Access } from '@ghostfolio/common/interfaces';
|
||||
|
||||
@Component({
|
||||
@ -24,6 +25,7 @@ export class AccessTableComponent implements OnChanges, OnInit {
|
||||
|
||||
public baseUrl = window.location.origin;
|
||||
public dataSource: MatTableDataSource<Access>;
|
||||
public defaultLanguageCode = DEFAULT_LANGUAGE_CODE;
|
||||
public displayedColumns = [];
|
||||
|
||||
public constructor() {}
|
||||
@ -44,7 +46,7 @@ export class AccessTableComponent implements OnChanges, OnInit {
|
||||
|
||||
public onDeleteAccess(aId: string) {
|
||||
const confirmation = confirm(
|
||||
'Do you really want to revoke this granted access?'
|
||||
$localize`Do you really want to revoke this granted access?`
|
||||
);
|
||||
|
||||
if (confirmation) {
|
||||
|
@ -50,7 +50,6 @@
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="false"
|
||||
[showSymbolColumn]="false"
|
||||
(export)="onExport()"
|
||||
></gf-activities-table>
|
||||
</div>
|
||||
|
@ -69,7 +69,9 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
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) {
|
||||
this.accountDeleted.emit(aId);
|
||||
|
@ -103,7 +103,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public onAddCurrency() {
|
||||
const currency = prompt('Please add a currency:');
|
||||
const currency = prompt($localize`Please add a currency:`);
|
||||
|
||||
if (currency) {
|
||||
const currencies = uniq([...this.customCurrencies, currency]);
|
||||
@ -116,7 +116,9 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
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) {
|
||||
const coupons = this.coupons.filter((coupon) => {
|
||||
@ -127,7 +129,9 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
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) {
|
||||
const currencies = this.customCurrencies.filter((currency) => {
|
||||
@ -142,7 +146,9 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
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) {
|
||||
this.cacheService
|
||||
@ -190,7 +196,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public onSetSystemMessage() {
|
||||
const systemMessage = prompt('Please set your system message:');
|
||||
const systemMessage = prompt($localize`Please set your system message:`);
|
||||
|
||||
if (systemMessage) {
|
||||
this.putSystemMessage(systemMessage);
|
||||
|
@ -55,7 +55,9 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
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) {
|
||||
this.dataService
|
||||
|
@ -109,7 +109,7 @@ export class HeaderComponent implements OnChanges {
|
||||
data: {
|
||||
accessToken: '',
|
||||
hasPermissionToUseSocialLogin: this.hasPermissionForSocialLogin,
|
||||
title: 'Sign in'
|
||||
title: $localize`Sign in`
|
||||
},
|
||||
width: '30rem'
|
||||
});
|
||||
@ -123,7 +123,7 @@ export class HeaderComponent implements OnChanges {
|
||||
.loginAnonymous(data?.accessToken)
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
alert('Oops! Incorrect Security Token.');
|
||||
alert($localize`Oops! Incorrect Security Token.`);
|
||||
|
||||
return EMPTY;
|
||||
}),
|
||||
|
@ -2,6 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
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 { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import {
|
||||
@ -9,7 +10,6 @@ import {
|
||||
SettingsStorageService
|
||||
} from '@ghostfolio/client/services/settings-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { defaultDateRangeOptions } from '@ghostfolio/common/config';
|
||||
import { Position, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
@ -27,7 +27,7 @@ import { PositionDetailDialogParams } from '../position/position-detail-dialog/i
|
||||
})
|
||||
export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
public dateRange: DateRange;
|
||||
public dateRangeOptions = defaultDateRangeOptions;
|
||||
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
||||
public deviceType: string;
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionToCreateOrder: boolean;
|
||||
|
@ -34,6 +34,7 @@
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
animation="pulse"
|
||||
class="px-2 py-3"
|
||||
[theme]="{
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
|
@ -1,4 +1,5 @@
|
||||
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 { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import {
|
||||
@ -6,7 +7,6 @@ import {
|
||||
SettingsStorageService
|
||||
} from '@ghostfolio/client/services/settings-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { defaultDateRangeOptions } from '@ghostfolio/common/config';
|
||||
import {
|
||||
PortfolioPerformance,
|
||||
UniqueAsset,
|
||||
@ -26,7 +26,7 @@ import { takeUntil } from 'rxjs/operators';
|
||||
})
|
||||
export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
public dateRange: DateRange;
|
||||
public dateRangeOptions = defaultDateRangeOptions;
|
||||
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
||||
public deviceType: string;
|
||||
public errors: UniqueAsset[];
|
||||
public hasError: boolean;
|
||||
|
@ -6,7 +6,7 @@
|
||||
.chart-container {
|
||||
aspect-ratio: 16 / 9;
|
||||
height: auto;
|
||||
max-width: 67rem;
|
||||
max-width: 50rem;
|
||||
|
||||
// Fallback for aspect-ratio (using padding hack)
|
||||
@supports not (aspect-ratio: 16 / 9) {
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
getTooltipPositionerMapTop,
|
||||
getVerticalHoverLinePlugin
|
||||
} from '@ghostfolio/common/chart-helper';
|
||||
import { primaryColorRgb } from '@ghostfolio/common/config';
|
||||
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
|
||||
import {
|
||||
getBackgroundColor,
|
||||
getDateFormatString,
|
||||
@ -34,6 +34,7 @@ import {
|
||||
TimeScale,
|
||||
Tooltip
|
||||
} from 'chart.js';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import { addDays, isAfter, parseISO, subDays } from 'date-fns';
|
||||
|
||||
@Component({
|
||||
@ -49,6 +50,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
@Input() investments: InvestmentItem[];
|
||||
@Input() isInPercent = false;
|
||||
@Input() locale: string;
|
||||
@Input() savingsRate = 0;
|
||||
|
||||
@ViewChild('chartCanvas') chartCanvas;
|
||||
|
||||
@ -57,6 +59,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
|
||||
public constructor() {
|
||||
Chart.register(
|
||||
annotationPlugin,
|
||||
BarController,
|
||||
BarElement,
|
||||
LinearScale,
|
||||
@ -158,6 +161,39 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
interaction: { intersect: false, mode: 'index' },
|
||||
maintainAspectRatio: true,
|
||||
plugins: <unknown>{
|
||||
annotation: {
|
||||
annotations: {
|
||||
savingsRate: this.savingsRate
|
||||
? {
|
||||
borderColor: `rgba(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b}, 0.75)`,
|
||||
borderWidth: 1,
|
||||
label: {
|
||||
backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
|
||||
borderRadius: 2,
|
||||
color: 'white',
|
||||
content: 'Savings Rate',
|
||||
display: true,
|
||||
font: { size: '10px', weight: 'normal' },
|
||||
padding: {
|
||||
x: 4,
|
||||
y: 2
|
||||
},
|
||||
position: 'start'
|
||||
},
|
||||
scaleID: 'y',
|
||||
type: 'line',
|
||||
value: this.savingsRate
|
||||
}
|
||||
: undefined,
|
||||
yAxis: {
|
||||
borderColor: `rgba(${getTextColor()}, 0.1)`,
|
||||
borderWidth: 1,
|
||||
scaleID: 'y',
|
||||
type: 'line',
|
||||
value: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
@ -172,6 +208,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
display: true,
|
||||
grid: {
|
||||
borderColor: `rgba(${getTextColor()}, 0.1)`,
|
||||
borderWidth: this.groupBy ? 0 : 1,
|
||||
color: `rgba(${getTextColor()}, 0.8)`,
|
||||
display: false
|
||||
},
|
||||
|
@ -25,14 +25,14 @@
|
||||
>
|
||||
<img
|
||||
class="mr-2"
|
||||
src="./assets/icons/internet-computer.svg"
|
||||
src="../assets/icons/internet-computer.svg"
|
||||
style="height: 0.75rem"
|
||||
/><span i18n>Sign in with Internet Identity</span>
|
||||
</button>
|
||||
<a href="/api/v1/auth/google" mat-stroked-button
|
||||
<a href="../api/v1/auth/google" mat-stroked-button
|
||||
><img
|
||||
class="mr-2"
|
||||
src="./assets/icons/google.svg"
|
||||
src="../assets/icons/google.svg"
|
||||
style="height: 1rem"
|
||||
/><span i18n>Sign in with Google</span></a
|
||||
>
|
||||
|
@ -45,7 +45,7 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
|
||||
|
||||
public onEditEmergencyFund() {
|
||||
const emergencyFundInput = prompt(
|
||||
'Please enter the amount of your emergency fund:',
|
||||
$localize`Please enter the amount of your emergency fund:`,
|
||||
this.summary.emergencyFund.toString()
|
||||
);
|
||||
const emergencyFund = parseFloat(emergencyFundInput?.trim());
|
||||
|
@ -17,6 +17,14 @@ import { ToggleOption } from '@ghostfolio/common/types';
|
||||
styleUrls: ['./toggle.component.scss']
|
||||
})
|
||||
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() isLoading: boolean;
|
||||
@Input() options: ToggleOption[];
|
||||
|
@ -21,6 +21,7 @@ export class AuthGuard implements CanActivate {
|
||||
'/de/blog',
|
||||
'/demo',
|
||||
'/en/blog',
|
||||
'/faq',
|
||||
'/features',
|
||||
'/markets',
|
||||
'/p',
|
||||
|
@ -56,14 +56,16 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
||||
if (!this.snackBarRef) {
|
||||
if (this.info.isReadOnlyMode) {
|
||||
this.snackBarRef = this.snackBar.open(
|
||||
'This feature is currently unavailable. Please try again later.',
|
||||
$localize`This feature is currently unavailable. Please try again later.`,
|
||||
undefined,
|
||||
{ duration: 6000 }
|
||||
);
|
||||
} else {
|
||||
this.snackBarRef = this.snackBar.open(
|
||||
'This feature requires a subscription.',
|
||||
this.hasPermissionForSubscription ? 'Upgrade Plan' : undefined,
|
||||
$localize`This feature requires a subscription.`,
|
||||
this.hasPermissionForSubscription
|
||||
? $localize`Upgrade Plan`
|
||||
: undefined,
|
||||
{ duration: 6000 }
|
||||
);
|
||||
}
|
||||
@ -79,8 +81,8 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
||||
} else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) {
|
||||
if (!this.snackBarRef) {
|
||||
this.snackBarRef = this.snackBar.open(
|
||||
'Oops! Something went wrong. Please try again later.',
|
||||
'Okay',
|
||||
$localize`Oops! Something went wrong. Please try again later.`,
|
||||
$localize`Okay`,
|
||||
{ duration: 6000 }
|
||||
);
|
||||
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { AboutPageComponent } from './about-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: AboutPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: AboutPageComponent,
|
||||
path: '',
|
||||
title: $localize`About`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -173,8 +173,18 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div *ngIf="hasPermissionForSubscription" class="col-md-3 col-xs-12 my-2">
|
||||
<a
|
||||
class="py-2 w-100"
|
||||
color="primary"
|
||||
i18n
|
||||
mat-stroked-button
|
||||
[routerLink]="['/faq']"
|
||||
>FAQ</a
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="col-md-4 col-xs-12 my-2"
|
||||
class="col-md-3 col-xs-12 my-2"
|
||||
[ngClass]="{ 'offset-md-4': !hasPermissionForBlog }"
|
||||
>
|
||||
<a
|
||||
@ -186,7 +196,7 @@
|
||||
>Changelog & License</a
|
||||
>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionForSubscription" class="col-md-4 col-xs-12 my-2">
|
||||
<div *ngIf="hasPermissionForSubscription" class="col-md-3 col-xs-12 my-2">
|
||||
<a
|
||||
class="py-2 w-100"
|
||||
color="primary"
|
||||
@ -196,7 +206,7 @@
|
||||
>Privacy Policy</a
|
||||
>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionForBlog" class="col-md-4 col-xs-12 my-2">
|
||||
<div *ngIf="hasPermissionForBlog" class="col-md-3 col-xs-12 my-2">
|
||||
<a
|
||||
class="py-2 w-100"
|
||||
color="primary"
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { ChangelogPageComponent } from './changelog-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: ChangelogPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: ChangelogPageComponent,
|
||||
path: '',
|
||||
title: $localize`Changelog & License`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -4,7 +4,7 @@
|
||||
<h3 class="mb-3 text-center" i18n>Changelog</h3>
|
||||
<mat-card class="changelog">
|
||||
<mat-card-content>
|
||||
<markdown [src]="'assets/CHANGELOG.md'"></markdown>
|
||||
<markdown [src]="'../assets/CHANGELOG.md'"></markdown>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@ -15,7 +15,7 @@
|
||||
<h3 class="mb-3 text-center" i18n>License</h3>
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<markdown [src]="'assets/LICENSE'"></markdown>
|
||||
<markdown [src]="'../assets/LICENSE'"></markdown>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { PrivacyPolicyPageComponent } from './privacy-policy-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: PrivacyPolicyPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: PrivacyPolicyPageComponent,
|
||||
path: '',
|
||||
title: $localize`Privacy Policy`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<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>
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { AccountPageComponent } from './account-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: AccountPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: AccountPageComponent,
|
||||
path: '',
|
||||
title: $localize`My Ghostfolio`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -218,7 +218,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public onRedeemCoupon() {
|
||||
let couponCode = prompt('Please enter your coupon code:');
|
||||
let couponCode = prompt($localize`Please enter your coupon code:`);
|
||||
couponCode = couponCode?.trim();
|
||||
|
||||
if (couponCode) {
|
||||
@ -227,17 +227,21 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
.pipe(
|
||||
takeUntil(this.unsubscribeSubject),
|
||||
catchError(() => {
|
||||
this.snackBar.open('😞 Could not redeem coupon code', undefined, {
|
||||
duration: 3000
|
||||
});
|
||||
this.snackBar.open(
|
||||
'😞 ' + $localize`Could not redeem coupon code`,
|
||||
undefined,
|
||||
{
|
||||
duration: 3000
|
||||
}
|
||||
);
|
||||
|
||||
return EMPTY;
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.snackBarRef = this.snackBar.open(
|
||||
'✅ Coupon code has been redeemed',
|
||||
'Reload',
|
||||
'✅' + $localize`Coupon code has been redeemed`,
|
||||
$localize`Reload`,
|
||||
{
|
||||
duration: 3000
|
||||
}
|
||||
@ -283,7 +287,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
this.registerDevice();
|
||||
} else {
|
||||
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) {
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { AccountsPageComponent } from './accounts-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: AccountsPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: AccountsPageComponent,
|
||||
path: '',
|
||||
title: $localize`Accounts`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -10,8 +10,6 @@ import { AdminPageComponent } from './admin-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AdminPageComponent,
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{ path: '', redirectTo: 'overview', pathMatch: 'full' },
|
||||
@ -19,7 +17,10 @@ const routes: Routes = [
|
||||
{ path: 'market-data', component: AdminMarketDataComponent },
|
||||
{ path: 'overview', component: AdminOverviewComponent },
|
||||
{ path: 'users', component: AdminUsersComponent }
|
||||
]
|
||||
],
|
||||
component: AdminPageComponent,
|
||||
path: '',
|
||||
title: $localize`Admin Control`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -4,8 +4,8 @@ import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthPageComponent } from './auth-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: AuthPageComponent },
|
||||
{ path: ':jwt', component: AuthPageComponent }
|
||||
{ component: AuthPageComponent, path: '' },
|
||||
{ component: AuthPageComponent, path: ':jwt' }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -28,6 +28,7 @@ export class AuthPageComponent implements OnDestroy, OnInit {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
const jwt = params['jwt'];
|
||||
|
||||
this.tokenStorageService.saveToken(
|
||||
jwt,
|
||||
this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true'
|
||||
|
@ -6,9 +6,10 @@ import { HalloGhostfolioPageComponent } from './hallo-ghostfolio-page.component'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
canActivate: [AuthGuard],
|
||||
component: HalloGhostfolioPageComponent,
|
||||
canActivate: [AuthGuard]
|
||||
path: '',
|
||||
title: 'Hallo Ghostfolio'
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1">Hallo Ghostfolio 👋</h1>
|
||||
<div class="text-muted"><small>31.07.2021</small></div>
|
||||
<div class="text-muted"><small>2021-07-31</small></div>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
@ -68,7 +68,7 @@
|
||||
<p class="my-5 text-center">
|
||||
<img
|
||||
alt="Ghostfol.io Screenshot"
|
||||
src="./assets/images/screenshot.png"
|
||||
src="../assets/images/screenshot.png"
|
||||
style="max-width: 100%; width: 20rem"
|
||||
title="Ghostfol.io Screenshot"
|
||||
/>
|
||||
|
@ -6,9 +6,10 @@ import { HelloGhostfolioPageComponent } from './hello-ghostfolio-page.component'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
canActivate: [AuthGuard],
|
||||
component: HelloGhostfolioPageComponent,
|
||||
canActivate: [AuthGuard]
|
||||
path: '',
|
||||
title: 'Hello Ghostfolio'
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1">Hello Ghostfolio 👋</h1>
|
||||
<div class="text-muted"><small>31.07.2021</small></div>
|
||||
<div class="text-muted"><small>2021-07-31</small></div>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
@ -66,7 +66,7 @@
|
||||
<p class="my-5 text-center">
|
||||
<img
|
||||
alt="Ghostfol.io Screenshot"
|
||||
src="./assets/images/screenshot.png"
|
||||
src="../assets/images/screenshot.png"
|
||||
style="max-width: 100%; width: 20rem"
|
||||
title="Ghostfol.io Screenshot"
|
||||
/>
|
||||
|
@ -6,9 +6,10 @@ import { FirstMonthsInOpenSourcePageComponent } from './first-months-in-open-sou
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
canActivate: [AuthGuard],
|
||||
component: FirstMonthsInOpenSourcePageComponent,
|
||||
canActivate: [AuthGuard]
|
||||
path: '',
|
||||
title: 'First months in Open Source'
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
👻 Ghostfolio –
|
||||
<span class="text-nowrap">First months in Open Source</span>
|
||||
</h1>
|
||||
<div class="text-muted"><small>05.01.2022</small></div>
|
||||
<div class="text-muted"><small>2022-01-05</small></div>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
@ -20,9 +20,7 @@
|
||||
<h2 class="h4">From 1* to 100 stars on GitHub</h2>
|
||||
<p>
|
||||
When I decided to
|
||||
<a [routerLink]="['/en', 'blog', '2021', '07', 'hello-ghostfolio']"
|
||||
>publish</a
|
||||
>
|
||||
<a href="../en/blog/2021/07/hello-ghostfolio">publish</a>
|
||||
the project as
|
||||
<a href="https://github.com/ghostfolio/ghostfolio"
|
||||
>open source software</a
|
||||
|
@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { GhostfolioMeetsInternetIdentityPageComponent } from './ghostfolio-meets-internet-identity-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: GhostfolioMeetsInternetIdentityPageComponent,
|
||||
path: '',
|
||||
title: 'Ghostfolio meets Internet Identity'
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class GhostfolioMeetsInternetIdentityRoutingModule {}
|
@ -0,0 +1,9 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-ghostfolio-meets-internet-identity-page',
|
||||
styleUrls: ['./ghostfolio-meets-internet-identity-page.scss'],
|
||||
templateUrl: './ghostfolio-meets-internet-identity-page.html'
|
||||
})
|
||||
export class GhostfolioMeetsInternetIdentityPageComponent {}
|
@ -0,0 +1,183 @@
|
||||
<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">Ghostfolio meets Internet Identity</h1>
|
||||
<div class="mb-3 text-muted"><small>2022-07-23</small></div>
|
||||
<img
|
||||
alt="Ghostfolio meets Internet Identity Teaser"
|
||||
class="rounded w-100"
|
||||
src="../assets/images/blog/ghostfolio-meets-internet-identity.png"
|
||||
title="Ghostfolio meets Internet Identity"
|
||||
/>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
<a href="https://ghostfol.io">Ghostfolio</a>, the web-based personal
|
||||
finance management software, supports passwordless authentication as
|
||||
of now thanks to the integration of
|
||||
<a href="https://identity.ic0.app">Internet Identity</a>. This
|
||||
blockchain authentication system enables you to sign in securely and
|
||||
anonymously to Ghostfolio. With this latest update, Ghostfolio is
|
||||
ready for Web3.
|
||||
</p>
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<blockquote class="blockquote m-0">
|
||||
<p class="mb-0">Track your portfolio without being tracked</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
To avoid the security issues that arise with password authentication
|
||||
on the traditional web, the
|
||||
<a href="https://internetcomputer.org">Internet Computer</a>
|
||||
blockchain by <a href="https://dfinity.org">dfinity</a> has
|
||||
introduced a new cryptographic authentication system. It is called
|
||||
<i>Internet Identity</i> and is as convenient to use as Web2
|
||||
<a href="https://en.wikipedia.org/wiki/OAuth">OAuth</a> ("Open
|
||||
Authorization") providers like <i>Google Sign-In</i> or
|
||||
<i>Facebook Login</i>.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">How to use Internet Identity?</h2>
|
||||
<p>
|
||||
<i>Internet Identity</i> is based on the
|
||||
<a href="https://en.wikipedia.org/wiki/WebAuthn"
|
||||
>WebAuthn protocol</a
|
||||
>
|
||||
and uses secure cryptographic authentication. It provides three
|
||||
options to authenticate yourself:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
The built-in biometric authentication methods of your smartphone
|
||||
or laptop (fingerprint sensor, <i>Face ID</i>, <i>Touch ID</i>)
|
||||
</li>
|
||||
<li>The password or pin to unlock your computer or mobile phone</li>
|
||||
<li>A security key plugged into the USB port of your computer</li>
|
||||
</ul>
|
||||
<p>
|
||||
When you authenticate with <i>Internet Identity</i>, the service
|
||||
only gets a dedicated pseudonym rather than sensitive user data like
|
||||
the email address or phone number. This preserves your anonymity and
|
||||
prevents you being tracked on the Internet.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">The key benefits in a nutshell</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Authenticate yourself securely without the need of an email
|
||||
address, username, or a password: all you need is your device to
|
||||
log in.
|
||||
</li>
|
||||
<li>
|
||||
Built-in recovery mechanisms to ensure you are not locked out of
|
||||
any service that requires the <i>Internet Identity</i>.
|
||||
</li>
|
||||
<li>
|
||||
Log in to various Internet services without being tracked by big
|
||||
tech companies.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
If you would like to provide feedback or get involved in further
|
||||
development of Ghostfolio, 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>.
|
||||
</p>
|
||||
<p>
|
||||
I look forward to hearing from you.<br />
|
||||
Thomas from Ghostfolio
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<ul class="list-inline">
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Anonymity</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">App</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Auth Provider</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Authentication</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Blockchain</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Cryptography</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">dfinity</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Face ID</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Fingerprint</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Ghostfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Internet Computer</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">OAuth</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">Password</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">passwordless</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">Security</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">Technology</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Touch ID</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">WebAuthn</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,17 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { GhostfolioMeetsInternetIdentityRoutingModule } from './ghostfolio-meets-internet-identity-page-routing.module';
|
||||
import { GhostfolioMeetsInternetIdentityPageComponent } from './ghostfolio-meets-internet-identity-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [GhostfolioMeetsInternetIdentityPageComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GhostfolioMeetsInternetIdentityRoutingModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GhostfolioMeetsInternetIdentityPageModule {}
|
@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
@ -6,9 +6,10 @@ import { HowDoIGetMyFinancesInOrderPageComponent } from './how-do-i-get-my-finan
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
canActivate: [AuthGuard],
|
||||
component: HowDoIGetMyFinancesInOrderPageComponent,
|
||||
canActivate: [AuthGuard]
|
||||
path: '',
|
||||
title: 'How do I get my finances in order?'
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1">How do I get my finances in order?</h1>
|
||||
<div class="text-muted"><small>14.07.2022</small></div>
|
||||
<div class="text-muted"><small>2022-07-14</small></div>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
|
@ -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 {}
|
@ -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 {}
|
@ -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>
|
@ -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 {}
|
@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { BlogPageComponent } from './blog-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: BlogPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: BlogPageComponent,
|
||||
path: '',
|
||||
title: $localize`Blog`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -8,13 +8,63 @@
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex w-100"
|
||||
[routerLink]="['/en', 'blog', '2022', '07', 'how-do-i-get-my-finances-in-order']"
|
||||
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="h6 m-0 text-truncate">
|
||||
Ghostfolio meets Internet Identity
|
||||
</div>
|
||||
<div class="d-flex text-muted">2022-07-23</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/how-do-i-get-my-finances-in-order"
|
||||
>
|
||||
<div class="flex-grow-1">
|
||||
<div class="h6 m-0 text-truncate">
|
||||
How do I get my finances in order?
|
||||
</div>
|
||||
<div class="d-flex text-muted">14.07.2022</div>
|
||||
<div class="d-flex text-muted">2022-07-14</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
@ -34,13 +84,13 @@
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
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="h6 m-0 text-truncate">
|
||||
First months in Open Source
|
||||
</div>
|
||||
<div class="d-flex text-muted">05.01.2022</div>
|
||||
<div class="d-flex text-muted">2022-01-05</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
@ -60,11 +110,11 @@
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
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="h6 m-0 text-truncate">Hello Ghostfolio</div>
|
||||
<div class="d-flex text-muted">31.07.2021</div>
|
||||
<div class="d-flex text-muted">2021-07-31</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
@ -84,11 +134,11 @@
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
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="h6 m-0 text-truncate">Hallo Ghostfolio</div>
|
||||
<div class="d-flex text-muted">31.07.2021</div>
|
||||
<div class="d-flex text-muted">2021-07-31</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
|
@ -28,7 +28,7 @@ export class DemoPageComponent implements OnDestroy {
|
||||
|
||||
if (hasToken) {
|
||||
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 {
|
||||
this.tokenStorageService.saveToken(this.info.demoAuthToken, true);
|
||||
|
20
apps/client/src/app/pages/faq/faq-page-routing.module.ts
Normal file
20
apps/client/src/app/pages/faq/faq-page-routing.module.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { FaqPageComponent } from './faq-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: FaqPageComponent,
|
||||
path: '',
|
||||
title: $localize`FAQ`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class FaqPageRoutingModule {}
|
19
apps/client/src/app/pages/faq/faq-page.component.ts
Normal file
19
apps/client/src/app/pages/faq/faq-page.component.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-faq-page',
|
||||
styleUrls: ['./faq-page.scss'],
|
||||
templateUrl: './faq-page.html'
|
||||
})
|
||||
export class FaqPageComponent implements OnDestroy {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
141
apps/client/src/app/pages/faq/faq-page.html
Normal file
141
apps/client/src/app/pages/faq/faq-page.html
Normal file
@ -0,0 +1,141 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>Frequently Asked Questions (FAQ)</h3>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n>What is Ghostfolio?</mat-card-title>
|
||||
<mat-card-content i18n>
|
||||
Ghostfolio is a lightweight, open source wealth management application
|
||||
for individuals to keep track of their net worth. The software
|
||||
empowers you to make solid, data-driven investment decisions.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n
|
||||
>What assets can I track with Ghostfolio?</mat-card-title
|
||||
>
|
||||
<mat-card-content i18n>
|
||||
With Ghostfolio, you can keep track of various assets like stocks,
|
||||
ETFs or cryptocurrencies.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n
|
||||
>What else is included in Ghostfolio?</mat-card-title
|
||||
>
|
||||
<mat-card-content i18n>
|
||||
Please find a feature overview to manage your wealth
|
||||
<a [routerLink]="['/features']">here</a>.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n>How do I start?</mat-card-title>
|
||||
<mat-card-content i18n>
|
||||
You can sign up via the “<a [routerLink]="['/register']"
|
||||
>Get Started</a
|
||||
>” button at the top of the page. You have multiple options to join
|
||||
Ghostfolio: Create an account with a security token, using
|
||||
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
|
||||
>Internet Identity</a
|
||||
>
|
||||
or <i>Google Sign</i>. We will guide you to set up your portfolio.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n>Can I use Ghostfolio anonymously?</mat-card-title>
|
||||
<mat-card-content i18n>
|
||||
Yes, the authentication systems (via security token or
|
||||
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
|
||||
>Internet Identity</a
|
||||
>) enable you to sign in securely and anonymously to Ghostfolio. There
|
||||
is no need for an email address, phone number, or a username.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n>How can Ghostfolio be free?</mat-card-title>
|
||||
<mat-card-content i18n
|
||||
>This project is driven by the efforts of contributors from around the
|
||||
world. The
|
||||
<a href="https://github.com/ghostfolio/ghostfolio">source code</a> is
|
||||
fully available as open source software (OSS). Our
|
||||
<a [routerLink]="['/pricing']">Ghostfolio Premium</a> users and
|
||||
<a href="https://www.buymeacoffee.com/ghostfolio">sponsors</a> allow
|
||||
us to run a free, limited plan for new investors.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n>Is it really free?</mat-card-title>
|
||||
<mat-card-content i18n
|
||||
>Yes, it is! Our
|
||||
<a [routerLink]="['/pricing']">pricing page</a> details everything you
|
||||
get for free.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n>What is Ghostfolio Premium?</mat-card-title>
|
||||
<mat-card-content i18n
|
||||
><a [routerLink]="['/pricing']">Ghostfolio Premium</a> is a fully
|
||||
managed Ghostfolio cloud offering for ambitious investors. The revenue
|
||||
is used to cover the hosting infrastructure. It is the Open Source
|
||||
code base with some extras like the market overview.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n>Can I start with a trial version?</mat-card-title>
|
||||
<mat-card-content i18n
|
||||
>Yes, you can try
|
||||
<a [routerLink]="['/pricing']">Ghostfolio Premium</a> by signing up
|
||||
for Ghostfolio and applying for a trial (see “My Ghostfolio”). It’s
|
||||
easy, free and there is no commitment. You can stop using it at any
|
||||
time.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n>Which devices are supported?</mat-card-title>
|
||||
<mat-card-content i18n
|
||||
>Ghostfolio works in every modern web browser on smartphones, tablets
|
||||
and desktop computers (where you have even more analysis options and
|
||||
statistics). For Android users, there is a dedicated Ghostfolio app
|
||||
available in the
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
|
||||
>Google Play Store</a
|
||||
>.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n
|
||||
>Ghostfolio sounds cool, how can I get involved?</mat-card-title
|
||||
>
|
||||
<mat-card-content i18n
|
||||
>Any support for Ghostfolio is welcome. Be it with a
|
||||
<a [routerLink]="['/pricing']">Ghostfolio Premium</a> subscription to
|
||||
finance the hosting, a positive rating in the
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
|
||||
>Google Play Store</a
|
||||
>, a star on
|
||||
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>,
|
||||
feedback, bug reports, feature requests and of course contributions!
|
||||
You can reach us by email at
|
||||
<a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a> or on Twitter
|
||||
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a
|
||||
>.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n>Got any other questions?</mat-card-title>
|
||||
<mat-card-content i18n
|
||||
>Join the Ghostfolio
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
>Slack community</a
|
||||
>, tweet to
|
||||
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a> or send an
|
||||
e-mail to
|
||||
<a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a>.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
13
apps/client/src/app/pages/faq/faq-page.module.ts
Normal file
13
apps/client/src/app/pages/faq/faq-page.module.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
|
||||
import { FaqPageRoutingModule } from './faq-page-routing.module';
|
||||
import { FaqPageComponent } from './faq-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [FaqPageComponent],
|
||||
imports: [CommonModule, FaqPageRoutingModule, MatCardModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class FaqPageModule {}
|
12
apps/client/src/app/pages/faq/faq-page.scss
Normal file
12
apps/client/src/app/pages/faq/faq-page.scss
Normal file
@ -0,0 +1,12 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { FeaturesPageComponent } from './features-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: FeaturesPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: FeaturesPageComponent,
|
||||
path: '',
|
||||
title: $localize`Features`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -10,8 +10,6 @@ import { HomePageComponent } from './home-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HomePageComponent,
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{ path: '', redirectTo: 'overview', pathMatch: 'full' },
|
||||
@ -19,7 +17,10 @@ const routes: Routes = [
|
||||
{ path: 'holdings', component: HomeHoldingsComponent },
|
||||
{ path: 'summary', component: HomeSummaryComponent },
|
||||
{ path: 'market', component: HomeMarketComponent }
|
||||
]
|
||||
],
|
||||
component: HomePageComponent,
|
||||
path: '',
|
||||
title: $localize`Overview`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
<img
|
||||
alt="Ghostfol.io Trailer"
|
||||
class="rounded video"
|
||||
src="./assets/images/video-preview.jpg"
|
||||
src="../assets/images/video-preview.jpg"
|
||||
style="max-width: 100%; width: 40rem"
|
||||
/>
|
||||
</a>
|
||||
@ -183,7 +183,7 @@
|
||||
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@ -194,7 +194,7 @@
|
||||
<div class="h-100 w-100"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row mb-4">
|
||||
<div
|
||||
class="align-items-center d-flex flex-column justify-content-center w-100"
|
||||
>
|
||||
@ -202,4 +202,14 @@
|
||||
<div>Wealth Management Software</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="align-items-center d-flex flex-column w-100">
|
||||
<a
|
||||
class="agplv3-logo"
|
||||
href="https://www.gnu.org/licenses/agpl-3.0.html"
|
||||
target="_blank"
|
||||
title="GNU Affero General Public License Version 3"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,6 +3,16 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.agplv3-logo {
|
||||
background-color: rgba(var(--dark-primary-text));
|
||||
height: 3rem;
|
||||
mask-image: url('/assets/images/AGPLv3-logo.svg');
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
width: 7.5rem;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
.mat-stroked-button {
|
||||
background-color: var(--light-background);
|
||||
@ -46,6 +56,10 @@
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
.agplv3-logo {
|
||||
background-color: rgba(var(--light-primary-text));
|
||||
}
|
||||
|
||||
.button-container {
|
||||
.mat-stroked-button {
|
||||
background-color: var(--dark-background);
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { MarketsPageComponent } from './markets-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: MarketsPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: MarketsPageComponent,
|
||||
path: '',
|
||||
title: $localize`Markets`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { AllocationsPageComponent } from './allocations-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: AllocationsPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: AllocationsPageComponent,
|
||||
path: '',
|
||||
title: $localize`Allocations`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { AnalysisPageComponent } from './analysis-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: AnalysisPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: AnalysisPageComponent,
|
||||
path: '',
|
||||
title: $localize`Analysis`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -40,6 +40,7 @@
|
||||
[investments]="investmentsByMonth"
|
||||
[locale]="user?.settings?.locale"
|
||||
[ngClass]="{ 'd-none': !mode }"
|
||||
[savingsRate]="(hasImpersonationId || user.settings.isRestrictedView) ? undefined : user?.settings?.savingsRate"
|
||||
></gf-investment-chart>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { FirePageComponent } from './fire-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: FirePageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: FirePageComponent,
|
||||
path: '',
|
||||
title: $localize`FIRE`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { HoldingsPageComponent } from './holdings-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: HoldingsPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: HoldingsPageComponent,
|
||||
path: '',
|
||||
title: $localize`Holdings`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { PortfolioPageComponent } from './portfolio-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: PortfolioPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: PortfolioPageComponent,
|
||||
path: '',
|
||||
title: 'Portfolio'
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { ReportPageComponent } from './report-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: ReportPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: ReportPageComponent,
|
||||
path: '',
|
||||
title: 'X-ray'
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -76,6 +76,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
||||
accountId: [this.data.activity?.accountId, Validators.required],
|
||||
assetClass: [this.data.activity?.SymbolProfile?.assetClass],
|
||||
assetSubClass: [this.data.activity?.SymbolProfile?.assetSubClass],
|
||||
comment: [this.data.activity?.comment],
|
||||
currency: [
|
||||
this.data.activity?.SymbolProfile?.currency,
|
||||
Validators.required
|
||||
@ -245,6 +246,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
||||
accountId: this.activityForm.controls['accountId'].value,
|
||||
assetClass: this.activityForm.controls['assetClass'].value,
|
||||
assetSubClass: this.activityForm.controls['assetSubClass'].value,
|
||||
comment: this.activityForm.controls['comment'].value,
|
||||
currency: this.activityForm.controls['currency'].value,
|
||||
date: this.activityForm.controls['date'].value,
|
||||
dataSource: this.activityForm.controls['dataSource'].value,
|
||||
|
@ -135,6 +135,18 @@
|
||||
>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Note</mat-label>
|
||||
<textarea
|
||||
cdkAutosizeMinRows="2"
|
||||
cdkTextareaAutosize
|
||||
formControlName="comment"
|
||||
matInput
|
||||
(keyup.enter)="$event.stopPropagation()"
|
||||
></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div
|
||||
[ngClass]="{ 'd-none': activityForm.controls['type']?.value !== 'ITEM' }"
|
||||
>
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { TransactionsPageComponent } from './transactions-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: TransactionsPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: TransactionsPageComponent,
|
||||
path: '',
|
||||
title: $localize`Activities`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -188,7 +188,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
||||
input.type = 'file';
|
||||
|
||||
input.onchange = (event) => {
|
||||
this.snackBar.open('⏳ Importing data...');
|
||||
this.snackBar.open('⏳' + $localize`Importing data...`);
|
||||
|
||||
// Getting the file reference
|
||||
const file = (event.target as HTMLInputElement).files[0];
|
||||
@ -334,7 +334,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
||||
private handleImportSuccess() {
|
||||
this.fetchActivities();
|
||||
|
||||
this.snackBar.open('✅ Import has been completed', undefined, {
|
||||
this.snackBar.open('✅' + $localize`Import has been completed`, undefined, {
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { PricingPageComponent } from './pricing-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: PricingPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: PricingPageComponent,
|
||||
path: '',
|
||||
title: $localize`Pricing`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { PublicPageComponent } from './public-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: ':id', component: PublicPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: PublicPageComponent,
|
||||
path: ':id',
|
||||
title: $localize`Portfolio`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -5,7 +5,12 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { RegisterPageComponent } from './register-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: RegisterPageComponent, canActivate: [AuthGuard] }
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: RegisterPageComponent,
|
||||
path: '',
|
||||
title: $localize`Registration`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user