Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
7c8530483c | |||
539d3ff754 | |||
9d28b63da6 | |||
24abbd85e6 | |||
b6f395fd3b | |||
04d894cf88 | |||
b4d2c4109e | |||
823093f4d7 | |||
56bf422407 | |||
df0e9ad03b | |||
0e3702c2be | |||
11136ae4f8 | |||
2e6a7d5a91 | |||
83845c256a | |||
34c9703716 | |||
48903238c5 | |||
57a14bd945 | |||
4fd0622114 | |||
52f0fb5ab8 | |||
20195b2b1a | |||
7fa4e6ebd2 | |||
d8531ddfcb | |||
70d670b711 | |||
27b0663a80 | |||
874dfb0235 | |||
072db0d558 | |||
12e692429a | |||
e22b8b78b8 | |||
dc5052f7dc | |||
335553e891 | |||
d480ad1023 | |||
7320751056 | |||
108c0c13c4 | |||
053a5cc5b5 | |||
c456a8bcfe | |||
6fcecb5bc6 | |||
e4e0a7d9f0 |
56
CHANGELOG.md
56
CHANGELOG.md
@ -5,7 +5,61 @@ 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.179.0 - 13.08.2022
|
||||
## 1.183.0 - 24.08.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added a filter by asset sub class for the asset profiles in the admin control
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the language localization for German (`de`)
|
||||
|
||||
## 1.182.0 - 23.08.2022
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the language localization for German (`de`)
|
||||
- Extended and made the columns of the asset profiles sortable in the admin control
|
||||
- Moved the asset profile details in the admin control panel to a dialog
|
||||
|
||||
## 1.181.2 - 21.08.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added a language selector to the account page
|
||||
- Added support for translated labels in the value component
|
||||
|
||||
### Changed
|
||||
|
||||
- Integrated the commands `database:setup` and `database:migrate` into the container start
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a division by zero error in the benchmarks calculation
|
||||
|
||||
### Todo
|
||||
|
||||
- Apply manual data migration (`yarn database:migrate`) is not needed anymore
|
||||
|
||||
## 1.180.1 - 18.08.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Set up `ng-extract-i18n-merge` to improve the i18n extraction and merge workflow
|
||||
- Set up language localization for German (`de`)
|
||||
- Resolved the feature graphic of the blog post
|
||||
|
||||
### Changed
|
||||
|
||||
- Tagged template literal strings in components for localization with `$localize`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the license component in the about page
|
||||
- Fixed the links to the blog posts
|
||||
|
||||
## 1.179.5 - 15.08.2022
|
||||
|
||||
### Added
|
||||
|
||||
|
@ -49,4 +49,4 @@ FROM node:16-alpine
|
||||
COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps
|
||||
WORKDIR /ghostfolio/apps/api
|
||||
EXPOSE 3333
|
||||
CMD [ "node", "main" ]
|
||||
CMD [ "yarn", "start:prod" ]
|
||||
|
20
README.md
20
README.md
@ -114,14 +114,6 @@ Run the following command to start the Docker images from [Docker Hub](https://h
|
||||
docker-compose --env-file ./.env -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
##### Setup Database
|
||||
|
||||
Run the following command to setup the database once Ghostfolio is running:
|
||||
|
||||
```bash
|
||||
docker-compose --env-file ./.env -f docker/docker-compose.yml exec ghostfolio yarn database:setup
|
||||
```
|
||||
|
||||
#### b. Build and run environment
|
||||
|
||||
Run the following commands to build and start the Docker images:
|
||||
@ -131,14 +123,6 @@ docker-compose --env-file ./.env -f docker/docker-compose.build.yml build
|
||||
docker-compose --env-file ./.env -f docker/docker-compose.build.yml up -d
|
||||
```
|
||||
|
||||
##### Setup Database
|
||||
|
||||
Run the following command to setup the database once Ghostfolio is running:
|
||||
|
||||
```bash
|
||||
docker-compose --env-file ./.env -f docker/docker-compose.build.yml exec ghostfolio yarn database:setup
|
||||
```
|
||||
|
||||
#### Fetch Historical Data
|
||||
|
||||
Open http://localhost:3333 in your browser and accomplish these steps:
|
||||
@ -150,8 +134,8 @@ Open http://localhost:3333 in your browser and accomplish these steps:
|
||||
#### Upgrade Version
|
||||
|
||||
1. Increase the version of the `ghostfolio/ghostfolio` Docker image in `docker/docker-compose.yml`
|
||||
1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d`
|
||||
1. Then, run the following command to keep your database schema in sync: `docker-compose --env-file ./.env -f docker/docker-compose.yml exec ghostfolio yarn database:migrate`
|
||||
1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d`
|
||||
At each start, the container will automatically apply the database schema migrations if needed.
|
||||
|
||||
### Run with _Unraid_ (Community)
|
||||
|
||||
|
19
angular.json
19
angular.json
@ -87,11 +87,6 @@
|
||||
"input": "",
|
||||
"output": "./../assets"
|
||||
},
|
||||
{
|
||||
"glob": "index.html",
|
||||
"input": "apps/client/src/assets",
|
||||
"output": "./../"
|
||||
},
|
||||
{
|
||||
"glob": "LICENSE",
|
||||
"input": "",
|
||||
@ -133,6 +128,10 @@
|
||||
"namedChunks": true
|
||||
},
|
||||
"configurations": {
|
||||
"development-de": {
|
||||
"baseHref": "/de/",
|
||||
"localize": ["de"]
|
||||
},
|
||||
"development-en": {
|
||||
"baseHref": "/en/",
|
||||
"localize": ["en"]
|
||||
@ -175,6 +174,9 @@
|
||||
"proxyConfig": "apps/client/proxy.conf.json"
|
||||
},
|
||||
"configurations": {
|
||||
"development-de": {
|
||||
"browserTarget": "client:build:development-de"
|
||||
},
|
||||
"development-en": {
|
||||
"browserTarget": "client:build:development-en"
|
||||
},
|
||||
@ -184,9 +186,12 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
|
@ -8,7 +8,8 @@ import {
|
||||
import {
|
||||
AdminData,
|
||||
AdminMarketData,
|
||||
AdminMarketDataDetails
|
||||
AdminMarketDataDetails,
|
||||
Filter
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
@ -22,6 +23,7 @@ import {
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
@ -226,7 +228,9 @@ export class AdminController {
|
||||
|
||||
@Get('market-data')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getMarketData(): Promise<AdminMarketData> {
|
||||
public async getMarketData(
|
||||
@Query('assetSubClasses') filterByAssetSubClasses?: string
|
||||
): Promise<AdminMarketData> {
|
||||
if (
|
||||
!hasPermission(
|
||||
this.request.user.permissions,
|
||||
@ -239,7 +243,18 @@ export class AdminController {
|
||||
);
|
||||
}
|
||||
|
||||
return this.adminService.getMarketData();
|
||||
const assetSubClasses = filterByAssetSubClasses?.split(',') ?? [];
|
||||
|
||||
const filters: Filter[] = [
|
||||
...assetSubClasses.map((assetSubClass) => {
|
||||
return <Filter>{
|
||||
id: assetSubClass,
|
||||
type: 'ASSET_SUB_CLASS'
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
return this.adminService.getMarketData(filters);
|
||||
}
|
||||
|
||||
@Get('market-data/:dataSource/:symbol')
|
||||
|
@ -11,11 +11,13 @@ import {
|
||||
AdminMarketData,
|
||||
AdminMarketDataDetails,
|
||||
AdminMarketDataItem,
|
||||
Filter,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Property } from '@prisma/client';
|
||||
import { AssetSubClass, Prisma, Property } from '@prisma/client';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { groupBy } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
@ -63,14 +65,27 @@ export class AdminService {
|
||||
};
|
||||
}
|
||||
|
||||
public async getMarketData(): Promise<AdminMarketData> {
|
||||
public async getMarketData(filters?: Filter[]): Promise<AdminMarketData> {
|
||||
const where: Prisma.SymbolProfileWhereInput = {};
|
||||
|
||||
const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy(
|
||||
filters,
|
||||
(filter) => {
|
||||
return filter.type;
|
||||
}
|
||||
);
|
||||
|
||||
const marketData = await this.prismaService.marketData.groupBy({
|
||||
_count: true,
|
||||
by: ['dataSource', 'symbol']
|
||||
});
|
||||
|
||||
const currencyPairsToGather: AdminMarketDataItem[] =
|
||||
this.exchangeRateDataService
|
||||
let currencyPairsToGather: AdminMarketDataItem[] = [];
|
||||
|
||||
if (filtersByAssetSubClass) {
|
||||
where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id];
|
||||
} else {
|
||||
currencyPairsToGather = this.exchangeRateDataService
|
||||
.getCurrencyPairs()
|
||||
.map(({ dataSource, symbol }) => {
|
||||
const marketDataItemCount =
|
||||
@ -84,17 +99,24 @@ export class AdminService {
|
||||
return {
|
||||
dataSource,
|
||||
marketDataItemCount,
|
||||
symbol
|
||||
symbol,
|
||||
countriesCount: 0,
|
||||
sectorsCount: 0
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const symbolProfilesToGather: AdminMarketDataItem[] = (
|
||||
await this.prismaService.symbolProfile.findMany({
|
||||
where,
|
||||
orderBy: [{ symbol: 'asc' }],
|
||||
select: {
|
||||
_count: {
|
||||
select: { Order: true }
|
||||
},
|
||||
assetClass: true,
|
||||
assetSubClass: true,
|
||||
countries: true,
|
||||
dataSource: true,
|
||||
Order: {
|
||||
orderBy: [{ date: 'asc' }],
|
||||
@ -102,10 +124,14 @@ export class AdminService {
|
||||
take: 1
|
||||
},
|
||||
scraperConfiguration: true,
|
||||
sectors: true,
|
||||
symbol: true
|
||||
}
|
||||
})
|
||||
).map((symbolProfile) => {
|
||||
const countriesCount = symbolProfile.countries
|
||||
? Object.keys(symbolProfile.countries).length
|
||||
: 0;
|
||||
const marketDataItemCount =
|
||||
marketData.find((marketDataItem) => {
|
||||
return (
|
||||
@ -113,10 +139,17 @@ export class AdminService {
|
||||
marketDataItem.symbol === symbolProfile.symbol
|
||||
);
|
||||
})?._count ?? 0;
|
||||
const sectorsCount = symbolProfile.sectors
|
||||
? Object.keys(symbolProfile.sectors).length
|
||||
: 0;
|
||||
|
||||
return {
|
||||
countriesCount,
|
||||
marketDataItemCount,
|
||||
sectorsCount,
|
||||
activityCount: symbolProfile._count.Order,
|
||||
assetClass: symbolProfile.assetClass,
|
||||
assetSubClass: symbolProfile.assetSubClass,
|
||||
dataSource: symbolProfile.dataSource,
|
||||
date: symbolProfile.Order?.[0]?.date,
|
||||
symbol: symbolProfile.symbol
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
@ -48,9 +48,13 @@ export class BenchmarkService {
|
||||
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
||||
const { marketPrice } = quotes[benchmarkAssets[index].symbol];
|
||||
|
||||
const performancePercentFromAllTimeHigh = new Big(marketPrice)
|
||||
.div(allTimeHigh)
|
||||
.minus(1);
|
||||
let performancePercentFromAllTimeHigh = new Big(0);
|
||||
|
||||
if (allTimeHigh) {
|
||||
performancePercentFromAllTimeHigh = new Big(marketPrice)
|
||||
.div(allTimeHigh)
|
||||
.minus(1);
|
||||
}
|
||||
|
||||
return {
|
||||
marketCondition: this.getMarketCondition(
|
||||
|
81
apps/api/src/app/frontend.middleware.ts
Normal file
81
apps/api/src/app/frontend.middleware.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class FrontendMiddleware implements NestMiddleware {
|
||||
public indexHtmlDe = fs.readFileSync(
|
||||
this.getPathOfIndexHtmlFile('de'),
|
||||
'utf8'
|
||||
);
|
||||
public indexHtmlEn = fs.readFileSync(
|
||||
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService
|
||||
) {}
|
||||
|
||||
public use(req: Request, res: Response, next: NextFunction) {
|
||||
let featureGraphicPath = 'assets/cover.png';
|
||||
|
||||
if (
|
||||
req.path === '/en/blog/2022/08/500-stars-on-github' ||
|
||||
req.path === '/en/blog/2022/08/500-stars-on-github/'
|
||||
) {
|
||||
featureGraphicPath = 'assets/images/blog/500-stars-on-github.jpg';
|
||||
}
|
||||
|
||||
if (req.path.startsWith('/api/') || this.isFileRequest(req.url)) {
|
||||
// Skip
|
||||
next();
|
||||
} else if (req.path === '/de' || req.path.startsWith('/de/')) {
|
||||
res.send(
|
||||
this.interpolate(this.indexHtmlDe, {
|
||||
featureGraphicPath,
|
||||
languageCode: 'de',
|
||||
path: req.path,
|
||||
rootUrl: this.configurationService.get('ROOT_URL')
|
||||
})
|
||||
);
|
||||
} else {
|
||||
res.send(
|
||||
this.interpolate(this.indexHtmlEn, {
|
||||
featureGraphicPath,
|
||||
languageCode: DEFAULT_LANGUAGE_CODE,
|
||||
path: req.path,
|
||||
rootUrl: this.configurationService.get('ROOT_URL')
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private getPathOfIndexHtmlFile(aLocale: string) {
|
||||
return path.join(__dirname, '..', 'client', aLocale, 'index.html');
|
||||
}
|
||||
|
||||
private interpolate(template: string, context: any) {
|
||||
return template.replace(/[$]{([^}]+)}/g, (_, objectPath) => {
|
||||
const properties = objectPath.split('.');
|
||||
return properties.reduce(
|
||||
(previous, current) => previous?.[current],
|
||||
context
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private isFileRequest(filename: string) {
|
||||
if (filename === '/assets/LICENSE') {
|
||||
return true;
|
||||
} else if (filename.includes('auth/ey')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return filename.split('.').pop() !== filename;
|
||||
}
|
||||
}
|
@ -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: [
|
||||
{
|
||||
|
@ -9,6 +9,10 @@ export class UpdateUserSettingDto {
|
||||
@IsOptional()
|
||||
isRestrictedView?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
language?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
locale?: string;
|
||||
|
@ -24,8 +24,8 @@
|
||||
class="cursor-pointer d-inline-block info-message px-3 py-2"
|
||||
(click)="onCreateAccount()"
|
||||
>
|
||||
<span i18n>You are using the Live Demo.</span>
|
||||
<span class="a ml-2" i18n>Create Account</span>
|
||||
<span>You are using the Live Demo.</span>
|
||||
<span class="a ml-2">Create Account</span>
|
||||
</div></a
|
||||
>
|
||||
<div
|
||||
|
@ -43,8 +43,8 @@
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #transactionMenu="matMenu" xPosition="before">
|
||||
<button i18n mat-menu-item (click)="onDeleteAccess(element.id)">
|
||||
Revoke
|
||||
<button mat-menu-item (click)="onDeleteAccess(element.id)">
|
||||
<ng-container i18n>Revoke</ng-container>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
|
@ -46,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) {
|
||||
|
@ -21,18 +21,10 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Account Type"
|
||||
size="medium"
|
||||
[value]="accountType"
|
||||
></gf-value>
|
||||
<gf-value size="medium" [value]="accountType">Account Type</gf-value>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Platform"
|
||||
size="medium"
|
||||
[value]="platformName"
|
||||
></gf-value>
|
||||
<gf-value size="medium" [value]="platformName">Platform</gf-value>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -19,13 +19,8 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="currency">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
i18n
|
||||
mat-header-cell
|
||||
>
|
||||
Currency
|
||||
<th *matHeaderCellDef class="d-none d-lg-table-cell px-1" mat-header-cell>
|
||||
<ng-container i18n>Currency</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||
{{ element.currency }}
|
||||
@ -36,13 +31,8 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="platform">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
i18n
|
||||
mat-header-cell
|
||||
>
|
||||
Platform
|
||||
<th *matHeaderCellDef class="d-none d-lg-table-cell px-1" mat-header-cell>
|
||||
<ng-container i18n>Platform</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||
<div class="d-flex">
|
||||
@ -81,10 +71,9 @@
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1 text-right"
|
||||
i18n
|
||||
mat-header-cell
|
||||
>
|
||||
Cash Balance
|
||||
<ng-container i18n>Cash Balance</ng-container>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
@ -116,10 +105,9 @@
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1 text-right"
|
||||
i18n
|
||||
mat-header-cell
|
||||
>
|
||||
Value
|
||||
<ng-container i18n>Value</ng-container>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
@ -151,10 +139,9 @@
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-lg-none d-xl-none px-1 text-right"
|
||||
i18n
|
||||
mat-header-cell
|
||||
>
|
||||
Value
|
||||
<ng-container i18n>Value</ng-container>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
|
@ -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);
|
||||
|
@ -24,7 +24,7 @@
|
||||
<table class="gf-table w-100">
|
||||
<thead>
|
||||
<tr class="mat-header-row">
|
||||
<th class="mat-header-cell px-1 py-2 text-right" i18n>#</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-right">#</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Type</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Symbol</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Data Source</th>
|
||||
@ -105,19 +105,18 @@
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<button i18n mat-menu-item (click)="onViewData(job.data)">
|
||||
View Data
|
||||
<button mat-menu-item (click)="onViewData(job.data)">
|
||||
<ng-container i18n>View Data</ng-container>
|
||||
</button>
|
||||
<button
|
||||
i18n
|
||||
mat-menu-item
|
||||
[disabled]="job.stacktrace?.length <= 0"
|
||||
(click)="onViewStacktrace(job.stacktrace)"
|
||||
>
|
||||
View Stacktrace
|
||||
<ng-container i18n>View Stacktrace</ng-container>
|
||||
</button>
|
||||
<button i18n mat-menu-item (click)="onDeleteJob(job.id)">
|
||||
Delete Job
|
||||
<button mat-menu-item (click)="onDeleteJob(job.id)">
|
||||
<ng-container i18n>Delete Job</ng-container>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
|
@ -43,8 +43,8 @@
|
||||
</div>
|
||||
<div class="justify-content-end" mat-dialog-actions>
|
||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||
<button color="primary" i18n mat-flat-button (click)="onUpdate()">
|
||||
Save
|
||||
<button color="primary" mat-flat-button (click)="onUpdate()">
|
||||
<ng-container i18n>Save</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -3,17 +3,27 @@ import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||
import { UniqueAsset, User } from '@ghostfolio/common/interfaces';
|
||||
import { DATE_FORMAT, getDateFormatString } from '@ghostfolio/common/helper';
|
||||
import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces';
|
||||
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
|
||||
import { DataSource, MarketData } from '@prisma/client';
|
||||
import { AssetSubClass, DataSource } from '@prisma/client';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { AssetProfileDialog } from './asset-profile-dialog/asset-profile-dialog.component';
|
||||
import { AssetProfileDialogParams } from './asset-profile-dialog/interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@ -22,11 +32,46 @@ import { takeUntil } from 'rxjs/operators';
|
||||
templateUrl: './admin-market-data.html'
|
||||
})
|
||||
export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
public activeFilters: Filter[] = [];
|
||||
public allFilters: Filter[] = [
|
||||
AssetSubClass.BOND,
|
||||
AssetSubClass.COMMODITY,
|
||||
AssetSubClass.CRYPTOCURRENCY,
|
||||
AssetSubClass.ETF,
|
||||
AssetSubClass.MUTUALFUND,
|
||||
AssetSubClass.PRECIOUS_METAL,
|
||||
AssetSubClass.PRIVATE_EQUITY,
|
||||
AssetSubClass.STOCK
|
||||
].map((id) => {
|
||||
return {
|
||||
id,
|
||||
label: id,
|
||||
type: 'ASSET_SUB_CLASS'
|
||||
};
|
||||
});
|
||||
public currentDataSource: DataSource;
|
||||
public currentSymbol: string;
|
||||
public dataSource: MatTableDataSource<AdminMarketDataItem> =
|
||||
new MatTableDataSource();
|
||||
public defaultDateFormat: string;
|
||||
public marketData: AdminMarketDataItem[] = [];
|
||||
public marketDataDetails: MarketData[] = [];
|
||||
public deviceType: string;
|
||||
public displayedColumns = [
|
||||
'symbol',
|
||||
'dataSource',
|
||||
'assetClass',
|
||||
'assetSubClass',
|
||||
'date',
|
||||
'activityCount',
|
||||
'marketDataItemCount',
|
||||
'countriesCount',
|
||||
'sectorsCount',
|
||||
'actions'
|
||||
];
|
||||
public filters$ = new Subject<Filter[]>();
|
||||
public isLoading = false;
|
||||
public placeholder = '';
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -35,8 +80,29 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
||||
private adminService: AdminService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private dialog: MatDialog,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (
|
||||
params['assetProfileDialog'] &&
|
||||
params['dataSource'] &&
|
||||
params['dateOfFirstActivity'] &&
|
||||
params['symbol']
|
||||
) {
|
||||
this.openAssetProfileDialog({
|
||||
dataSource: params['dataSource'],
|
||||
dateOfFirstActivity: params['dateOfFirstActivity'],
|
||||
symbol: params['symbol']
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
@ -51,7 +117,31 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.fetchAdminMarketData();
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.filters$
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((filters) => {
|
||||
this.isLoading = true;
|
||||
this.activeFilters = filters;
|
||||
this.placeholder =
|
||||
this.activeFilters.length <= 0 ? $localize`Filter by...` : '';
|
||||
|
||||
return this.dataService.fetchAdminMarketData({
|
||||
filters: this.activeFilters
|
||||
});
|
||||
}),
|
||||
takeUntil(this.unsubscribeSubject)
|
||||
)
|
||||
.subscribe(({ marketData }) => {
|
||||
this.dataSource = new MatTableDataSource(marketData);
|
||||
this.dataSource.sort = this.sort;
|
||||
|
||||
this.isLoading = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) {
|
||||
@ -75,28 +165,19 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
||||
.subscribe(() => {});
|
||||
}
|
||||
|
||||
public onMarketDataChanged(withRefresh: boolean = false) {
|
||||
if (withRefresh) {
|
||||
this.fetchAdminMarketData();
|
||||
this.fetchAdminMarketDataBySymbol({
|
||||
dataSource: this.currentDataSource,
|
||||
symbol: this.currentSymbol
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public setCurrentProfile({ dataSource, symbol }: UniqueAsset) {
|
||||
this.marketDataDetails = [];
|
||||
|
||||
if (this.currentSymbol === symbol) {
|
||||
this.currentDataSource = undefined;
|
||||
this.currentSymbol = '';
|
||||
} else {
|
||||
this.currentDataSource = dataSource;
|
||||
this.currentSymbol = symbol;
|
||||
|
||||
this.fetchAdminMarketDataBySymbol({ dataSource, symbol });
|
||||
}
|
||||
public onOpenAssetProfileDialog({
|
||||
dataSource,
|
||||
dateOfFirstActivity,
|
||||
symbol
|
||||
}: UniqueAsset & { dateOfFirstActivity: string }) {
|
||||
this.router.navigate([], {
|
||||
queryParams: {
|
||||
dataSource,
|
||||
symbol,
|
||||
assetProfileDialog: true,
|
||||
dateOfFirstActivity: format(parseISO(dateOfFirstActivity), DATE_FORMAT)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
@ -104,25 +185,40 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private fetchAdminMarketData() {
|
||||
this.dataService
|
||||
.fetchAdminMarketData()
|
||||
private openAssetProfileDialog({
|
||||
dataSource,
|
||||
dateOfFirstActivity,
|
||||
symbol
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
dateOfFirstActivity: string;
|
||||
symbol: string;
|
||||
}) {
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ marketData }) => {
|
||||
this.marketData = marketData;
|
||||
.subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
const dialogRef = this.dialog.open(AssetProfileDialog, {
|
||||
autoFocus: false,
|
||||
data: <AssetProfileDialogParams>{
|
||||
dataSource,
|
||||
dateOfFirstActivity,
|
||||
symbol,
|
||||
deviceType: this.deviceType,
|
||||
locale: this.user?.settings?.locale
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
private fetchAdminMarketDataBySymbol({ dataSource, symbol }: UniqueAsset) {
|
||||
this.adminService
|
||||
.fetchAdminMarketDataBySymbol({ dataSource, symbol })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ marketData }) => {
|
||||
this.marketDataDetails = marketData;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,79 +1,147 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="gf-table w-100">
|
||||
<thead>
|
||||
<tr class="mat-header-row">
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Symbol</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Data Source</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>First Activity</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Activity Count</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Historical Data</th>
|
||||
<th class="mat-header-cell px-1 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ng-container *ngFor="let item of marketData; let i = index">
|
||||
<tr
|
||||
class="cursor-pointer mat-row"
|
||||
(click)="setCurrentProfile({ dataSource: item.dataSource, symbol: item.symbol })"
|
||||
<gf-activities-filter
|
||||
[allFilters]="allFilters"
|
||||
[isLoading]="isLoading"
|
||||
[placeholder]="placeholder"
|
||||
(valueChanged)="filters$.next($event)"
|
||||
></gf-activities-filter>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table
|
||||
class="gf-table w-100"
|
||||
matSort
|
||||
matSortActive="symbol"
|
||||
matSortDirection="asc"
|
||||
mat-table
|
||||
[dataSource]="dataSource"
|
||||
>
|
||||
<ng-container matColumnDef="symbol">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Symbol</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.symbol }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="dataSource">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Data Source</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.dataSource }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="assetClass">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Asset Class</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.assetClass }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="assetSubClass">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Asset Sub Class</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.assetSubClass }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="date">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>First Activity</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ (element.date | date: defaultDateFormat) ?? '' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="activityCount">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Activity Count</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
{{ element.activityCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="marketDataItemCount">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Historical Data</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
{{ element.marketDataItemCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="countriesCount">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Countries Count</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
{{ element.countriesCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="sectorsCount">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Sectors Count</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
{{ element.sectorsCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th>
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="accountMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<td class="mat-cell px-1 py-2">{{ item.symbol }}</td>
|
||||
<td class="mat-cell px-1 py-2">{{ item.dataSource }}</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
{{ (item.date | date: defaultDateFormat) ?? '' }}
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2">{{ item.activityCount }}</td>
|
||||
<td class="mat-cell px-1 py-2">{{ item.marketDataItemCount }}</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="accountMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<button
|
||||
i18n
|
||||
mat-menu-item
|
||||
(click)="onGatherSymbol({dataSource: item.dataSource, symbol: item.symbol})"
|
||||
>
|
||||
Gather Data
|
||||
</button>
|
||||
<button
|
||||
i18n
|
||||
mat-menu-item
|
||||
(click)="onGatherProfileDataBySymbol({dataSource: item.dataSource, symbol: item.symbol})"
|
||||
>
|
||||
Gather Profile Data
|
||||
</button>
|
||||
<button
|
||||
i18n
|
||||
mat-menu-item
|
||||
[disabled]="item.activityCount !== 0"
|
||||
(click)="onDeleteProfileData({dataSource: item.dataSource, symbol: item.symbol})"
|
||||
>
|
||||
Delete Profile Data
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="currentSymbol === item.symbol" class="mat-row">
|
||||
<td class="p-1" colspan="6">
|
||||
<gf-admin-market-data-detail
|
||||
[dataSource]="item.dataSource"
|
||||
[dateOfFirstActivity]="item.date"
|
||||
[locale]="user?.settings?.locale"
|
||||
[marketData]="marketDataDetails"
|
||||
[symbol]="item.symbol"
|
||||
(marketDataChanged)="onMarketDataChanged($event)"
|
||||
></gf-admin-market-data-detail>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onGatherSymbol({dataSource: element.dataSource, symbol: element.symbol})"
|
||||
>
|
||||
<ng-container i18n>Gather Data</ng-container>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onGatherProfileDataBySymbol({dataSource: element.dataSource, symbol: element.symbol})"
|
||||
>
|
||||
<ng-container i18n>Gather Profile Data</ng-container>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="element.activityCount !== 0"
|
||||
(click)="onDeleteProfileData({dataSource: element.dataSource, symbol: element.symbol})"
|
||||
>
|
||||
<ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
class="cursor-pointer"
|
||||
mat-row
|
||||
(click)="onOpenAssetProfileDialog({ dateOfFirstActivity: row.date, dataSource: row.dataSource, symbol: row.symbol })"
|
||||
></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,17 +2,23 @@ import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
|
||||
|
||||
import { AdminMarketDataComponent } from './admin-market-data.component';
|
||||
import { GfAssetProfileDialogModule } from './asset-profile-dialog/assset-profile-dialog.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdminMarketDataComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfAdminMarketDataDetailModule,
|
||||
GfActivitiesFilterModule,
|
||||
GfAssetProfileDialogModule,
|
||||
MatButtonModule,
|
||||
MatMenuModule
|
||||
MatMenuModule,
|
||||
MatSortModule,
|
||||
MatTableModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
|
@ -0,0 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-dialog-content {
|
||||
max-height: unset;
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Inject,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import { MarketData } from '@prisma/client';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { AssetProfileDialogParams } from './interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
host: { class: 'd-flex flex-column h-100' },
|
||||
selector: 'gf-asset-profile-dialog',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: 'asset-profile-dialog.html',
|
||||
styleUrls: ['./asset-profile-dialog.component.scss']
|
||||
})
|
||||
export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
public marketDataDetails: MarketData[] = [];
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private adminService: AdminService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
public dialogRef: MatDialogRef<AssetProfileDialog>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams
|
||||
) {}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
public onClose(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
public onMarketDataChanged(withRefresh: boolean = false) {
|
||||
if (withRefresh) {
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private fetchAdminMarketDataBySymbol({ dataSource, symbol }: UniqueAsset) {
|
||||
this.adminService
|
||||
.fetchAdminMarketDataBySymbol({ dataSource, symbol })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ marketData }) => {
|
||||
this.marketDataDetails = marketData;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
this.fetchAdminMarketDataBySymbol({
|
||||
dataSource: this.data.dataSource,
|
||||
symbol: this.data.symbol
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<gf-dialog-header
|
||||
mat-dialog-title
|
||||
position="center"
|
||||
[deviceType]="data.deviceType"
|
||||
[title]="data.symbol"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-header>
|
||||
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<gf-admin-market-data-detail
|
||||
[dataSource]="data.dataSource"
|
||||
[dateOfFirstActivity]="data.dateOfFirstActivity"
|
||||
[locale]="data.locale"
|
||||
[marketData]="marketDataDetails"
|
||||
[symbol]="data.symbol"
|
||||
(marketDataChanged)="onMarketDataChanged($event)"
|
||||
></gf-admin-market-data-detail>
|
||||
</div>
|
||||
|
||||
<gf-dialog-footer
|
||||
mat-dialog-actions
|
||||
[deviceType]="data.deviceType"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-footer>
|
@ -0,0 +1,23 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
|
||||
import { AssetProfileDialog } from './asset-profile-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AssetProfileDialog],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfAdminMarketDataDetailModule,
|
||||
GfDialogFooterModule,
|
||||
GfDialogHeaderModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAssetProfileDialogModule {}
|
@ -0,0 +1,9 @@
|
||||
import { DataSource } from '@prisma/client';
|
||||
|
||||
export interface AssetProfileDialogParams {
|
||||
dateOfFirstActivity: string;
|
||||
dataSource: DataSource;
|
||||
deviceType: string;
|
||||
locale: string;
|
||||
symbol: string;
|
||||
}
|
@ -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);
|
||||
|
@ -8,7 +8,7 @@
|
||||
<div class="w-50">{{ userCount }}</div>
|
||||
</div>
|
||||
<div class="d-flex my-3">
|
||||
<div class="w-50" i18n>Transaction Count</div>
|
||||
<div class="w-50" i18n>Activity Count</div>
|
||||
<div class="w-50">
|
||||
<ng-container *ngIf="transactionCount">
|
||||
{{ transactionCount }} ({{ transactionCount / userCount | number
|
||||
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex my-3">
|
||||
<div class="w-50" i18n>Data Gathering</div>
|
||||
<div class="w-50" i18n>Data Management</div>
|
||||
<div class="w-50">
|
||||
<div class="overflow-hidden">
|
||||
<div class="mb-2">
|
||||
|
@ -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
|
||||
|
@ -7,17 +7,17 @@
|
||||
<tr class="mat-header-row">
|
||||
<th class="mat-header-cell px-1 py-2 text-right">#</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>User</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-right" i18n>
|
||||
Registration
|
||||
<th class="mat-header-cell px-1 py-2 text-right">
|
||||
<ng-container i18n>Registration</ng-container>
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-right" i18n>
|
||||
Accounts
|
||||
<th class="mat-header-cell px-1 py-2 text-right">
|
||||
<ng-container i18n>Accounts</ng-container>
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-right" i18n>
|
||||
Activities
|
||||
<th class="mat-header-cell px-1 py-2 text-right">
|
||||
<ng-container i18n>Activities</ng-container>
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-right" i18n>
|
||||
Engagement per Day
|
||||
<th class="mat-header-cell px-1 py-2 text-right">
|
||||
<ng-container i18n>Engagement per Day</ng-container>
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Last Request</th>
|
||||
<th class="mat-header-cell px-1 py-2"></th>
|
||||
|
@ -285,17 +285,16 @@
|
||||
mat-flat-button
|
||||
><ion-icon name="logo-github"></ion-icon
|
||||
></a>
|
||||
<button class="mx-1" i18n mat-flat-button (click)="openLoginDialog()">
|
||||
Sign In
|
||||
<button class="mx-1" mat-flat-button (click)="openLoginDialog()">
|
||||
<ng-container i18n>Sign in</ng-container>
|
||||
</button>
|
||||
<a
|
||||
*ngIf="currentRoute !== 'register' && !info?.isReadOnlyMode"
|
||||
class="d-none d-sm-block"
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[routerLink]="['/register']"
|
||||
>Get Started
|
||||
><ng-container i18n>Get started</ng-container>
|
||||
</a>
|
||||
</ng-container>
|
||||
</mat-toolbar>
|
||||
|
@ -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;
|
||||
@ -47,7 +47,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
private settingsStorageService: SettingsStorageService,
|
||||
private userService: UserService
|
||||
) {
|
||||
route.queryParams
|
||||
this.route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (
|
||||
|
@ -21,6 +21,8 @@ import { takeUntil } from 'rxjs/operators';
|
||||
export class HomeMarketComponent implements OnDestroy, OnInit {
|
||||
public benchmarks: Benchmark[];
|
||||
public fearAndGreedIndex: number;
|
||||
public fearLabel = $localize`Fear`;
|
||||
public greedLabel = $localize`Greed`;
|
||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||
public historicalData: HistoricalDataItem[];
|
||||
public info: InfoItem;
|
||||
|
@ -9,13 +9,13 @@
|
||||
class="mb-3"
|
||||
symbol="Fear & Greed Index"
|
||||
yMax="100"
|
||||
yMaxLabel="Greed"
|
||||
yMin="0"
|
||||
yMinLabel="Fear"
|
||||
[historicalDataItems]="historicalData"
|
||||
[locale]="user?.settings?.locale"
|
||||
[showXAxis]="true"
|
||||
[showYAxis]="true"
|
||||
[yMaxLabel]="greedLabel"
|
||||
[yMinLabel]="fearLabel"
|
||||
></gf-line-chart>
|
||||
<gf-fear-and-greed-index
|
||||
class="d-flex justify-content-center"
|
||||
|
@ -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;
|
||||
|
@ -122,7 +122,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
data: this.investments.map((position) => {
|
||||
return position.investment;
|
||||
}),
|
||||
label: 'Investment',
|
||||
label: $localize`Deposit`,
|
||||
segment: {
|
||||
borderColor: (context: unknown) =>
|
||||
this.isInFuture(
|
||||
|
@ -49,12 +49,11 @@
|
||||
<div>
|
||||
<button
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[disabled]="!data.accessToken"
|
||||
[mat-dialog-close]="data"
|
||||
>
|
||||
Sign in
|
||||
<ng-container i18n>Sign in</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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());
|
||||
|
@ -35,112 +35,124 @@
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Change"
|
||||
i18n
|
||||
size="medium"
|
||||
[colorizeSign]="true"
|
||||
[currency]="data.baseCurrency"
|
||||
[locale]="data.locale"
|
||||
[value]="netPerformance"
|
||||
></gf-value>
|
||||
>Change</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Performance"
|
||||
i18n
|
||||
size="medium"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="data.locale"
|
||||
[value]="netPerformancePercent"
|
||||
></gf-value>
|
||||
>Performance</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Average Unit Price"
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="SymbolProfile?.currency"
|
||||
[locale]="data.locale"
|
||||
[value]="averagePrice"
|
||||
></gf-value>
|
||||
>Average Unit Price</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Market Price"
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="SymbolProfile?.currency"
|
||||
[locale]="data.locale"
|
||||
[value]="marketPrice"
|
||||
></gf-value>
|
||||
>Market Price</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Minimum Price"
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="SymbolProfile?.currency"
|
||||
[locale]="data.locale"
|
||||
[ngClass]="{ 'text-danger': minPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }"
|
||||
[value]="minPrice"
|
||||
></gf-value>
|
||||
>Minimum Price</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Maximum Price"
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="SymbolProfile?.currency"
|
||||
[locale]="data.locale"
|
||||
[ngClass]="{ 'text-success': maxPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }"
|
||||
[value]="maxPrice"
|
||||
></gf-value>
|
||||
>Maximum Price</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Quantity"
|
||||
i18n
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[precision]="quantityPrecision"
|
||||
[value]="quantity"
|
||||
></gf-value>
|
||||
>Quantity</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Investment"
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="data.baseCurrency"
|
||||
[locale]="data.locale"
|
||||
[value]="investment"
|
||||
></gf-value>
|
||||
>Investment</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="First Buy Date"
|
||||
i18n
|
||||
size="medium"
|
||||
[isDate]="true"
|
||||
[locale]="data.locale"
|
||||
[value]="firstBuyDate"
|
||||
></gf-value>
|
||||
>First Buy Date</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[label]="transactionCount === 1 ? 'Transaction' : 'Transactions'"
|
||||
[locale]="data.locale"
|
||||
[value]="transactionCount"
|
||||
></gf-value>
|
||||
>Transactions</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Asset Class"
|
||||
i18n
|
||||
size="medium"
|
||||
[hidden]="!SymbolProfile?.assetClass"
|
||||
[value]="SymbolProfile?.assetClass"
|
||||
></gf-value>
|
||||
>Asset Class</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Asset Sub Class"
|
||||
i18n
|
||||
size="medium"
|
||||
[hidden]="!SymbolProfile?.assetSubClass"
|
||||
[value]="SymbolProfile?.assetSubClass"
|
||||
></gf-value>
|
||||
>Asset Sub Class</gf-value
|
||||
>
|
||||
</div>
|
||||
<ng-container
|
||||
*ngIf="SymbolProfile?.countries?.length > 0 || SymbolProfile?.sectors?.length > 0"
|
||||
@ -150,22 +162,24 @@
|
||||
>
|
||||
<div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3">
|
||||
<gf-value
|
||||
label="Sector"
|
||||
i18n
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[value]="SymbolProfile.sectors[0].name"
|
||||
></gf-value>
|
||||
>Sector</gf-value
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="SymbolProfile?.countries?.length === 1"
|
||||
class="col-6 mb-3"
|
||||
>
|
||||
<gf-value
|
||||
label="Country"
|
||||
i18n
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[value]="SymbolProfile.countries[0].name"
|
||||
></gf-value>
|
||||
>Country</gf-value
|
||||
>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #charts>
|
||||
|
@ -18,8 +18,8 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="symbol">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
||||
Symbol
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Symbol</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
<span [title]="element.name">{{ element.symbol | gfSymbol }}</span>
|
||||
@ -30,11 +30,10 @@
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
i18n
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
Name
|
||||
<ng-container i18n>Name</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||
<ng-container *ngIf="element.name !== element.symbol">{{
|
||||
@ -47,11 +46,10 @@
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||
i18n
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
Value
|
||||
<ng-container i18n>Value</ng-container>
|
||||
</th>
|
||||
<td class="d-none d-lg-table-cell px-1" mat-cell *matCellDef="let element">
|
||||
<div class="d-flex justify-content-end">
|
||||
@ -68,11 +66,10 @@
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="justify-content-end px-1"
|
||||
i18n
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
Allocation
|
||||
<ng-container i18n>Allocation</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
<div class="d-flex justify-content-end">
|
||||
@ -89,10 +86,9 @@
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1 text-right"
|
||||
i18n
|
||||
mat-header-cell
|
||||
>
|
||||
Performance
|
||||
<ng-container i18n>Performance</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||
<div class="d-flex justify-content-end">
|
||||
@ -137,8 +133,8 @@
|
||||
*ngIf="dataSource.data.length > pageSize && !isLoading"
|
||||
class="my-3 text-center"
|
||||
>
|
||||
<button i18n mat-stroked-button (click)="onShowAllPositions()">
|
||||
Show all
|
||||
<button mat-stroked-button (click)="onShowAllPositions()">
|
||||
<ng-container i18n>Show all</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -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[];
|
||||
|
@ -72,7 +72,13 @@ export class AuthGuard implements CanActivate {
|
||||
})
|
||||
)
|
||||
.subscribe((user) => {
|
||||
if (
|
||||
const userLanguage = user?.settings?.language;
|
||||
|
||||
if (userLanguage && document.documentElement.lang !== userLanguage) {
|
||||
window.location.href = `../${userLanguage}`;
|
||||
resolve(false);
|
||||
return;
|
||||
} else if (
|
||||
state.url.startsWith('/home') &&
|
||||
user.settings.viewMode === ViewMode.ZEN
|
||||
) {
|
||||
|
@ -56,14 +56,18 @@ 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.` +
|
||||
' ' +
|
||||
$localize`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 +83,10 @@ 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.` +
|
||||
' ' +
|
||||
$localize`Please try again later.`,
|
||||
$localize`Okay`,
|
||||
{ duration: 6000 }
|
||||
);
|
||||
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: AboutPageComponent,
|
||||
path: '',
|
||||
title: 'About'
|
||||
title: $localize`About`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>About Ghostfolio</h3>
|
||||
<h3 class="d-flex justify-content-center mb-3">About Ghostfolio</h3>
|
||||
<div class="about-container">
|
||||
<p>
|
||||
Ghostfolio is a lightweight wealth management application for
|
||||
@ -21,7 +21,7 @@
|
||||
<ng-container *ngIf="version">
|
||||
This instance is running Ghostfolio {{ version }}.
|
||||
</ng-container>
|
||||
<ng-container *ngIf="hasPermissionForStatistics" i18n
|
||||
<ng-container *ngIf="hasPermissionForStatistics"
|
||||
>Check the system status at
|
||||
<a href="https://status.ghostfol.io" title="Ghostfolio status"
|
||||
>status.ghostfol.io</a
|
||||
@ -102,33 +102,36 @@
|
||||
|
||||
<div *ngIf="hasPermissionForStatistics" class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>Ghostfolio in Numbers</h3>
|
||||
<h3 class="mb-3 text-center">Ghostfolio in Numbers</h3>
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
label="Active Users"
|
||||
i18n
|
||||
size="large"
|
||||
subLabel="(Last 24 hours)"
|
||||
[value]="statistics?.activeUsers1d ?? '-'"
|
||||
></gf-value>
|
||||
>Active Users</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
label="New Users"
|
||||
i18n
|
||||
size="large"
|
||||
subLabel="(Last 30 days)"
|
||||
[value]="statistics?.newUsers30d ?? '-'"
|
||||
></gf-value>
|
||||
>New Users</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
label="Active Users"
|
||||
i18n
|
||||
size="large"
|
||||
subLabel="(Last 30 days)"
|
||||
[value]="statistics?.activeUsers30d ?? '-'"
|
||||
></gf-value>
|
||||
>Active Users</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<a
|
||||
@ -136,10 +139,11 @@
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
>
|
||||
<gf-value
|
||||
label="Users in Slack community"
|
||||
i18n
|
||||
size="large"
|
||||
[value]="statistics?.slackCommunityUsers ?? '-'"
|
||||
></gf-value>
|
||||
>Users in Slack community</gf-value
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
@ -148,10 +152,11 @@
|
||||
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
|
||||
>
|
||||
<gf-value
|
||||
label="Contributors on GitHub"
|
||||
i18n
|
||||
size="large"
|
||||
[value]="statistics?.gitHubContributors ?? '-'"
|
||||
></gf-value>
|
||||
>Contributors on GitHub</gf-value
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
@ -160,10 +165,11 @@
|
||||
href="https://github.com/ghostfolio/ghostfolio/stargazers"
|
||||
>
|
||||
<gf-value
|
||||
label="Stars on GitHub"
|
||||
i18n
|
||||
size="large"
|
||||
[value]="statistics?.gitHubStargazers ?? '-'"
|
||||
></gf-value>
|
||||
>Stars on GitHub</gf-value
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -177,7 +183,6 @@
|
||||
<a
|
||||
class="py-2 w-100"
|
||||
color="primary"
|
||||
i18n
|
||||
mat-stroked-button
|
||||
[routerLink]="['/faq']"
|
||||
>FAQ</a
|
||||
@ -190,7 +195,6 @@
|
||||
<a
|
||||
class="py-2 w-100"
|
||||
color="primary"
|
||||
i18n
|
||||
mat-stroked-button
|
||||
[routerLink]="['/about', 'changelog']"
|
||||
>Changelog & License</a
|
||||
@ -200,7 +204,6 @@
|
||||
<a
|
||||
class="py-2 w-100"
|
||||
color="primary"
|
||||
i18n
|
||||
mat-stroked-button
|
||||
[routerLink]="['/about', 'privacy-policy']"
|
||||
>Privacy Policy</a
|
||||
@ -210,7 +213,6 @@
|
||||
<a
|
||||
class="py-2 w-100"
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[routerLink]="['/blog']"
|
||||
>Blog</a
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: ChangelogPageComponent,
|
||||
path: '',
|
||||
title: 'Changelog & License'
|
||||
title: $localize`Changelog & License`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: PrivacyPolicyPageComponent,
|
||||
path: '',
|
||||
title: 'Privacy Policy'
|
||||
title: $localize`Privacy Policy`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: AccountPageComponent,
|
||||
path: '',
|
||||
title: 'My Ghostfolio'
|
||||
title: $localize`My Ghostfolio`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -53,6 +53,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
public hasPermissionToDeleteAccess: boolean;
|
||||
public hasPermissionToUpdateViewMode: boolean;
|
||||
public hasPermissionToUpdateUserSettings: boolean;
|
||||
public language = document.documentElement.lang;
|
||||
public locales = ['de', 'de-CH', 'en-GB', 'en-US'];
|
||||
public price: number;
|
||||
public priceId: string;
|
||||
@ -162,6 +163,14 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
this.user = user;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
|
||||
if (aKey === 'language') {
|
||||
if (aValue) {
|
||||
window.location.href = `../${aValue}/account`;
|
||||
} else {
|
||||
window.location.href = `../`;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -218,7 +227,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public onRedeemCoupon() {
|
||||
let couponCode = prompt('Please enter your coupon code:');
|
||||
let couponCode = prompt($localize`Please enter your coupon code:`);
|
||||
couponCode = couponCode?.trim();
|
||||
|
||||
if (couponCode) {
|
||||
@ -227,17 +236,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 +296,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) {
|
||||
|
@ -31,11 +31,10 @@
|
||||
<ng-container *ngIf="hasPermissionForSubscription">
|
||||
<button
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
(click)="onCheckout(priceId)"
|
||||
>
|
||||
Upgrade
|
||||
<ng-container i18n>Upgrade</ng-container>
|
||||
</button>
|
||||
<div *ngIf="price" class="mt-1">
|
||||
<ng-container *ngIf="coupon"
|
||||
@ -91,8 +90,8 @@
|
||||
<div class="d-flex mt-4 py-1">
|
||||
<form #changeUserSettingsForm="ngForm" class="w-100">
|
||||
<div class="d-flex mb-2">
|
||||
<div class="align-items-center d-flex pt-1 pt-1 w-50" i18n>
|
||||
Base Currency
|
||||
<div class="align-items-center d-flex pt-1 pt-1 w-50">
|
||||
<ng-container i18n>Base Currency</ng-container>
|
||||
</div>
|
||||
<div class="pl-1 w-50">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
@ -111,11 +110,30 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex mb-2">
|
||||
<div class="pr-1 w-50">
|
||||
<div i18n>Language</div>
|
||||
<div class="hint-text text-muted" i18n>Beta</div>
|
||||
</div>
|
||||
<div class="pl-1 w-50">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-select
|
||||
name="language"
|
||||
[value]="language"
|
||||
(selectionChange)="onChangeUserSetting('language', $event.value)"
|
||||
>
|
||||
<mat-option [value]="null"></mat-option>
|
||||
<mat-option value="de">Deutsch</mat-option>
|
||||
<mat-option value="en">English</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex mb-2">
|
||||
<div class="pr-1 w-50">
|
||||
<div i18n>Locale</div>
|
||||
<div class="hint-text text-muted" i18n>
|
||||
Date and number format
|
||||
<div class="hint-text text-muted">
|
||||
<ng-container i18n>Date and number format</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-1 w-50">
|
||||
@ -137,8 +155,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<div class="align-items-center d-flex pr-1 pt-1 w-50" i18n>
|
||||
View Mode
|
||||
<div class="align-items-center d-flex pr-1 pt-1 w-50">
|
||||
<ng-container i18n>View Mode</ng-container>
|
||||
</div>
|
||||
<div class="pl-1 w-50">
|
||||
<div class="align-items-center d-flex overflow-hidden">
|
||||
|
@ -14,12 +14,11 @@
|
||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||
<button
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[disabled]="!addAccessForm.form.valid"
|
||||
[mat-dialog-close]="data"
|
||||
>
|
||||
Save
|
||||
<ng-container i18n>Save</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: AccountsPageComponent,
|
||||
path: '',
|
||||
title: 'Accounts'
|
||||
title: $localize`Accounts`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -66,12 +66,11 @@
|
||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||
<button
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[disabled]="!addAccountForm.form.valid"
|
||||
[mat-dialog-close]="data"
|
||||
>
|
||||
Save
|
||||
<ng-container i18n>Save</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -20,7 +20,7 @@ const routes: Routes = [
|
||||
],
|
||||
component: AdminPageComponent,
|
||||
path: '',
|
||||
title: 'Admin Control'
|
||||
title: $localize`Admin Control`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -4,7 +4,7 @@
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1">500 Stars</h1>
|
||||
<div class="mb-3 text-muted"><small>2022-08-13</small></div>
|
||||
<div class="mb-3 text-muted"><small>2022-08-18</small></div>
|
||||
<img
|
||||
alt="500 Stars on GitHub Teaser"
|
||||
class="rounded w-100"
|
||||
@ -19,8 +19,7 @@
|
||||
<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
|
||||
[routerLink]="['/en', 'blog', '2022', '01', 'ghostfolio-first-months-in-open-source']"
|
||||
<a href="../en/blog/2022/01/ghostfolio-first-months-in-open-source"
|
||||
>recap</a
|
||||
>.
|
||||
</p>
|
||||
@ -56,8 +55,7 @@
|
||||
<h2 class="h4">Ready for Web 3.0</h2>
|
||||
<p>
|
||||
The
|
||||
<a
|
||||
[routerLink]="['/en', 'blog', '2022', '07', 'ghostfolio-meets-internet-identity']"
|
||||
<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
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: BlogPageComponent,
|
||||
path: '',
|
||||
title: 'Blog'
|
||||
title: $localize`Blog`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -8,11 +8,11 @@
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex w-100"
|
||||
[routerLink]="['/blog', '2022', '08', '500-stars-on-github']"
|
||||
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-10</div>
|
||||
<div class="d-flex text-muted">2022-08-18</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
@ -32,7 +32,7 @@
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex w-100"
|
||||
[routerLink]="['/blog', '2022', '07', 'ghostfolio-meets-internet-identity']"
|
||||
href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
|
||||
>
|
||||
<div class="flex-grow-1">
|
||||
<div class="h6 m-0 text-truncate">
|
||||
@ -58,7 +58,7 @@
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex w-100"
|
||||
[routerLink]="['/blog', '2022', '07', 'how-do-i-get-my-finances-in-order']"
|
||||
href="../en/blog/2022/07/how-do-i-get-my-finances-in-order"
|
||||
>
|
||||
<div class="flex-grow-1">
|
||||
<div class="h6 m-0 text-truncate">
|
||||
@ -84,7 +84,7 @@
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex w-100"
|
||||
[routerLink]="['/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">
|
||||
@ -110,7 +110,7 @@
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex w-100"
|
||||
[routerLink]="['/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>
|
||||
@ -134,7 +134,7 @@
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex w-100"
|
||||
[routerLink]="['/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>
|
||||
|
@ -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);
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: FaqPageComponent,
|
||||
path: '',
|
||||
title: 'FAQ'
|
||||
title: $localize`FAQ`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -1,61 +1,57 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>Frequently Asked Questions (FAQ)</h3>
|
||||
<h3 class="mb-3 text-center">Frequently Asked Questions (FAQ)</h3>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n>What is Ghostfolio?</mat-card-title>
|
||||
<mat-card-content i18n>
|
||||
<mat-card-title>What is Ghostfolio?</mat-card-title>
|
||||
<mat-card-content>
|
||||
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
|
||||
<mat-card-title
|
||||
>What assets can I track with Ghostfolio?</mat-card-title
|
||||
>
|
||||
<mat-card-content i18n>
|
||||
<mat-card-content>
|
||||
With Ghostfolio, you can keep track of various assets like stocks,
|
||||
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>
|
||||
<mat-card-title>What else is included in Ghostfolio?</mat-card-title>
|
||||
<mat-card-content>
|
||||
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>
|
||||
<mat-card-title>How do I start?</mat-card-title>
|
||||
<mat-card-content>
|
||||
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
|
||||
[routerLink]="['/en', 'blog', '2022', '07', 'ghostfolio-meets-internet-identity']"
|
||||
<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>
|
||||
<mat-card-title>Can I use Ghostfolio anonymously?</mat-card-title>
|
||||
<mat-card-content>
|
||||
Yes, the authentication systems (via security token or
|
||||
<a
|
||||
[routerLink]="['/en', 'blog', '2022', '07', 'ghostfolio-meets-internet-identity']"
|
||||
<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
|
||||
<mat-card-title>How can Ghostfolio be free?</mat-card-title>
|
||||
<mat-card-content
|
||||
>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
|
||||
@ -66,16 +62,16 @@
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n>Is it really free?</mat-card-title>
|
||||
<mat-card-content i18n
|
||||
<mat-card-title>Is it really free?</mat-card-title>
|
||||
<mat-card-content
|
||||
>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
|
||||
<mat-card-title>What is Ghostfolio Premium?</mat-card-title>
|
||||
<mat-card-content
|
||||
><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
|
||||
@ -83,8 +79,8 @@
|
||||
>
|
||||
</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
|
||||
<mat-card-title>Can I start with a trial version?</mat-card-title>
|
||||
<mat-card-content
|
||||
>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
|
||||
@ -93,8 +89,8 @@
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n>Which devices are supported?</mat-card-title>
|
||||
<mat-card-content i18n
|
||||
<mat-card-title>Which devices are supported?</mat-card-title>
|
||||
<mat-card-content
|
||||
>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
|
||||
@ -106,10 +102,10 @@
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n
|
||||
<mat-card-title
|
||||
>Ghostfolio sounds cool, how can I get involved?</mat-card-title
|
||||
>
|
||||
<mat-card-content i18n
|
||||
<mat-card-content
|
||||
>Any support for Ghostfolio is welcome. Be it with a
|
||||
<a [routerLink]="['/pricing']">Ghostfolio Premium</a> subscription to
|
||||
finance the hosting, a positive rating in the
|
||||
@ -126,8 +122,8 @@
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-title i18n>Got any other questions?</mat-card-title>
|
||||
<mat-card-content i18n
|
||||
<mat-card-title>Got any other questions?</mat-card-title>
|
||||
<mat-card-content
|
||||
>Join the Ghostfolio
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: FeaturesPageComponent,
|
||||
path: '',
|
||||
title: 'Features'
|
||||
title: $localize`Features`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="d-flex justify-content-center mb-3 text-center" i18n>
|
||||
Features
|
||||
</h3>
|
||||
<h3 class="d-flex justify-content-center mb-3 text-center">Features</h3>
|
||||
<div class="mb-4">
|
||||
<p>
|
||||
Check out the numerous features of <strong>Ghostfolio</strong> to
|
||||
@ -14,7 +12,7 @@
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 i18n>Stocks</h4>
|
||||
<h4>Stocks</h4>
|
||||
<p class="m-0">Keep track of your stock purchases and sales.</p>
|
||||
</div>
|
||||
</mat-card>
|
||||
@ -22,7 +20,7 @@
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 i18n>ETFs</h4>
|
||||
<h4>ETFs</h4>
|
||||
<p class="m-0">
|
||||
Are you into ETFs (Exchange Traded Funds)? Track your ETF
|
||||
investments.
|
||||
@ -33,7 +31,7 @@
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 i18n>Bonds</h4>
|
||||
<h4>Bonds</h4>
|
||||
<p class="m-0">
|
||||
Manage your investment in bonds and other assets with fixed
|
||||
income.
|
||||
@ -44,7 +42,7 @@
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 i18n>Cryptocurrencies</h4>
|
||||
<h4>Cryptocurrencies</h4>
|
||||
<p class="m-0">
|
||||
Keep track of your Bitcoin and Altcoin holdings.
|
||||
</p>
|
||||
@ -54,7 +52,7 @@
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 i18n>Dividend</h4>
|
||||
<h4>Dividend</h4>
|
||||
<p class="m-0">
|
||||
Are you building a dividend portfolio? Track your dividend in
|
||||
Ghostfolio.
|
||||
@ -65,7 +63,7 @@
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex" i18n>Wealth Items</h4>
|
||||
<h4 class="align-items-center d-flex">Wealth Items</h4>
|
||||
<p class="m-0">
|
||||
Track all your treasuries, be it your luxury watch or rare
|
||||
trading cards.
|
||||
@ -76,7 +74,7 @@
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex" i18n>Emergency Fund</h4>
|
||||
<h4 class="align-items-center d-flex">Emergency Fund</h4>
|
||||
<p class="m-0">
|
||||
Define your emergency fund you are comfortable with for
|
||||
difficult times.
|
||||
@ -87,7 +85,7 @@
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex" i18n>Import and Export</h4>
|
||||
<h4 class="align-items-center d-flex">Import and Export</h4>
|
||||
<p class="m-0">Import and export your investment activities.</p>
|
||||
</div>
|
||||
</mat-card>
|
||||
@ -95,7 +93,7 @@
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 i18n>Multi-Accounts</h4>
|
||||
<h4>Multi-Accounts</h4>
|
||||
<p class="m-0">
|
||||
Keep an eye on all your accounts across multiple platforms
|
||||
(multi-banking).
|
||||
@ -107,7 +105,7 @@
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">
|
||||
<span i18n>Portfolio Calculations</span>
|
||||
<span>Portfolio Calculations</span>
|
||||
<gf-premium-indicator
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="ml-1"
|
||||
@ -125,7 +123,7 @@
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">
|
||||
<span i18n>Portfolio Allocations</span>
|
||||
<span>Portfolio Allocations</span>
|
||||
<gf-premium-indicator
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="ml-1"
|
||||
@ -141,7 +139,7 @@
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex" i18n>Dark Mode</h4>
|
||||
<h4 class="align-items-center d-flex">Dark Mode</h4>
|
||||
<p class="m-0">
|
||||
Ghostfolio automatically switches to a dark color theme based on
|
||||
your operating system's preferences.
|
||||
@ -152,7 +150,7 @@
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex" i18n>Zen Mode</h4>
|
||||
<h4 class="align-items-center d-flex">Zen Mode</h4>
|
||||
<p class="m-0">
|
||||
Keep calm and activate Zen Mode if the markets are going crazy.
|
||||
</p>
|
||||
@ -166,7 +164,7 @@
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">
|
||||
<span i18n>Market Mood</span>
|
||||
<span>Market Mood</span>
|
||||
<gf-premium-indicator class="ml-1"></gf-premium-indicator>
|
||||
</h4>
|
||||
<p class="m-0">
|
||||
@ -181,7 +179,7 @@
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">
|
||||
<span i18n>Static Analysis</span>
|
||||
<span>Static Analysis</span>
|
||||
<gf-premium-indicator
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="ml-1"
|
||||
@ -197,7 +195,7 @@
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 i18n>Community</h4>
|
||||
<h4>Community</h4>
|
||||
<p class="m-0">
|
||||
Join the Ghostfolio
|
||||
<a
|
||||
@ -214,7 +212,7 @@
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 i18n>Open Source Software</h4>
|
||||
<h4>Open Source Software</h4>
|
||||
<p class="m-0">
|
||||
The source code is fully available as
|
||||
<a
|
||||
@ -232,7 +230,7 @@
|
||||
</div>
|
||||
<div *ngIf="!user" class="row">
|
||||
<div class="col mt-3 text-center">
|
||||
<a color="primary" i18n mat-flat-button [routerLink]="['/register']">
|
||||
<a color="primary" mat-flat-button [routerLink]="['/register']">
|
||||
Get Started
|
||||
</a>
|
||||
</div>
|
||||
|
@ -20,7 +20,7 @@ const routes: Routes = [
|
||||
],
|
||||
component: HomePageComponent,
|
||||
path: '',
|
||||
title: 'Overview'
|
||||
title: $localize`Overview`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<h1 class="font-weight-bold intro my-5" i18n>
|
||||
<h1 class="font-weight-bold intro my-5">
|
||||
Manage your wealth like a boss
|
||||
</h1>
|
||||
<div>
|
||||
@ -29,19 +29,13 @@
|
||||
<a
|
||||
class="d-inline-block"
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[routerLink]="['/register']"
|
||||
>
|
||||
Get Started
|
||||
</a>
|
||||
<div class="d-inline-block mx-3 text-muted" i18n>or</div>
|
||||
<a
|
||||
class="d-inline-block"
|
||||
i18n
|
||||
mat-stroked-button
|
||||
[routerLink]="['/demo']"
|
||||
>
|
||||
<div class="d-inline-block mx-3 text-muted">or</div>
|
||||
<a class="d-inline-block" mat-stroked-button [routerLink]="['/demo']">
|
||||
Live Demo
|
||||
</a>
|
||||
</div>
|
||||
@ -107,7 +101,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mt-4 text-center">
|
||||
<a [routerLink]="['/about']" i18n mat-stroked-button
|
||||
<a [routerLink]="['/about']" mat-stroked-button
|
||||
>Learn more about Ghostfolio</a
|
||||
>
|
||||
</div>
|
||||
@ -162,16 +156,11 @@
|
||||
Join now or check out the example account
|
||||
</p>
|
||||
<div class="py-2 text-center">
|
||||
<a color="primary" i18n mat-flat-button [routerLink]="['/register']">
|
||||
<a color="primary" mat-flat-button [routerLink]="['/register']">
|
||||
Get Started
|
||||
</a>
|
||||
<div class="d-inline-block mx-3 text-muted" i18n>or</div>
|
||||
<a
|
||||
class="d-inline-block"
|
||||
i18n
|
||||
mat-stroked-button
|
||||
[routerLink]="['/demo']"
|
||||
>
|
||||
<div class="d-inline-block mx-3 text-muted">or</div>
|
||||
<a class="d-inline-block" mat-stroked-button [routerLink]="['/demo']">
|
||||
Live Demo
|
||||
</a>
|
||||
</div>
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: MarketsPageComponent,
|
||||
path: '',
|
||||
title: 'Markets'
|
||||
title: $localize`Markets`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: AllocationsPageComponent,
|
||||
path: '',
|
||||
title: 'Allocations'
|
||||
title: $localize`Allocations`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -54,8 +54,8 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
};
|
||||
public period = 'current';
|
||||
public periodOptions: ToggleOption[] = [
|
||||
{ label: 'Initial', value: 'original' },
|
||||
{ label: 'Current', value: 'current' }
|
||||
{ label: $localize`Initial`, value: 'original' },
|
||||
{ label: $localize`Current`, value: 'current' }
|
||||
];
|
||||
public placeholder = '';
|
||||
public portfolioDetails: PortfolioDetails;
|
||||
@ -85,7 +85,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
public user: User;
|
||||
|
||||
private readonly SEARCH_PLACEHOLDER = 'Filter by account or tag...';
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
@ -133,7 +132,9 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
this.isLoading = true;
|
||||
this.activeFilters = filters;
|
||||
this.placeholder =
|
||||
this.activeFilters.length <= 0 ? this.SEARCH_PLACEHOLDER : '';
|
||||
this.activeFilters.length <= 0
|
||||
? $localize`Filter by account or tag...`
|
||||
: '';
|
||||
|
||||
return this.dataService.fetchPortfolioDetails({
|
||||
filters: this.activeFilters
|
||||
|
@ -94,8 +94,8 @@
|
||||
<div class="col-md-12 allocations-by-symbol">
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-header class="overflow-hidden w-100">
|
||||
<mat-card-title class="align-items-center d-flex text-truncate" i18n>
|
||||
By Holding</mat-card-title
|
||||
<mat-card-title class="align-items-center d-flex text-truncate">
|
||||
<ng-container i18n>By Holding</ng-container></mat-card-title
|
||||
>
|
||||
<gf-toggle
|
||||
[defaultValue]="period"
|
||||
@ -233,27 +233,30 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
label="Developed Markets"
|
||||
i18n
|
||||
size="large"
|
||||
[isPercent]="true"
|
||||
[value]="markets?.developedMarkets?.value"
|
||||
></gf-value>
|
||||
>Developed Markets</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
label="Emerging Markets"
|
||||
i18n
|
||||
size="large"
|
||||
[isPercent]="true"
|
||||
[value]="markets?.emergingMarkets?.value"
|
||||
></gf-value>
|
||||
>Emerging Markets</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
label="Other Markets"
|
||||
i18n
|
||||
size="large"
|
||||
[isPercent]="true"
|
||||
[value]="markets?.otherMarkets?.value"
|
||||
></gf-value>
|
||||
>Other Markets</gf-value
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: AnalysisPageComponent,
|
||||
path: '',
|
||||
title: 'Analysis'
|
||||
title: $localize`Analysis`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -26,8 +26,8 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
public investmentsByMonth: InvestmentItem[];
|
||||
public mode: GroupBy;
|
||||
public modeOptions: ToggleOption[] = [
|
||||
{ label: 'Monthly', value: 'month' },
|
||||
{ label: 'Accumulating', value: undefined }
|
||||
{ label: $localize`Monthly`, value: 'month' },
|
||||
{ label: $localize`Accumulating`, value: undefined }
|
||||
];
|
||||
public top3: Position[];
|
||||
public user: User;
|
||||
|
@ -52,7 +52,7 @@
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="align-items-center d-flex" i18n
|
||||
>Top 3</mat-card-title
|
||||
>Top</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
@ -88,7 +88,7 @@
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="align-items-center d-flex" i18n
|
||||
>Bottom 3</mat-card-title
|
||||
>Bottom</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: FirePageComponent,
|
||||
path: '',
|
||||
title: 'FIRE'
|
||||
title: $localize`FIRE`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: HoldingsPageComponent,
|
||||
path: '',
|
||||
title: 'Holdings'
|
||||
title: $localize`Holdings`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -91,7 +91,7 @@
|
||||
<div class="col-xs-12 col-md-6 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<h4 class="align-items-center d-flex">
|
||||
<span i18n>X-ray</span>
|
||||
<span>X-ray</span>
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
|
@ -1,10 +1,10 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="align-items-center d-flex justify-content-center mb-3" i18n>
|
||||
<h3 class="align-items-center d-flex justify-content-center mb-3">
|
||||
X-ray
|
||||
</h3>
|
||||
<p class="mb-4" i18n>
|
||||
<p class="mb-4">
|
||||
Ghostfolio X-ray uses static analysis to identify potential issues and
|
||||
risks in your portfolio.
|
||||
<span class="d-none"
|
||||
@ -14,21 +14,21 @@
|
||||
>
|
||||
</p>
|
||||
<div class="mb-4">
|
||||
<h4 class="m-0" i18n>Currency Cluster Risks</h4>
|
||||
<h4 class="m-0">Currency Cluster Risks</h4>
|
||||
<gf-rules
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[rules]="currencyClusterRiskRules"
|
||||
></gf-rules>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<h4 class="m-0" i18n>Account Cluster Risks</h4>
|
||||
<h4 class="m-0">Account Cluster Risks</h4>
|
||||
<gf-rules
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[rules]="accountClusterRiskRules"
|
||||
></gf-rules>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="m-0" i18n>Fees</h4>
|
||||
<h4 class="m-0">Fees</h4>
|
||||
<gf-rules
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[rules]="feeRules"
|
||||
|
@ -166,7 +166,7 @@
|
||||
[ngClass]="{ 'd-none': activityForm.controls['type']?.value !== 'ITEM' }"
|
||||
>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Asset Sub-Class</mat-label>
|
||||
<mat-label i18n>Asset Sub Class</mat-label>
|
||||
<mat-select formControlName="assetSubClass">
|
||||
<mat-option [value]="null"></mat-option>
|
||||
<mat-option
|
||||
@ -201,12 +201,11 @@
|
||||
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
|
||||
<button
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
type="submit"
|
||||
[disabled]="!activityForm.valid"
|
||||
>
|
||||
Save
|
||||
<ng-container i18n>Save</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: TransactionsPageComponent,
|
||||
path: '',
|
||||
title: 'Activities'
|
||||
title: $localize`Activities`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: PricingPageComponent,
|
||||
path: '',
|
||||
title: 'Pricing'
|
||||
title: $localize`Pricing`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="d-flex justify-content-center mb-3 text-center" i18n>
|
||||
<h3 class="d-flex justify-content-center mb-3 text-center">
|
||||
Pricing Plans
|
||||
</h3>
|
||||
<div class="mb-4">
|
||||
@ -20,7 +20,7 @@
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4 i18n>Open Source</h4>
|
||||
<h4>Open Source</h4>
|
||||
<p>
|
||||
For tech-savvy investors who prefer to run
|
||||
<strong>Ghostfolio</strong> on their own infrastructure.
|
||||
@ -73,7 +73,7 @@
|
||||
[ngClass]="{ 'active': user?.subscription?.type === 'Basic' }"
|
||||
>
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex" i18n>Basic</h4>
|
||||
<h4 class="align-items-center d-flex">Basic</h4>
|
||||
<p>
|
||||
For new investors who are just getting started with trading.
|
||||
</p>
|
||||
@ -124,7 +124,7 @@
|
||||
>
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">
|
||||
<span i18n>Premium</span>
|
||||
<span>Premium</span>
|
||||
<gf-premium-indicator
|
||||
class="ml-1"
|
||||
[enableLink]="false"
|
||||
@ -186,7 +186,7 @@
|
||||
>{{ baseCurrency }} <strong
|
||||
>{{ price }}</strong
|
||||
></ng-container
|
||||
> <span i18n>per year</span></span
|
||||
> <span>per year</span></span
|
||||
>
|
||||
</p>
|
||||
</mat-card>
|
||||
@ -196,14 +196,14 @@
|
||||
</div>
|
||||
<div *ngIf="user?.subscription?.type === 'Basic'" class="row">
|
||||
<div class="col mt-3 text-center">
|
||||
<a color="primary" i18n mat-flat-button [routerLink]="['/account']">
|
||||
<a color="primary" mat-flat-button [routerLink]="['/account']">
|
||||
Upgrade Plan
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!user" class="row">
|
||||
<div class="col mt-3 text-center">
|
||||
<a color="primary" i18n mat-flat-button [routerLink]="['/register']">
|
||||
<a color="primary" mat-flat-button [routerLink]="['/register']">
|
||||
Get Started
|
||||
</a>
|
||||
<p class="text-muted"><small>It's free</small></p>
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: PublicPageComponent,
|
||||
path: ':id',
|
||||
title: 'Portfolio'
|
||||
title: $localize`Portfolio`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -82,27 +82,30 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
label="Developed Markets"
|
||||
i18n
|
||||
size="large"
|
||||
[isPercent]="true"
|
||||
[value]="markets?.developedMarkets?.value"
|
||||
></gf-value>
|
||||
>Developed Markets</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
label="Emerging Markets"
|
||||
i18n
|
||||
size="large"
|
||||
[isPercent]="true"
|
||||
[value]="markets?.emergingMarkets?.value"
|
||||
></gf-value>
|
||||
>Emerging Markets</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
label="Other Markets"
|
||||
i18n
|
||||
size="large"
|
||||
[isPercent]="true"
|
||||
[value]="markets?.otherMarkets?.value"
|
||||
></gf-value>
|
||||
>Other Markets</gf-value
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
@ -129,8 +132,8 @@
|
||||
Ghostfolio empowers you to keep track of your wealth.
|
||||
</p>
|
||||
<div class="py-2 text-center">
|
||||
<a color="primary" href="https://ghostfol.io" i18n mat-flat-button>
|
||||
Get Started
|
||||
<a color="primary" href="https://ghostfol.io" mat-flat-button>
|
||||
<ng-container i18n>Get Started</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: RegisterPageComponent,
|
||||
path: '',
|
||||
title: 'Registration'
|
||||
title: $localize`Registration`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -20,12 +20,11 @@
|
||||
<button
|
||||
class="d-inline-block"
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[disabled]="!demoAuthToken || info?.isReadOnlyMode"
|
||||
(click)="createAccount()"
|
||||
>
|
||||
Create Account
|
||||
<ng-container i18n>Create Account</ng-container>
|
||||
</button>
|
||||
<ng-container *ngIf="hasPermissionForSocialLogin">
|
||||
<div class="my-3 text-muted" i18n>or</div>
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: ResourcesPageComponent,
|
||||
path: '',
|
||||
title: 'Resources'
|
||||
title: $localize`Resources`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -29,8 +29,7 @@
|
||||
easier and faster in this guide.
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
[routerLink]="['/en', 'blog', '2022', '07', 'how-do-i-get-my-finances-in-order']"
|
||||
<a href="../en/blog/2022/07/how-do-i-get-my-finances-in-order"
|
||||
>How do I get my finances in order? →</a
|
||||
>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@ import { RouterModule, Routes } from '@angular/router';
|
||||
import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ component: WebauthnPageComponent, path: '', title: 'Login' }
|
||||
{ component: WebauthnPageComponent, path: '', title: $localize`Sign in` }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -14,21 +14,20 @@
|
||||
*ngIf="hasError"
|
||||
class="align-items-center col d-flex flex-column justify-content-center"
|
||||
>
|
||||
<h1 class="d-flex h5 justify-content-center mb-0 text-center" i18n>
|
||||
Oops, authentication has failed.
|
||||
<h1 class="d-flex h5 justify-content-center mb-0 text-center">
|
||||
<ng-container i18n>Oops, authentication has failed.</ng-container>
|
||||
</h1>
|
||||
<button
|
||||
class="mb-3 mt-4"
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
(click)="signIn()"
|
||||
>
|
||||
Try again
|
||||
<ng-container i18n>Try again</ng-container>
|
||||
</button>
|
||||
<div class="text-muted" i18n>or</div>
|
||||
<button class="mt-1" i18n mat-flat-button (click)="deregisterDevice()">
|
||||
Go back to Home Page
|
||||
<div class="text-muted"><ng-container i18n>or</ng-container></div>
|
||||
<button class="mt-1" mat-flat-button (click)="deregisterDevice()">
|
||||
<ng-container i18n>Go back to Home Page</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,7 +16,7 @@ const routes: Routes = [
|
||||
],
|
||||
component: ZenPageComponent,
|
||||
path: '',
|
||||
title: 'Overview'
|
||||
title: $localize`Overview`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -133,8 +133,32 @@ export class DataService {
|
||||
return this.http.get<AdminData>('/api/v1/admin');
|
||||
}
|
||||
|
||||
public fetchAdminMarketData() {
|
||||
return this.http.get<AdminMarketData>('/api/v1/admin/market-data');
|
||||
public fetchAdminMarketData({ filters }: { filters?: Filter[] }) {
|
||||
let params = new HttpParams();
|
||||
|
||||
if (filters?.length > 0) {
|
||||
const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy(
|
||||
filters,
|
||||
(filter) => {
|
||||
return filter.type;
|
||||
}
|
||||
);
|
||||
|
||||
if (filtersByAssetSubClass) {
|
||||
params = params.append(
|
||||
'assetSubClasses',
|
||||
filtersByAssetSubClass
|
||||
.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
.join(',')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.http.get<AdminMarketData>('/api/v1/admin/market-data', {
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
public deleteAccess(aId: string) {
|
||||
|
@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Ghostfol.io</title>
|
||||
<link rel="canonical" href="https://ghostfol.io/en/" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="refresh" content="0; url=https://ghostfol.io/en/" />
|
||||
</head>
|
||||
</html>
|
@ -6,70 +6,70 @@
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<url>
|
||||
<loc>https://ghostfol.io</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/about</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/about/changelog</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/08/500-stars-on-github</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/demo</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/faq</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/features</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/markets</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/pricing</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/register</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources</loc>
|
||||
<lastmod>2022-08-13T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
</urlset>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="h-100 position-relative" lang="en">
|
||||
<html class="h-100 position-relative" lang="${languageCode}">
|
||||
<head>
|
||||
<title>Ghostfolio – Open Source Wealth Management Software</title>
|
||||
<base href="/" />
|
||||
@ -19,10 +19,7 @@
|
||||
name="twitter:description"
|
||||
content="Ghostfolio is a lightweight wealth management application for individuals to keep track of stocks, ETFs or cryptocurrencies"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://ghostfol.io/en/assets/cover.png"
|
||||
/>
|
||||
<meta name="twitter:image" content="${rootUrl}/${featureGraphicPath}" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="Ghostfolio – Open Source Wealth Management Software"
|
||||
@ -37,12 +34,9 @@
|
||||
content="Ghostfolio – Open Source Wealth Management Software"
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://ghostfol.io" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://ghostfol.io/en/assets/cover.png"
|
||||
/>
|
||||
<meta property="og:updated_time" content="2022-05-28T00:00:00+00:00" />
|
||||
<meta property="og:url" content="${rootUrl}${path}" />
|
||||
<meta property="og:image" content="${rootUrl}/${featureGraphicPath}" />
|
||||
<meta property="og:updated_time" content="2022-08-18T00:00:00+00:00" />
|
||||
<meta
|
||||
property="og:site_name"
|
||||
content="Ghostfolio – Open Source Wealth Management Software"
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -2,16 +2,6 @@ import { DataSource } from '@prisma/client';
|
||||
import { JobOptions, JobStatus } from 'bull';
|
||||
import ms from 'ms';
|
||||
|
||||
import { ToggleOption } from './types';
|
||||
|
||||
export const defaultDateRangeOptions: ToggleOption[] = [
|
||||
{ label: 'Today', value: '1d' },
|
||||
{ label: 'YTD', value: 'ytd' },
|
||||
{ label: '1Y', value: '1y' },
|
||||
{ label: '5Y', value: '5y' },
|
||||
{ label: 'Max', value: 'max' }
|
||||
];
|
||||
|
||||
export const DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f';
|
||||
|
||||
export const ghostfolioScraperApiSymbolPrefix = '_GF_';
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user