Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
55b03733f4 | |||
0000317041 | |||
e5f2a3865d | |||
c61561664f | |||
a7d8a63ab8 | |||
5c51c1e825 | |||
3a67bf9bb4 | |||
f7597c213d | |||
2e7f46ad78 | |||
cfffb99f52 | |||
69ac3408f1 | |||
e1806b4bd8 | |||
6aae0cc1e4 | |||
5d8a50a80d | |||
662231e830 | |||
4d84459b5b | |||
efba7429c1 | |||
9cae5a3e79 | |||
c2ed0a436f | |||
8486c02575 | |||
5122ef3456 | |||
579b86665e | |||
52b3ad6dc3 | |||
bf9b60aa74 | |||
6cd51fb044 | |||
271001f523 | |||
a7e513a6d1 | |||
b5f256be95 | |||
a834ef6b4c |
2
.github/workflows/build-code.yml
vendored
2
.github/workflows/build-code.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
node_version:
|
||||
- 16
|
||||
- 18
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
54
CHANGELOG.md
54
CHANGELOG.md
@ -5,7 +5,59 @@ 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.228.0 - 2023-01-18
|
||||
## 1.231.0 - 2023-02-04
|
||||
|
||||
### Added
|
||||
|
||||
- Added the dividend and fees to the position detail dialog
|
||||
- Added support to link a (wealth) item to an account
|
||||
|
||||
### Changed
|
||||
|
||||
- Relaxed the validation rule of the _Redis_ host environment variable (`REDIS_HOST`)
|
||||
- Improved the language localization for German (`de`)
|
||||
- Eliminated `angular-material-css-vars`
|
||||
- Upgraded `angular` from version `14.2.0` to `15.1.2`
|
||||
- Upgraded `Nx` from version `15.0.13` to `15.6.3`
|
||||
|
||||
## 1.230.0 - 2023-01-29
|
||||
|
||||
### Added
|
||||
|
||||
- Added an interstitial for the subscription
|
||||
- Added _SourceForge_ to the _As seen in_ section on the landing page
|
||||
- Added a quote to the blog post _Ghostfolio auf Sackgeld.com vorgestellt_
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the unit format (`%`) in the global heat map component of the public page
|
||||
- Improved the pricing page
|
||||
- Upgraded `Node.js` from version `16` to `18` (`Dockerfile`)
|
||||
- Upgraded `prisma` from version `4.8.0` to `4.9.0`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the click of unknown accounts in the portfolio proportion chart component
|
||||
- Fixed an issue with `value` in the value redaction interceptor for the impersonation mode
|
||||
|
||||
## 1.229.0 - 2023-01-21
|
||||
|
||||
### Added
|
||||
|
||||
- Added a blog post: _Ghostfolio auf Sackgeld.com vorgestellt_
|
||||
- Added _Sackgeld.com_ to the _As seen in_ section on the landing page
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed the toggle _Original Shares_ vs. _Current Shares_ on the allocations page
|
||||
- Hid error messages related to no current investment in the client
|
||||
- Refactored the value redaction interceptor for the impersonation mode
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the value of the active (emergency fund) filter in percentage on the allocations page
|
||||
|
||||
## 1.228.1 - 2023-01-18
|
||||
|
||||
### Added
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM node:16-slim as builder
|
||||
FROM --platform=$BUILDPLATFORM node:18-slim as builder
|
||||
|
||||
# Build application and add additional files
|
||||
WORKDIR /ghostfolio
|
||||
@ -50,7 +50,7 @@ COPY package.json /ghostfolio/dist/apps/api
|
||||
RUN yarn database:generate-typings
|
||||
|
||||
# Image to run, copy everything needed from builder
|
||||
FROM node:16-slim
|
||||
FROM node:18-slim
|
||||
RUN apt update && apt install -y \
|
||||
openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
@ -40,7 +40,7 @@ Ghostfolio is for you if you are...
|
||||
- 🧘 into minimalism
|
||||
- 🧺 caring about diversifying your financial resources
|
||||
- 🆓 interested in financial independence
|
||||
- 🙅 saying no to spreadsheets in 2023
|
||||
- 🙅 saying no to spreadsheets
|
||||
- 😎 still reading this list
|
||||
|
||||
## Features
|
||||
@ -148,7 +148,7 @@ Please follow the instructions of the Ghostfolio [Unraid Community App](https://
|
||||
### Prerequisites
|
||||
|
||||
- [Docker](https://www.docker.com/products/docker-desktop)
|
||||
- [Node.js](https://nodejs.org/en/download) (version 16+)
|
||||
- [Node.js](https://nodejs.org/en/download) (version 16)
|
||||
- [Yarn](https://yarnpkg.com/en/docs/install)
|
||||
- A local copy of this Git repository (clone)
|
||||
|
||||
@ -269,7 +269,7 @@ You can get the _Bearer Token_ via `GET http://localhost:3333/api/v1/auth/anonym
|
||||
|
||||
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
|
||||
|
||||
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg), tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you.
|
||||
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) or tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_). We would love to hear from you.
|
||||
|
||||
If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio).
|
||||
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||
import {
|
||||
nullifyValuesInObject,
|
||||
nullifyValuesInObjects
|
||||
} from '@ghostfolio/api/helper/object.helper';
|
||||
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { Accounts } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
@ -22,7 +19,8 @@ import {
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
UseGuards
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
@ -39,8 +37,7 @@ export class AccountController {
|
||||
private readonly accountService: AccountService,
|
||||
private readonly impersonationService: ImpersonationService,
|
||||
private readonly portfolioService: PortfolioService,
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||
private readonly userService: UserService
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||
) {}
|
||||
|
||||
@Delete(':id')
|
||||
@ -85,6 +82,7 @@ export class AccountController {
|
||||
|
||||
@Get()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(RedactValuesInResponseInterceptor)
|
||||
public async getAllAccounts(
|
||||
@Headers('impersonation-id') impersonationId
|
||||
): Promise<Accounts> {
|
||||
@ -94,39 +92,15 @@ export class AccountController {
|
||||
this.request.user.id
|
||||
);
|
||||
|
||||
let accountsWithAggregations =
|
||||
await this.portfolioService.getAccountsWithAggregations({
|
||||
userId: impersonationUserId || this.request.user.id,
|
||||
withExcludedAccounts: true
|
||||
});
|
||||
|
||||
if (
|
||||
impersonationUserId ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
accountsWithAggregations = {
|
||||
...nullifyValuesInObject(accountsWithAggregations, [
|
||||
'totalBalanceInBaseCurrency',
|
||||
'totalValueInBaseCurrency'
|
||||
]),
|
||||
accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [
|
||||
'balance',
|
||||
'balanceInBaseCurrency',
|
||||
'convertedBalance',
|
||||
'fee',
|
||||
'quantity',
|
||||
'unitPrice',
|
||||
'value',
|
||||
'valueInBaseCurrency'
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
return accountsWithAggregations;
|
||||
return this.portfolioService.getAccountsWithAggregations({
|
||||
userId: impersonationUserId || this.request.user.id,
|
||||
withExcludedAccounts: true
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(RedactValuesInResponseInterceptor)
|
||||
public async getAccountById(
|
||||
@Headers('impersonation-id') impersonationId,
|
||||
@Param('id') id: string
|
||||
@ -137,35 +111,13 @@ export class AccountController {
|
||||
this.request.user.id
|
||||
);
|
||||
|
||||
let accountsWithAggregations =
|
||||
const accountsWithAggregations =
|
||||
await this.portfolioService.getAccountsWithAggregations({
|
||||
filters: [{ id, type: 'ACCOUNT' }],
|
||||
userId: impersonationUserId || this.request.user.id,
|
||||
withExcludedAccounts: true
|
||||
});
|
||||
|
||||
if (
|
||||
impersonationUserId ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
accountsWithAggregations = {
|
||||
...nullifyValuesInObject(accountsWithAggregations, [
|
||||
'totalBalanceInBaseCurrency',
|
||||
'totalValueInBaseCurrency'
|
||||
]),
|
||||
accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [
|
||||
'balance',
|
||||
'balanceInBaseCurrency',
|
||||
'convertedBalance',
|
||||
'fee',
|
||||
'quantity',
|
||||
'unitPrice',
|
||||
'value',
|
||||
'valueInBaseCurrency'
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
return accountsWithAggregations.accounts[0];
|
||||
}
|
||||
|
||||
|
@ -1,24 +1,23 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { AuthDeviceModule } from '@ghostfolio/api/app/auth-device/auth-device.module';
|
||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { CronService } from '@ghostfolio/api/services/cron.service';
|
||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||
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 { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
|
||||
import { ConfigurationModule } from '../services/configuration.module';
|
||||
import { CronService } from '../services/cron.service';
|
||||
import { DataGatheringModule } from '../services/data-gathering.module';
|
||||
import { DataProviderModule } from '../services/data-provider/data-provider.module';
|
||||
import { ExchangeRateDataModule } from '../services/exchange-rate-data.module';
|
||||
import { PrismaModule } from '../services/prisma.module';
|
||||
import { TwitterBotModule } from '../services/twitter-bot/twitter-bot.module';
|
||||
import { AccessModule } from './access/access.module';
|
||||
import { AccountModule } from './account/account.module';
|
||||
import { AdminModule } from './admin/admin.module';
|
||||
import { AppController } from './app.controller';
|
||||
import { AuthDeviceModule } from './auth-device/auth-device.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { BenchmarkModule } from './benchmark/benchmark.module';
|
||||
import { CacheModule } from './cache/cache.module';
|
||||
@ -30,6 +29,7 @@ import { InfoModule } from './info/info.module';
|
||||
import { LogoModule } from './logo/logo.module';
|
||||
import { OrderModule } from './order/order.module';
|
||||
import { PortfolioModule } from './portfolio/portfolio.module';
|
||||
import { RedisCacheModule } from './redis-cache/redis-cache.module';
|
||||
import { SubscriptionModule } from './subscription/subscription.module';
|
||||
import { SymbolModule } from './symbol/symbol.module';
|
||||
import { UserModule } from './user/user.module';
|
||||
@ -45,7 +45,7 @@ import { UserModule } from './user/user.module';
|
||||
BullModule.forRoot({
|
||||
redis: {
|
||||
host: process.env.REDIS_HOST,
|
||||
port: parseInt(process.env.REDIS_PORT, 10),
|
||||
port: parseInt(process.env.REDIS_PORT ?? '6379', 10),
|
||||
password: process.env.REDIS_PASSWORD
|
||||
}
|
||||
}),
|
||||
|
@ -83,6 +83,13 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
) {
|
||||
featureGraphicPath = 'assets/images/blog/20221226.jpg';
|
||||
title = `The importance of tracking your personal finances - ${title}`;
|
||||
} else if (
|
||||
request.path.startsWith(
|
||||
'/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt'
|
||||
)
|
||||
) {
|
||||
featureGraphicPath = 'assets/images/blog/ghostfolio-x-sackgeld.png';
|
||||
title = `Ghostfolio auf Sackgeld.com vorgestellt - ${title}`;
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -20,7 +20,6 @@ import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { ImportDataDto } from './import-data.dto';
|
||||
import { ImportService } from './import.service';
|
||||
|
@ -76,23 +76,21 @@ export class OrderService {
|
||||
userId: string;
|
||||
}
|
||||
): Promise<Order> {
|
||||
const defaultAccount = (
|
||||
await this.accountService.getAccounts(data.userId)
|
||||
).find((account) => {
|
||||
return account.isDefault === true;
|
||||
});
|
||||
let Account;
|
||||
|
||||
if (data.accountId) {
|
||||
Account = {
|
||||
connect: {
|
||||
id_userId: {
|
||||
userId: data.userId,
|
||||
id: data.accountId
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const tags = data.tags ?? [];
|
||||
|
||||
let Account = {
|
||||
connect: {
|
||||
id_userId: {
|
||||
userId: data.userId,
|
||||
id: data.accountId ?? defaultAccount?.id
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (data.type === 'ITEM') {
|
||||
const assetClass = data.assetClass;
|
||||
const assetSubClass = data.assetSubClass;
|
||||
@ -101,7 +99,6 @@ export class OrderService {
|
||||
const id = uuidv4();
|
||||
const name = data.SymbolProfile.connectOrCreate.create.symbol;
|
||||
|
||||
Account = undefined;
|
||||
data.id = id;
|
||||
data.SymbolProfile.connectOrCreate.create.assetClass = assetClass;
|
||||
data.SymbolProfile.connectOrCreate.create.assetSubClass = assetSubClass;
|
||||
|
@ -7,6 +7,8 @@ import { Tag } from '@prisma/client';
|
||||
|
||||
export interface PortfolioPositionDetail {
|
||||
averagePrice: number;
|
||||
dividendInBaseCurrency: number;
|
||||
feeInBaseCurrency: number;
|
||||
firstBuyDate: string;
|
||||
grossPerformance: number;
|
||||
grossPerformancePercent: number;
|
||||
|
@ -82,6 +82,7 @@ describe('PortfolioCalculator', () => {
|
||||
averagePrice: new Big('0'),
|
||||
currency: 'CHF',
|
||||
dataSource: 'YAHOO',
|
||||
fee: new Big('3.2'),
|
||||
firstBuyDate: '2021-11-22',
|
||||
grossPerformance: new Big('-12.6'),
|
||||
grossPerformancePercentage: new Big('-0.0440867739678096571'),
|
||||
|
@ -71,6 +71,7 @@ describe('PortfolioCalculator', () => {
|
||||
averagePrice: new Big('136.6'),
|
||||
currency: 'CHF',
|
||||
dataSource: 'YAHOO',
|
||||
fee: new Big('1.55'),
|
||||
firstBuyDate: '2021-11-30',
|
||||
grossPerformance: new Big('24.6'),
|
||||
grossPerformancePercentage: new Big('0.09004392386530014641'),
|
||||
|
@ -82,6 +82,7 @@ describe('PortfolioCalculator', () => {
|
||||
averagePrice: new Big('320.43'),
|
||||
currency: 'CHF',
|
||||
dataSource: 'YAHOO',
|
||||
fee: new Big('0'),
|
||||
firstBuyDate: '2015-01-01',
|
||||
grossPerformance: new Big('27172.74'),
|
||||
grossPerformancePercentage: new Big('42.40043067128546016291'),
|
||||
|
@ -82,6 +82,7 @@ describe('PortfolioCalculator', () => {
|
||||
averagePrice: new Big('75.80'),
|
||||
currency: 'CHF',
|
||||
dataSource: 'YAHOO',
|
||||
fee: new Big('4.25'),
|
||||
firstBuyDate: '2022-03-07',
|
||||
grossPerformance: new Big('21.93'),
|
||||
grossPerformancePercentage: new Big('0.14465699208443271768'),
|
||||
|
@ -102,6 +102,7 @@ describe('PortfolioCalculator', () => {
|
||||
averagePrice: new Big('0'),
|
||||
currency: 'CHF',
|
||||
dataSource: 'YAHOO',
|
||||
fee: new Big('0'),
|
||||
firstBuyDate: '2022-03-07',
|
||||
grossPerformance: new Big('19.86'),
|
||||
grossPerformancePercentage: new Big('0.13100263852242744063'),
|
||||
|
@ -431,6 +431,7 @@ export class PortfolioCalculator {
|
||||
: item.investment.div(item.quantity),
|
||||
currency: item.currency,
|
||||
dataSource: item.dataSource,
|
||||
fee: item.fee,
|
||||
firstBuyDate: item.firstBuyDate,
|
||||
grossPerformance: !hasErrors ? grossPerformance ?? null : null,
|
||||
grossPerformancePercentage: !hasErrors
|
||||
@ -447,7 +448,7 @@ export class PortfolioCalculator {
|
||||
transactionCount: item.transactionCount
|
||||
});
|
||||
|
||||
if (hasErrors) {
|
||||
if (hasErrors && item.investment.gt(0)) {
|
||||
errors.push({ dataSource: item.dataSource, symbol: item.symbol });
|
||||
}
|
||||
}
|
||||
|
@ -131,7 +131,8 @@ export class PortfolioController {
|
||||
portfolioPosition.investment / totalInvestment;
|
||||
portfolioPosition.netPerformance = null;
|
||||
portfolioPosition.quantity = null;
|
||||
portfolioPosition.value = portfolioPosition.value / totalValue;
|
||||
portfolioPosition.valueInPercentage =
|
||||
portfolioPosition.value / totalValue;
|
||||
}
|
||||
|
||||
for (const [name, { current, original }] of Object.entries(accounts)) {
|
||||
@ -322,7 +323,7 @@ export class PortfolioController {
|
||||
totalInvestment: new Big(totalInvestment)
|
||||
.div(performanceInformation.performance.totalInvestment)
|
||||
.toNumber(),
|
||||
value: new Big(value)
|
||||
valueInPercentage: new Big(value)
|
||||
.div(performanceInformation.performance.currentValue)
|
||||
.toNumber()
|
||||
};
|
||||
@ -356,6 +357,7 @@ export class PortfolioController {
|
||||
|
||||
@Get('positions')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(RedactValuesInResponseInterceptor)
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getPositions(
|
||||
@Headers('impersonation-id') impersonationId: string,
|
||||
@ -370,27 +372,11 @@ export class PortfolioController {
|
||||
filterByTags
|
||||
});
|
||||
|
||||
const result = await this.portfolioService.getPositions({
|
||||
return this.portfolioService.getPositions({
|
||||
dateRange,
|
||||
filters,
|
||||
impersonationId
|
||||
});
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
result.positions = result.positions.map((position) => {
|
||||
return nullifyValuesInObject(position, [
|
||||
'grossPerformance',
|
||||
'investment',
|
||||
'netPerformance',
|
||||
'quantity'
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get('public/:accessId')
|
||||
@ -441,7 +427,7 @@ export class PortfolioController {
|
||||
|
||||
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
||||
portfolioPublicDetails.holdings[symbol] = {
|
||||
allocationCurrent: portfolioPosition.value / totalValue,
|
||||
allocationInPercentage: portfolioPosition.value / totalValue,
|
||||
countries: hasDetails ? portfolioPosition.countries : [],
|
||||
currency: hasDetails ? portfolioPosition.currency : undefined,
|
||||
dataSource: portfolioPosition.dataSource,
|
||||
@ -452,7 +438,7 @@ export class PortfolioController {
|
||||
sectors: hasDetails ? portfolioPosition.sectors : [],
|
||||
symbol: portfolioPosition.symbol,
|
||||
url: portfolioPosition.url,
|
||||
value: portfolioPosition.value / totalValue
|
||||
valueInPercentage: portfolioPosition.value / totalValue
|
||||
};
|
||||
}
|
||||
|
||||
@ -460,6 +446,7 @@ export class PortfolioController {
|
||||
}
|
||||
|
||||
@Get('position/:dataSource/:symbol')
|
||||
@UseInterceptors(RedactValuesInResponseInterceptor)
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@ -468,27 +455,13 @@ export class PortfolioController {
|
||||
@Param('dataSource') dataSource,
|
||||
@Param('symbol') symbol
|
||||
): Promise<PortfolioPositionDetail> {
|
||||
let position = await this.portfolioService.getPosition(
|
||||
const position = await this.portfolioService.getPosition(
|
||||
dataSource,
|
||||
impersonationId,
|
||||
symbol
|
||||
);
|
||||
|
||||
if (position) {
|
||||
if (
|
||||
impersonationId ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
position = nullifyValuesInObject(position, [
|
||||
'grossPerformance',
|
||||
'investment',
|
||||
'netPerformance',
|
||||
'orders',
|
||||
'quantity',
|
||||
'value'
|
||||
]);
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ import {
|
||||
MAX_CHART_ITEMS,
|
||||
UNKNOWN_KEY
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||
import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Accounts,
|
||||
EnhancedSymbolProfile,
|
||||
@ -537,12 +537,9 @@ export class PortfolioService {
|
||||
|
||||
holdings[item.symbol] = {
|
||||
markets,
|
||||
allocationCurrent: filteredValueInBaseCurrency.eq(0)
|
||||
allocationInPercentage: filteredValueInBaseCurrency.eq(0)
|
||||
? 0
|
||||
: value.div(filteredValueInBaseCurrency).toNumber(),
|
||||
allocationInvestment: item.investment
|
||||
.div(totalInvestmentInBaseCurrency)
|
||||
.toNumber(),
|
||||
assetClass: symbolProfile.assetClass,
|
||||
assetSubClass: symbolProfile.assetSubClass,
|
||||
countries: symbolProfile.countries,
|
||||
@ -576,7 +573,6 @@ export class PortfolioService {
|
||||
const cashPositions = await this.getCashPositions({
|
||||
cashDetails,
|
||||
userCurrency,
|
||||
investment: totalInvestmentInBaseCurrency,
|
||||
value: filteredValueInBaseCurrency
|
||||
});
|
||||
|
||||
@ -596,13 +592,12 @@ export class PortfolioService {
|
||||
|
||||
if (
|
||||
filters?.length === 1 &&
|
||||
filters[0].id === 'EMERGENCY_FUND_TAG_ID' &&
|
||||
filters[0].id === EMERGENCY_FUND_TAG_ID &&
|
||||
filters[0].type === 'TAG'
|
||||
) {
|
||||
const cashPositions = await this.getCashPositions({
|
||||
cashDetails,
|
||||
userCurrency,
|
||||
investment: totalInvestmentInBaseCurrency,
|
||||
value: filteredValueInBaseCurrency
|
||||
});
|
||||
|
||||
@ -614,6 +609,8 @@ export class PortfolioService {
|
||||
)
|
||||
.toNumber();
|
||||
|
||||
filteredValueInBaseCurrency = emergencyFund;
|
||||
|
||||
accounts[UNKNOWN_KEY] = {
|
||||
balance: 0,
|
||||
currency: userCurrency,
|
||||
@ -681,6 +678,8 @@ export class PortfolioService {
|
||||
return {
|
||||
tags,
|
||||
averagePrice: undefined,
|
||||
dividendInBaseCurrency: undefined,
|
||||
feeInBaseCurrency: undefined,
|
||||
firstBuyDate: undefined,
|
||||
grossPerformance: undefined,
|
||||
grossPerformancePercent: undefined,
|
||||
@ -747,12 +746,23 @@ export class PortfolioService {
|
||||
averagePrice,
|
||||
currency,
|
||||
dataSource,
|
||||
fee,
|
||||
firstBuyDate,
|
||||
marketPrice,
|
||||
quantity,
|
||||
transactionCount
|
||||
} = position;
|
||||
|
||||
const dividendInBaseCurrency = getSum(
|
||||
orders
|
||||
.filter(({ type }) => {
|
||||
return type === 'DIVIDEND';
|
||||
})
|
||||
.map(({ valueInBaseCurrency }) => {
|
||||
return new Big(valueInBaseCurrency);
|
||||
})
|
||||
);
|
||||
|
||||
// Convert investment, gross and net performance to currency of user
|
||||
const investment = this.exchangeRateDataService.toCurrency(
|
||||
position.investment?.toNumber(),
|
||||
@ -786,8 +796,8 @@ export class PortfolioService {
|
||||
historicalDataArray.push({
|
||||
averagePrice: orders[0].unitPrice,
|
||||
date: firstBuyDate,
|
||||
quantity: orders[0].quantity,
|
||||
value: orders[0].unitPrice
|
||||
marketPrice: orders[0].unitPrice,
|
||||
quantity: orders[0].quantity
|
||||
});
|
||||
}
|
||||
|
||||
@ -816,9 +826,9 @@ export class PortfolioService {
|
||||
|
||||
historicalDataArray.push({
|
||||
date,
|
||||
marketPrice,
|
||||
averagePrice: currentAveragePrice,
|
||||
quantity: currentQuantity,
|
||||
value: marketPrice
|
||||
quantity: currentQuantity
|
||||
});
|
||||
|
||||
maxPrice = Math.max(marketPrice ?? 0, maxPrice);
|
||||
@ -839,6 +849,12 @@ export class PortfolioService {
|
||||
tags,
|
||||
transactionCount,
|
||||
averagePrice: averagePrice.toNumber(),
|
||||
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
|
||||
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||
fee.toNumber(),
|
||||
SymbolProfile.currency,
|
||||
userCurrency
|
||||
),
|
||||
grossPerformancePercent:
|
||||
position.grossPerformancePercentage?.toNumber(),
|
||||
historicalData: historicalDataArray,
|
||||
@ -895,6 +911,8 @@ export class PortfolioService {
|
||||
SymbolProfile,
|
||||
tags,
|
||||
averagePrice: 0,
|
||||
dividendInBaseCurrency: 0,
|
||||
feeInBaseCurrency: 0,
|
||||
firstBuyDate: undefined,
|
||||
grossPerformance: undefined,
|
||||
grossPerformancePercent: undefined,
|
||||
@ -1040,29 +1058,21 @@ export class PortfolioService {
|
||||
|
||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
||||
const startDate = this.getStartDate(dateRange, portfolioStart);
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
startDate
|
||||
);
|
||||
const {
|
||||
currentValue,
|
||||
errors,
|
||||
grossPerformance,
|
||||
grossPerformancePercentage,
|
||||
hasErrors,
|
||||
netPerformance,
|
||||
netPerformancePercentage,
|
||||
totalInvestment
|
||||
} = await portfolioCalculator.getCurrentPositions(startDate);
|
||||
|
||||
const hasErrors = currentPositions.hasErrors;
|
||||
const currentValue = currentPositions.currentValue.toNumber();
|
||||
const currentGrossPerformance = currentPositions.grossPerformance;
|
||||
const currentGrossPerformancePercent =
|
||||
currentPositions.grossPerformancePercentage;
|
||||
let currentNetPerformance = currentPositions.netPerformance;
|
||||
let currentNetPerformancePercent =
|
||||
currentPositions.netPerformancePercentage;
|
||||
const totalInvestment = currentPositions.totalInvestment;
|
||||
|
||||
// if (currentGrossPerformance.mul(currentGrossPerformancePercent).lt(0)) {
|
||||
// // If algebraic sign is different, harmonize it
|
||||
// currentGrossPerformancePercent = currentGrossPerformancePercent.mul(-1);
|
||||
// }
|
||||
|
||||
// if (currentNetPerformance.mul(currentNetPerformancePercent).lt(0)) {
|
||||
// // If algebraic sign is different, harmonize it
|
||||
// currentNetPerformancePercent = currentNetPerformancePercent.mul(-1);
|
||||
// }
|
||||
const currentGrossPerformance = grossPerformance;
|
||||
const currentGrossPerformancePercent = grossPerformancePercentage;
|
||||
let currentNetPerformance = netPerformance;
|
||||
let currentNetPerformancePercent = netPerformancePercentage;
|
||||
|
||||
const historicalDataContainer = await this.getChart({
|
||||
dateRange,
|
||||
@ -1084,28 +1094,28 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
hasErrors,
|
||||
chart: historicalDataContainer.items.map(
|
||||
({
|
||||
date,
|
||||
netPerformance,
|
||||
netPerformance: netPerformanceOfItem,
|
||||
netPerformanceInPercentage,
|
||||
totalInvestment,
|
||||
totalInvestment: totalInvestmentOfItem,
|
||||
value
|
||||
}) => {
|
||||
return {
|
||||
date,
|
||||
netPerformance,
|
||||
netPerformanceInPercentage,
|
||||
totalInvestment,
|
||||
value
|
||||
value,
|
||||
netPerformance: netPerformanceOfItem,
|
||||
totalInvestment: totalInvestmentOfItem
|
||||
};
|
||||
}
|
||||
),
|
||||
errors: currentPositions.errors,
|
||||
firstOrderDate: parseDate(historicalDataContainer.items[0]?.date),
|
||||
hasErrors: currentPositions.hasErrors || hasErrors,
|
||||
performance: {
|
||||
currentValue,
|
||||
currentValue: currentValue.toNumber(),
|
||||
currentGrossPerformance: currentGrossPerformance.toNumber(),
|
||||
currentGrossPerformancePercent:
|
||||
currentGrossPerformancePercent.toNumber(),
|
||||
@ -1218,12 +1228,10 @@ export class PortfolioService {
|
||||
|
||||
private async getCashPositions({
|
||||
cashDetails,
|
||||
investment,
|
||||
userCurrency,
|
||||
value
|
||||
}: {
|
||||
cashDetails: CashDetails;
|
||||
investment: Big;
|
||||
userCurrency: string;
|
||||
value: Big;
|
||||
}) {
|
||||
@ -1258,12 +1266,9 @@ export class PortfolioService {
|
||||
|
||||
for (const symbol of Object.keys(cashPositions)) {
|
||||
// Calculate allocations for each currency
|
||||
cashPositions[symbol].allocationCurrent = value.gt(0)
|
||||
cashPositions[symbol].allocationInPercentage = value.gt(0)
|
||||
? new Big(cashPositions[symbol].value).div(value).toNumber()
|
||||
: 0;
|
||||
cashPositions[symbol].allocationInvestment = investment.gt(0)
|
||||
? new Big(cashPositions[symbol].investment).div(investment).toNumber()
|
||||
: 0;
|
||||
}
|
||||
|
||||
return cashPositions;
|
||||
@ -1430,8 +1435,7 @@ export class PortfolioService {
|
||||
}): PortfolioPosition {
|
||||
return {
|
||||
currency,
|
||||
allocationCurrent: 0,
|
||||
allocationInvestment: 0,
|
||||
allocationInPercentage: 0,
|
||||
assetClass: AssetClass.CASH,
|
||||
assetSubClass: AssetClass.CASH,
|
||||
countries: [],
|
||||
@ -1705,6 +1709,14 @@ export class PortfolioService {
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
}) {
|
||||
const ordersOfTypeItem = await this.orderService.getOrders({
|
||||
filters,
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts,
|
||||
types: ['ITEM']
|
||||
});
|
||||
|
||||
const accounts: PortfolioDetails['accounts'] = {};
|
||||
|
||||
let currentAccounts: (Account & {
|
||||
@ -1735,10 +1747,18 @@ export class PortfolioService {
|
||||
});
|
||||
|
||||
for (const account of currentAccounts) {
|
||||
const ordersByAccount = orders.filter(({ accountId }) => {
|
||||
let ordersByAccount = orders.filter(({ accountId }) => {
|
||||
return accountId === account.id;
|
||||
});
|
||||
|
||||
const ordersOfTypeItemByAccount = ordersOfTypeItem.filter(
|
||||
({ accountId }) => {
|
||||
return accountId === account.id;
|
||||
}
|
||||
);
|
||||
|
||||
ordersByAccount = ordersByAccount.concat(ordersOfTypeItemByAccount);
|
||||
|
||||
accounts[account.id] = {
|
||||
balance: account.balance,
|
||||
currency: account.currency,
|
||||
@ -1758,7 +1778,9 @@ export class PortfolioService {
|
||||
for (const order of ordersByAccount) {
|
||||
let currentValueOfSymbolInBaseCurrency =
|
||||
order.quantity *
|
||||
portfolioItemsNow[order.SymbolProfile.symbol]?.marketPrice ?? 0;
|
||||
(portfolioItemsNow[order.SymbolProfile.symbol]?.marketPrice ??
|
||||
order.unitPrice ??
|
||||
0);
|
||||
let originalValueOfSymbolInBaseCurrency =
|
||||
this.exchangeRateDataService.toCurrency(
|
||||
order.quantity * order.unitPrice,
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { PROPERTY_IS_USER_SIGNUP_ENABLED } from '@ghostfolio/common/config';
|
||||
import { User, UserSettings } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
@ -31,7 +29,6 @@ import { UserService } from './user.service';
|
||||
@Controller('user')
|
||||
export class UserController {
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly propertyService: PropertyService,
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||
|
@ -97,6 +97,7 @@ export class UserService {
|
||||
const {
|
||||
accessToken,
|
||||
Account,
|
||||
Analytics,
|
||||
authChallenge,
|
||||
createdAt,
|
||||
id,
|
||||
@ -107,7 +108,12 @@ export class UserService {
|
||||
thirdPartyId,
|
||||
updatedAt
|
||||
} = await this.prismaService.user.findUnique({
|
||||
include: { Account: true, Settings: true, Subscription: true },
|
||||
include: {
|
||||
Account: true,
|
||||
Analytics: true,
|
||||
Settings: true,
|
||||
Subscription: true
|
||||
},
|
||||
where: userWhereUniqueInput
|
||||
});
|
||||
|
||||
@ -121,7 +127,8 @@ export class UserService {
|
||||
role,
|
||||
Settings,
|
||||
thirdPartyId,
|
||||
updatedAt
|
||||
updatedAt,
|
||||
activityCount: Analytics?.activityCount
|
||||
};
|
||||
|
||||
if (user?.Settings) {
|
||||
@ -154,15 +161,22 @@ export class UserService {
|
||||
(user.Settings.settings as UserSettings).viewMode = 'DEFAULT';
|
||||
}
|
||||
|
||||
let currentPermissions = getPermissions(user.role);
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||
user.subscription =
|
||||
this.subscriptionService.getSubscription(Subscription);
|
||||
}
|
||||
|
||||
let currentPermissions = getPermissions(user.role);
|
||||
if (
|
||||
Analytics?.activityCount % 25 === 0 &&
|
||||
user.subscription?.type === 'Basic'
|
||||
) {
|
||||
currentPermissions.push(permissions.enableSubscriptionInterstitial);
|
||||
}
|
||||
|
||||
if (user.subscription?.type === 'Premium') {
|
||||
currentPermissions.push(permissions.reportDataGlitch);
|
||||
if (user.subscription?.type === 'Premium') {
|
||||
currentPermissions.push(permissions.reportDataGlitch);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { cloneDeep, isObject } from 'lodash';
|
||||
import { cloneDeep, isArray, isObject } from 'lodash';
|
||||
|
||||
export function hasNotDefinedValuesInObject(aObject: Object): boolean {
|
||||
for (const key in aObject) {
|
||||
@ -43,15 +43,23 @@ export function redactAttributes({
|
||||
|
||||
for (const option of options) {
|
||||
if (redactedObject.hasOwnProperty(option.attribute)) {
|
||||
redactedObject[option.attribute] =
|
||||
option.valueMap[redactedObject[option.attribute]] ??
|
||||
option.valueMap['*'] ??
|
||||
redactedObject[option.attribute];
|
||||
if (option.valueMap['*'] || option.valueMap['*'] === null) {
|
||||
redactedObject[option.attribute] = option.valueMap['*'];
|
||||
} else if (option.valueMap[redactedObject[option.attribute]]) {
|
||||
redactedObject[option.attribute] =
|
||||
option.valueMap[redactedObject[option.attribute]];
|
||||
}
|
||||
} else {
|
||||
// If the attribute is not present on the current object,
|
||||
// check if it exists on any nested objects
|
||||
for (const property in redactedObject) {
|
||||
if (typeof redactedObject[property] === 'object') {
|
||||
if (isArray(redactedObject[property])) {
|
||||
redactedObject[property] = redactedObject[property].map(
|
||||
(currentObject) => {
|
||||
return redactAttributes({ options, object: currentObject });
|
||||
}
|
||||
);
|
||||
} else if (isObject(redactedObject[property])) {
|
||||
// Recursively call the function on the nested object
|
||||
redactedObject[property] = redactAttributes({
|
||||
options,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||
import { redactAttributes } from '@ghostfolio/api/helper/object.helper';
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
@ -28,59 +28,36 @@ export class RedactValuesInResponseInterceptor<T>
|
||||
hasImpersonationId ||
|
||||
this.userService.isRestrictedView(request.user)
|
||||
) {
|
||||
if (data.accounts) {
|
||||
for (const accountId of Object.keys(data.accounts)) {
|
||||
if (data.accounts[accountId]?.balance !== undefined) {
|
||||
data.accounts[accountId].balance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.activities) {
|
||||
data.activities = data.activities.map((activity: Activity) => {
|
||||
if (activity.Account?.balance !== undefined) {
|
||||
activity.Account.balance = null;
|
||||
}
|
||||
|
||||
if (activity.comment !== undefined) {
|
||||
activity.comment = null;
|
||||
}
|
||||
|
||||
if (activity.fee !== undefined) {
|
||||
activity.fee = null;
|
||||
}
|
||||
|
||||
if (activity.feeInBaseCurrency !== undefined) {
|
||||
activity.feeInBaseCurrency = null;
|
||||
}
|
||||
|
||||
if (activity.quantity !== undefined) {
|
||||
activity.quantity = null;
|
||||
}
|
||||
|
||||
if (activity.unitPrice !== undefined) {
|
||||
activity.unitPrice = null;
|
||||
}
|
||||
|
||||
if (activity.value !== undefined) {
|
||||
activity.value = null;
|
||||
}
|
||||
|
||||
if (activity.valueInBaseCurrency !== undefined) {
|
||||
activity.valueInBaseCurrency = null;
|
||||
}
|
||||
|
||||
return activity;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.filteredValueInBaseCurrency) {
|
||||
data.filteredValueInBaseCurrency = null;
|
||||
}
|
||||
|
||||
if (data.totalValueInBaseCurrency) {
|
||||
data.totalValueInBaseCurrency = null;
|
||||
}
|
||||
data = redactAttributes({
|
||||
object: data,
|
||||
options: [
|
||||
'balance',
|
||||
'balanceInBaseCurrency',
|
||||
'comment',
|
||||
'convertedBalance',
|
||||
'dividendInBaseCurrency',
|
||||
'fee',
|
||||
'feeInBaseCurrency',
|
||||
'filteredValueInBaseCurrency',
|
||||
'grossPerformance',
|
||||
'investment',
|
||||
'netPerformance',
|
||||
'quantity',
|
||||
'symbolMapping',
|
||||
'totalBalanceInBaseCurrency',
|
||||
'totalValueInBaseCurrency',
|
||||
'unitPrice',
|
||||
'value',
|
||||
'valueInBaseCurrency'
|
||||
].map((attribute) => {
|
||||
return {
|
||||
attribute,
|
||||
valueMap: {
|
||||
'*': null
|
||||
}
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
|
@ -42,7 +42,7 @@ export class ConfigurationService {
|
||||
MAX_ITEM_IN_CACHE: num({ default: 9999 }),
|
||||
PORT: port({ default: 3333 }),
|
||||
RAPID_API_API_KEY: str({ default: '' }),
|
||||
REDIS_HOST: host({ default: 'localhost' }),
|
||||
REDIS_HOST: str({ default: 'localhost' }),
|
||||
REDIS_PASSWORD: str({ default: '' }),
|
||||
REDIS_PORT: port({ default: 6379 }),
|
||||
ROOT_URL: str({ default: 'http://localhost:4200' }),
|
||||
|
@ -1,18 +0,0 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
|
||||
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
@ -65,7 +65,10 @@
|
||||
"output": "./../assets/"
|
||||
}
|
||||
],
|
||||
"styles": ["apps/client/src/styles.scss"],
|
||||
"styles": [
|
||||
"apps/client/src/styles/theme.scss",
|
||||
"apps/client/src/styles.scss"
|
||||
],
|
||||
"scripts": ["node_modules/marked/marked.min.js"],
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
|
@ -116,6 +116,13 @@ const routes: Routes = [
|
||||
'./pages/blog/2022/12/the-importance-of-tracking-your-personal-finances/the-importance-of-tracking-your-personal-finances-page.module'
|
||||
).then((m) => m.TheImportanceOfTrackingYourPersonalFinancesPageModule)
|
||||
},
|
||||
{
|
||||
path: 'blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.module'
|
||||
).then((m) => m.GhostfolioAufSackgeldVorgestelltPageModule)
|
||||
},
|
||||
{
|
||||
path: 'demo',
|
||||
loadChildren: () =>
|
||||
@ -215,9 +222,8 @@ const routes: Routes = [
|
||||
// Preload all lazy loaded modules with the attribute preload === true
|
||||
{
|
||||
anchorScrolling: 'enabled',
|
||||
preloadingStrategy: ModulePreloadService,
|
||||
preloadingStrategy: ModulePreloadService
|
||||
// enableTracing: true // <-- debugging purposes only
|
||||
relativeLinkResolution: 'legacy'
|
||||
}
|
||||
)
|
||||
],
|
||||
|
@ -1,26 +1,17 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Inject,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
NavigationEnd,
|
||||
PRIMARY_OUTLET,
|
||||
Router
|
||||
} from '@angular/router';
|
||||
import {
|
||||
primaryColorHex,
|
||||
secondaryColorHex,
|
||||
warnColorHex
|
||||
} from '@ghostfolio/common/config';
|
||||
import { NavigationEnd, PRIMARY_OUTLET, Router } from '@angular/router';
|
||||
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { ColorScheme } from '@ghostfolio/common/types';
|
||||
import { MaterialCssVarsService } from 'angular-material-css-vars';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { filter, takeUntil } from 'rxjs/operators';
|
||||
@ -52,7 +43,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private materialCssVarsService: MaterialCssVarsService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
private router: Router,
|
||||
private title: Title,
|
||||
private tokenStorageService: TokenStorageService,
|
||||
@ -126,16 +117,20 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
? userPreferredColorScheme === 'DARK'
|
||||
: window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
this.materialCssVarsService.setDarkTheme(isDarkTheme);
|
||||
this.toggleThemeStyleClass(isDarkTheme);
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addListener((event) => {
|
||||
if (!this.user?.settings.colorScheme) {
|
||||
this.materialCssVarsService.setDarkTheme(event.matches);
|
||||
this.toggleThemeStyleClass(event.matches);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.materialCssVarsService.setPrimaryColor(primaryColorHex);
|
||||
this.materialCssVarsService.setAccentColor(secondaryColorHex);
|
||||
this.materialCssVarsService.setWarnColor(warnColorHex);
|
||||
private toggleThemeStyleClass(isDarkTheme: boolean) {
|
||||
if (isDarkTheme) {
|
||||
this.document.body.classList.add('is-dark-theme');
|
||||
} else {
|
||||
this.document.body.classList.remove('is-dark-theme');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,19 @@
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import {
|
||||
DateAdapter,
|
||||
MAT_DATE_FORMATS,
|
||||
MAT_DATE_LOCALE,
|
||||
MatNativeDateModule
|
||||
} from '@angular/material/core';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete';
|
||||
import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips';
|
||||
import { MatLegacySnackBarModule as MatSnackBarModule } from '@angular/material/legacy-snack-bar';
|
||||
import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { MaterialCssVarsModule } from 'angular-material-css-vars';
|
||||
import { MarkdownModule } from 'ngx-markdown';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { NgxStripeModule, STRIPE_PUBLISHABLE_KEY } from 'ngx-stripe';
|
||||
@ -25,6 +24,7 @@ import { DateFormats } from './adapter/date-formats';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { GfHeaderModule } from './components/header/header.module';
|
||||
import { GfSubscriptionInterstitialDialogModule } from './components/subscription-interstitial-dialog/subscription-interstitial-dialog.module';
|
||||
import { authInterceptorProviders } from './core/auth.interceptor';
|
||||
import { httpResponseInterceptorProviders } from './core/http-response.interceptor';
|
||||
import { LanguageService } from './core/language.service';
|
||||
@ -40,15 +40,11 @@ export function NgxStripeFactory(): string {
|
||||
BrowserAnimationsModule,
|
||||
BrowserModule,
|
||||
GfHeaderModule,
|
||||
GfSubscriptionInterstitialDialogModule,
|
||||
HttpClientModule,
|
||||
MarkdownModule.forRoot(),
|
||||
MatAutocompleteModule,
|
||||
MatChipsModule,
|
||||
MaterialCssVarsModule.forRoot({
|
||||
darkThemeClass: 'is-dark-theme',
|
||||
isAutoContrast: true,
|
||||
lightThemeClass: 'is-light-theme'
|
||||
}),
|
||||
MatNativeDateModule,
|
||||
MatSnackBarModule,
|
||||
MatTooltipModule,
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table';
|
||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
||||
import { Access } from '@ghostfolio/common/interfaces';
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
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 { MatTableModule } from '@angular/material/table';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
||||
import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table';
|
||||
|
||||
import { AccessTableComponent } from './access-table.component';
|
||||
|
||||
|
@ -6,7 +6,10 @@ import {
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
} from '@angular/material/legacy-dialog';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||
|
@ -1,7 +1,7 @@
|
||||
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 { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
|
@ -9,8 +9,8 @@ import {
|
||||
Output,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Router } from '@angular/router';
|
||||
import { Account as AccountModel } from '@prisma/client';
|
||||
import { get } from 'lodash';
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
||||
import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
@ -20,7 +19,6 @@ import { AccountsTableComponent } from './accounts-table.component';
|
||||
GfSymbolIconModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatInputModule,
|
||||
MatMenuModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
||||
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
||||
|
||||
import { AdminJobsComponent } from './admin-jobs.component';
|
||||
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
|
@ -6,7 +6,10 @@ import {
|
||||
OnDestroy
|
||||
} from '@angular/core';
|
||||
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
} from '@angular/material/legacy-dialog';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@ -36,7 +39,7 @@ export class MarketDataDetailDialog implements OnDestroy {
|
||||
this.dateAdapter.setLocale(this.locale);
|
||||
}
|
||||
|
||||
public onCancel(): void {
|
||||
public onCancel() {
|
||||
this.dialogRef.close({ withRefresh: false });
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
|
||||
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
|
||||
|
||||
import { MarketDataDetailDialog } from './market-data-detail-dialog.component';
|
||||
|
||||
|
@ -6,9 +6,9 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
||||
import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table';
|
||||
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';
|
||||
|
@ -1,13 +1,13 @@
|
||||
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 { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
||||
import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table';
|
||||
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';
|
||||
import { GfAssetProfileDialogModule } from './asset-profile-dialog/asset-profile-dialog.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdminMarketDataComponent],
|
||||
|
@ -7,7 +7,10 @@ import {
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
} from '@angular/material/legacy-dialog';
|
||||
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import {
|
||||
|
@ -2,10 +2,10 @@ import { TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
|
||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
||||
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
|
||||
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
@ -1,5 +1,5 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
||||
import { MatLegacySlideToggleChange as MatSlideToggleChange } from '@angular/material/legacy-slide-toggle';
|
||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
||||
import { MatLegacySlideToggleModule as MatSlideToggleModule } from '@angular/material/legacy-slide-toggle';
|
||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
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 { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
|
||||
import { DialogFooterComponent } from './dialog-footer.component';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
|
||||
import { DialogHeaderComponent } from './dialog-header.component';
|
||||
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
OnChanges,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
|
@ -1,7 +1,7 @@
|
||||
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 { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-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';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
|
||||
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
||||
|
@ -110,13 +110,12 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
range: this.user?.settings?.dateRange
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((response) => {
|
||||
this.errors = response.errors;
|
||||
this.hasError = response.hasErrors;
|
||||
this.performance = response.performance;
|
||||
.subscribe(({ chart, errors, performance }) => {
|
||||
this.errors = errors;
|
||||
this.performance = performance;
|
||||
this.isLoadingPerformance = false;
|
||||
|
||||
this.historicalDataItems = response.chart.map(
|
||||
this.historicalDataItems = chart.map(
|
||||
({ date, netPerformanceInPercentage }) => {
|
||||
return {
|
||||
date,
|
||||
|
@ -37,7 +37,6 @@
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[errors]="errors"
|
||||
[hasError]="hasError"
|
||||
[isAllTimeHigh]="isAllTimeHigh"
|
||||
[isAllTimeLow]="isAllTimeLow"
|
||||
[isLoading]="isLoadingPerformance"
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import {
|
||||
MatSnackBar,
|
||||
MatSnackBarRef,
|
||||
TextOnlySnackBar
|
||||
} from '@angular/material/snack-bar';
|
||||
MatLegacySnackBar as MatSnackBar,
|
||||
MatLegacySnackBarRef as MatSnackBarRef,
|
||||
LegacyTextOnlySnackBar as TextOnlySnackBar
|
||||
} from '@angular/material/legacy-snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatLegacyCheckboxChange as MatCheckboxChange } from '@angular/material/legacy-checkbox';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
} from '@angular/material/legacy-dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
|
||||
import {
|
||||
|
@ -2,11 +2,11 @@ import { TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
|
||||
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
|
||||
|
||||
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
|
||||
import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.component';
|
||||
|
@ -3,14 +3,14 @@
|
||||
<div
|
||||
class="flex-grow-1 status text-muted text-right"
|
||||
[title]="
|
||||
hasError && !isLoading
|
||||
errors?.length > 0 && !isLoading
|
||||
? 'Sorry! Our data provider partner is experiencing the hiccups.'
|
||||
: ''
|
||||
"
|
||||
(click)="errors?.length > 0 && onShowErrors()"
|
||||
>
|
||||
<ion-icon
|
||||
*ngIf="hasError && !isLoading"
|
||||
*ngIf="errors?.length > 0 && !isLoading"
|
||||
name="alert-circle-outline"
|
||||
></ion-icon>
|
||||
</div>
|
||||
|
@ -28,7 +28,6 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||
@Input() baseCurrency: string;
|
||||
@Input() deviceType: string;
|
||||
@Input() errors: ResponseError['errors'];
|
||||
@Input() hasError: boolean;
|
||||
@Input() isAllTimeHigh: boolean;
|
||||
@Input() isAllTimeLow: boolean;
|
||||
@Input() isLoading: boolean;
|
||||
|
@ -6,7 +6,10 @@ import {
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
} from '@angular/material/legacy-dialog';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
@ -37,6 +40,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
public countries: {
|
||||
[code: string]: { name: string; value: number };
|
||||
};
|
||||
public dividendInBaseCurrency: number;
|
||||
public feeInBaseCurrency: number;
|
||||
public firstBuyDate: string;
|
||||
public grossPerformance: number;
|
||||
public grossPerformancePercent: number;
|
||||
@ -78,6 +83,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
.subscribe(
|
||||
({
|
||||
averagePrice,
|
||||
dividendInBaseCurrency,
|
||||
feeInBaseCurrency,
|
||||
firstBuyDate,
|
||||
grossPerformance,
|
||||
grossPerformancePercent,
|
||||
@ -98,7 +105,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
this.averagePrice = averagePrice;
|
||||
this.benchmarkDataItems = [];
|
||||
this.countries = {};
|
||||
this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=Ghostfolio Data Glitch Report&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${SymbolProfile?.symbol}%0DData Source: ${SymbolProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`;
|
||||
this.dividendInBaseCurrency = dividendInBaseCurrency;
|
||||
this.feeInBaseCurrency = feeInBaseCurrency;
|
||||
this.firstBuyDate = firstBuyDate;
|
||||
this.grossPerformance = grossPerformance;
|
||||
this.grossPerformancePercent = grossPerformancePercent;
|
||||
@ -111,7 +119,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
|
||||
return {
|
||||
date: historicalDataItem.date,
|
||||
value: historicalDataItem.value
|
||||
value: historicalDataItem.marketPrice
|
||||
};
|
||||
}
|
||||
);
|
||||
@ -123,6 +131,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
this.netPerformancePercent = netPerformancePercent;
|
||||
this.orders = orders;
|
||||
this.quantity = quantity;
|
||||
this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=Ghostfolio Data Glitch Report&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${SymbolProfile?.symbol}%0DData Source: ${SymbolProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`;
|
||||
this.sectors = {};
|
||||
this.SymbolProfile = SymbolProfile;
|
||||
this.tags = tags.map(({ id, name }) => {
|
||||
|
@ -119,6 +119,26 @@
|
||||
>Investment</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="data.baseCurrency"
|
||||
[locale]="data.locale"
|
||||
[value]="dividendInBaseCurrency"
|
||||
>Dividend</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="data.baseCurrency"
|
||||
[locale]="data.locale"
|
||||
[value]="feeInBaseCurrency"
|
||||
>Fees</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { GfTrendIndicatorModule } from '@ghostfolio/ui/trend-indicator';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||
|
||||
import { GfPositionModule } from '../position/position.module';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module';
|
||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||
|
||||
|
@ -0,0 +1 @@
|
||||
export interface SubscriptionInterstitialDialogParams {}
|
@ -0,0 +1,25 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
} from '@angular/material/legacy-dialog';
|
||||
|
||||
import { SubscriptionInterstitialDialogParams } from './interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'd-flex flex-column flex-grow-1 h-100' },
|
||||
selector: 'gf-subscription-interstitial-dialog',
|
||||
styleUrls: ['./subscription-interstitial-dialog.scss'],
|
||||
templateUrl: 'subscription-interstitial-dialog.html'
|
||||
})
|
||||
export class SubscriptionInterstitialDialog {
|
||||
public constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: SubscriptionInterstitialDialogParams,
|
||||
public dialogRef: MatDialogRef<SubscriptionInterstitialDialog>
|
||||
) {}
|
||||
|
||||
public onCancel() {
|
||||
this.dialogRef.close({});
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
<h1 class="align-items-center d-flex" mat-dialog-title>
|
||||
<span>Ghostfolio Premium</span>
|
||||
<gf-premium-indicator class="ml-1"></gf-premium-indicator>
|
||||
</h1>
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<p class="h5" i18n>
|
||||
Are you an ambitious investor who needs the full picture?
|
||||
</p>
|
||||
<p i18n>
|
||||
By upgrading to Ghostfolio Premium, you will get these additional features:
|
||||
</p>
|
||||
<ul class="list-unstyled mb-3">
|
||||
<li class="align-items-center d-flex mb-1">
|
||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||
<span i18n>Portfolio Summary</span>
|
||||
</li>
|
||||
<li class="align-items-center d-flex mb-1">
|
||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||
<span i18n>Performance Benchmarks</span>
|
||||
</li>
|
||||
<li class="align-items-center d-flex mb-1">
|
||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||
<span i18n>Allocations</span>
|
||||
</li>
|
||||
<li class="align-items-center d-flex mb-1">
|
||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||
<span i18n>FIRE Calculator</span>
|
||||
</li>
|
||||
<li class="align-items-center d-flex mb-1">
|
||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||
<a i18n [routerLink]="['/features']">and more Features...</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p>Refine your personal investment strategy now.</p>
|
||||
</div>
|
||||
<div class="justify-content-end" mat-dialog-actions>
|
||||
<button i18n mat-button (click)="onCancel()">Skip</button>
|
||||
<a color="primary" mat-flat-button [routerLink]="['/pricing']">
|
||||
<span i18n>Upgrade Plan</span>
|
||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||
</a>
|
||||
</div>
|
@ -0,0 +1,21 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
|
||||
import { SubscriptionInterstitialDialog } from './subscription-interstitial-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [SubscriptionInterstitialDialog],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfPremiumIndicatorModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfSubscriptionInterstitialDialogModule {}
|
@ -0,0 +1,11 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-dialog-content {
|
||||
max-height: unset;
|
||||
|
||||
ion-icon[name='checkmark-circle-outline'] {
|
||||
color: rgba(var(--palette-accent-500), 1);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatLegacyRadioModule as MatRadioModule } from '@angular/material/legacy-radio';
|
||||
|
||||
import { ToggleComponent } from './toggle.component';
|
||||
|
||||
|
@ -8,10 +8,10 @@ import {
|
||||
} from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
MatSnackBar,
|
||||
MatSnackBarRef,
|
||||
TextOnlySnackBar
|
||||
} from '@angular/material/snack-bar';
|
||||
MatLegacySnackBar as MatSnackBar,
|
||||
MatLegacySnackBarRef as MatSnackBarRef,
|
||||
LegacyTextOnlySnackBar as TextOnlySnackBar
|
||||
} from '@angular/material/legacy-snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
|
@ -42,9 +42,11 @@
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Tweet to Ghostfolio on Twitter"
|
||||
>@ghostfolio_</a
|
||||
>, send an e-mail to
|
||||
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
|
||||
>hi@ghostfol.io</a
|
||||
><ng-container *ngIf="hasPermissionForSubscription"
|
||||
>, send an e-mail to
|
||||
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
|
||||
>hi@ghostfol.io</a
|
||||
></ng-container
|
||||
>
|
||||
or open an issue at
|
||||
<a
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
|
||||
import { AboutPageRoutingModule } from './about-page-routing.module';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { MarkdownModule } from 'ngx-markdown';
|
||||
|
||||
import { ChangelogPageRoutingModule } from './changelog-page-routing.module';
|
||||
|
@ -5,16 +5,16 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
||||
import {
|
||||
MatSlideToggle,
|
||||
MatSlideToggleChange
|
||||
} from '@angular/material/slide-toggle';
|
||||
MatLegacySlideToggle as MatSlideToggle,
|
||||
MatLegacySlideToggleChange as MatSlideToggleChange
|
||||
} from '@angular/material/legacy-slide-toggle';
|
||||
import {
|
||||
MatSnackBar,
|
||||
MatSnackBarRef,
|
||||
TextOnlySnackBar
|
||||
} from '@angular/material/snack-bar';
|
||||
MatLegacySnackBar as MatSnackBar,
|
||||
MatLegacySnackBarRef as MatSnackBarRef,
|
||||
LegacyTextOnlySnackBar as TextOnlySnackBar
|
||||
} from '@angular/material/legacy-snack-bar';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
|
||||
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
||||
import { MatLegacySlideToggleModule as MatSlideToggleModule } from '@angular/material/legacy-slide-toggle';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
@ -31,7 +30,6 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di
|
||||
MatCardModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatSlideToggleModule,
|
||||
ReactiveFormsModule,
|
||||
|
@ -4,7 +4,10 @@ import {
|
||||
Inject,
|
||||
OnDestroy
|
||||
} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
} from '@angular/material/legacy-dialog';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
|
||||
@ -26,7 +29,7 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
public onCancel(): void {
|
||||
public onCancel() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
|
||||
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
|
||||
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
||||
|
||||
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.component';
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module';
|
||||
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
|
||||
|
@ -4,7 +4,10 @@ import {
|
||||
Inject,
|
||||
OnDestroy
|
||||
} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import {
|
||||
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
|
||||
MatLegacyDialogRef as MatDialogRef
|
||||
} from '@angular/material/legacy-dialog';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { DataService } from '../../../services/data.service';
|
||||
@ -36,7 +39,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
||||
this.platforms = platforms;
|
||||
}
|
||||
|
||||
public onCancel(): void {
|
||||
public onCancel() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
|
||||
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
|
||||
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
|
||||
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
|
||||
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
||||
|
||||
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.component';
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
|
||||
import { MatLegacyTabsModule as MatTabsModule } from '@angular/material/legacy-tabs';
|
||||
import { GfAdminJobsModule } from '@ghostfolio/client/components/admin-jobs/admin-jobs.module';
|
||||
import { GfAdminMarketDataModule } from '@ghostfolio/client/components/admin-market-data/admin-market-data.module';
|
||||
import { GfAdminOverviewModule } from '@ghostfolio/client/components/admin-overview/admin-overview.module';
|
||||
|
@ -7,7 +7,7 @@
|
||||
<div class="mb-3 text-muted"><small>2022-07-23</small></div>
|
||||
<img
|
||||
alt="Ghostfolio meets Internet Identity Teaser"
|
||||
class="rounded w-100"
|
||||
class="border rounded w-100"
|
||||
src="../assets/images/blog/ghostfolio-meets-internet-identity.png"
|
||||
title="Ghostfolio meets Internet Identity"
|
||||
/>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { TheImportanceOfTrackingYourPersonalFinancesRoutingModule } from './the-importance-of-tracking-your-personal-finances-page-routing.module';
|
||||
|
@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { GhostfolioAufSackgeldVorgestelltPageComponent } from './ghostfolio-auf-sackgeld-vorgestellt-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: GhostfolioAufSackgeldVorgestelltPageComponent,
|
||||
path: '',
|
||||
title: 'Ghostfolio auf Sackgeld.com vorgestellt'
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class GhostfolioAufSackgeldVorgestelltPageRoutingModule {}
|
@ -0,0 +1,9 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-ghostfolio-auf-sackgeld-vorgestellt-page',
|
||||
styleUrls: ['./ghostfolio-auf-sackgeld-vorgestellt-page.scss'],
|
||||
templateUrl: './ghostfolio-auf-sackgeld-vorgestellt-page.html'
|
||||
})
|
||||
export class GhostfolioAufSackgeldVorgestelltPageComponent {}
|
@ -0,0 +1,178 @@
|
||||
<div class="blog container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1">Ghostfolio auf Sackgeld.com vorgestellt</h1>
|
||||
<div class="mb-3 text-muted"><small>2023-01-21</small></div>
|
||||
<img
|
||||
alt="Ghostfolio auf Sackgeld.com vorgestellt Teaser"
|
||||
class="border rounded w-100"
|
||||
src="../assets/images/blog/ghostfolio-x-sackgeld.png"
|
||||
title="Ghostfolio auf Sackgeld.com vorgestellt"
|
||||
/>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
Wir freuen uns darüber, dass unsere Open Source Portfolio Tracking
|
||||
Software <a href="https://ghostfol.io">Ghostfolio</a> auf dem
|
||||
FinTech Newsportal <i>Sackgeld.com</i> vorgestellt wurde.
|
||||
</p>
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<blockquote class="blockquote m-0">
|
||||
<p class="mb-0">
|
||||
«Ghostfolio ist ein umfassender Portfolio Performance
|
||||
Tracker der einfach zu bedienen ist, mit einigen sehr
|
||||
innovativen Features aufwartet und echten Mehrwert für den
|
||||
Investor bringt.»
|
||||
</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Im ausführlichen Bericht wird die Funktionsweise von Ghostfolio
|
||||
erläutert, die unterstützten Assets aufgeführt sowie die
|
||||
Preisstruktur im Vergleich zu anderen Anbietern dargelegt.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">
|
||||
Ghostfolio – Open Source Wealth Management Software
|
||||
</h2>
|
||||
<p>
|
||||
Ghostfolio ermöglicht es dir, dein Portfolio einfach zu verfolgen
|
||||
und zu analysieren. Es bietet dir detaillierte Informationen über
|
||||
deine Positionen, historische Entwicklung, Performance und die
|
||||
Zusammenstellung deines Portfolios. Durch die Open Source-Lizenz (<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/LICENSE"
|
||||
target="_blank"
|
||||
>GNU Affero General Public License v3.0</a
|
||||
>) wird die Software ständig weiterentwickelt, verbessert und du
|
||||
hast sogar selbst die Möglichkeit, dich daran zu beteiligen. Wir
|
||||
sind davon überzeugt, mit diesem Open-Source-Ansatz von Ghostfolio
|
||||
das Finanzwissen und Investieren für alle zugänglicher zu machen.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Sackgeld.com – App für ein höheres Sackgeld</h2>
|
||||
<p>
|
||||
Das Schweizer FinTech Nachrichtenportal
|
||||
<a href="https://www.sackgeld.com" target="_blank">Sackgeld.com</a>
|
||||
informiert über die neuesten Entwicklungen und Innovationen im
|
||||
Bereich FinTech. Dazu gehören News, Artikel und persönliche
|
||||
Erfahrungen aus der Welt der digitalen Finanz Apps, Säule 3a, P2P
|
||||
und Immobilien.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
Wenn du mehr über Ghostfolio erfahren möchtest, kannst du hier den
|
||||
ganzen Artikel nachlesen:
|
||||
<a
|
||||
href="https://www.sackgeld.com/was-taugt-ghostfolio-als-portfolio-performance-tracking-tool"
|
||||
target="_blank"
|
||||
>Was taugt Ghostfolio als Portfolio Performance Tracking-Tool?</a
|
||||
>
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<ul class="list-inline">
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">AGPL-3.0</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Aktie</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Altersvorsorge</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Anlage</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">App</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Asset</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Feedback</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Finanzwissen</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Fintech</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Ghostfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Immobilien</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Innovation</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Investieren</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Lizenz</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Media</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Open Source</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">OSS</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">P2P</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Performance</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Portfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Presse</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Sackgeld</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Säule 3a</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Schweiz</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Software</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Taschengeld</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Tool</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Vermögen</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Vorsorge</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Wealth Management</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,17 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { GhostfolioAufSackgeldVorgestelltPageRoutingModule } from './ghostfolio-auf-sackgeld-vorgestellt-page-routing.module';
|
||||
import { GhostfolioAufSackgeldVorgestelltPageComponent } from './ghostfolio-auf-sackgeld-vorgestellt-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [GhostfolioAufSackgeldVorgestelltPageComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GhostfolioAufSackgeldVorgestelltPageRoutingModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GhostfolioAufSackgeldVorgestelltPageModule {}
|
@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
@ -2,6 +2,32 @@
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Blog</h3>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex overflow-hidden w-100"
|
||||
href="../de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt"
|
||||
>
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="h6 m-0 text-truncate">
|
||||
Ghostfolio auf Sackgeld.com vorgestellt
|
||||
</div>
|
||||
<div class="d-flex text-muted">2023-01-21</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
class="chevron text-muted"
|
||||
name="chevron-forward-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
|
||||
import { BlogPageRoutingModule } from './blog-page-routing.module';
|
||||
import { BlogPageComponent } from './blog-page.component';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
|
||||
import { FaqPageRoutingModule } from './faq-page-routing.module';
|
||||
import { FaqPageComponent } from './faq-page.component';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
|
||||
import { FeaturesPageRoutingModule } from './features-page-routing.module';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatLegacyTabsModule as MatTabsModule } from '@angular/material/legacy-tabs';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfHomeHoldingsModule } from '@ghostfolio/client/components/home-holdings/home-holdings.module';
|
||||
import { GfHomeMarketModule } from '@ghostfolio/client/components/home-market/home-market.module';
|
||||
|
@ -106,7 +106,7 @@
|
||||
|
||||
<div class="row mb-5">
|
||||
<div class="col-12 text-center text-muted"><small>As seen in</small></div>
|
||||
<div class="col-md-2 d-flex justify-content-center my-1">
|
||||
<div class="col-md-3 d-flex justify-content-center my-1">
|
||||
<a
|
||||
class="d-block logo logo-alternative-to mask"
|
||||
href="https://alternativeto.net"
|
||||
@ -114,7 +114,7 @@
|
||||
title="AlternativeTo - Crowdsourced software recommendations"
|
||||
></a>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex justify-content-center my-1">
|
||||
<div class="col-md-3 d-flex justify-content-center my-1">
|
||||
<a
|
||||
class="d-block logo logo-awesome"
|
||||
href="https://github.com/awesome-selfhosted/awesome-selfhosted"
|
||||
@ -122,7 +122,7 @@
|
||||
title="Awesome-Selfhosted: A list of Free Software network services and web applications which can be hosted on your own servers"
|
||||
></a>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex justify-content-center my-1">
|
||||
<div class="col-md-3 d-flex justify-content-center my-1">
|
||||
<a
|
||||
class="d-block logo logo-openstartup"
|
||||
href="https://openstartup.tm"
|
||||
@ -130,7 +130,7 @@
|
||||
title="Open Startup: The most complete list of open startups"
|
||||
></a>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex justify-content-center my-1">
|
||||
<div class="col-md-3 d-flex justify-content-center my-1">
|
||||
<a
|
||||
class="d-block logo logo-privacy-tools mask"
|
||||
href="https://www.privacytools.io"
|
||||
@ -138,7 +138,7 @@
|
||||
title="Privacy Tools: Software Alternatives and Encryption"
|
||||
></a>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex justify-content-center my-1">
|
||||
<div class="col-md-3 d-flex justify-content-center my-1">
|
||||
<a
|
||||
class="d-block logo logo-product-hunt"
|
||||
href="https://www.producthunt.com"
|
||||
@ -146,7 +146,23 @@
|
||||
title="Product Hunt – The best new products in tech."
|
||||
></a>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex justify-content-center my-1">
|
||||
<div class="col-md-3 d-flex justify-content-center my-1">
|
||||
<a
|
||||
class="d-block logo logo-sackgeld mask"
|
||||
href="https://www.sackgeld.com"
|
||||
target="_blank"
|
||||
title="Sackgeld.com – Apps für ein höheres Sackgeld"
|
||||
></a>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex justify-content-center my-1">
|
||||
<a
|
||||
class="d-block logo logo-sourceforge mask"
|
||||
href="https://sourceforge.net"
|
||||
target="_blank"
|
||||
title="SourceForge: The Complete Open-Source and Business Software Platform"
|
||||
></a>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex justify-content-center my-1">
|
||||
<a
|
||||
class="d-block logo logo-unraid mask"
|
||||
href="https://unraid.net"
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user