Compare commits

..

31 Commits

Author SHA1 Message Date
ad88acff1c Release 1.164.0 (#1038) 2022-06-23 19:25:46 +02:00
1ff736537c Feature/add positions table to public page (#1037)
* Add positions table

* Update changelog
2022-06-23 19:24:24 +02:00
1fa65e1efd Release 1.163.0 (#1036) 2022-06-22 20:32:06 +02:00
df6bb489c2 Feature/improve onboarding for ios (#1035)
* Improve onboarding for iOS

* Update changelog
2022-06-22 20:30:03 +02:00
928a13310d Improve title (#1031) 2022-06-21 17:58:13 +02:00
2384861953 Remove experimental (#1032) 2022-06-20 20:32:51 +02:00
fe90bda6fb Release 1.162.0 (#1029) 2022-06-18 17:48:54 +02:00
d4b29ff11c Feature/add privacy policy page (#1028)
* Add privacy policy page

* Update changelog
2022-06-18 17:46:51 +02:00
a0a26cfa58 Feature/simplify header (#1027)
* Hide pricing page link for Premium users

* Harmonize content

* Update changelog
2022-06-18 11:57:27 +02:00
1610150427 Bugfix/fix currency conversion of ila to ils (#1026)
* Fix currency conversion: ILA to ILS

* Update changelog
2022-06-17 20:34:41 +02:00
cff8acd7b1 Clean up (#1022) 2022-06-17 20:04:52 +02:00
0f36d6cbdb Release 1.161.1 (#1025) 2022-06-16 17:20:44 +02:00
046e28b521 Release 1.161.0 (#1024) 2022-06-16 16:51:57 +02:00
aba562cb35 Bugfix/fix error handling for missing market prices (#1023)
* Add fallback for missing market price

* Update changelog
2022-06-16 16:49:29 +02:00
03f2f33344 Feature/restructure landing page (#1021)
* Restructure landing page

* Update changelog
2022-06-16 16:29:35 +02:00
a996dd7ed5 Feature/add vertical hover line to performance chart (#1020)
* Add vertical hover line

* Update changelog
2022-06-16 14:16:53 +02:00
002b883668 Feature/upgrade to angular 14 (#1019)
* Upgrade to angular 14

* Migrate UntypedFormControl to FormControl

* Update changelog
2022-06-16 10:28:23 +02:00
0b06823893 Release 1.160.0 (#1018) 2022-06-15 20:18:13 +02:00
2dfd779444 Bugfix/fix no data provider has been found in search (#1017)
* Fix default value of DATA_SOURCES

* Update changelog
2022-06-15 20:16:38 +02:00
1824413379 Release 1.159.0 (#1015) 2022-06-15 13:24:07 +02:00
3332ade3d3 Feature/filter public endpoint by equity asset class (#1014)
* Filter by ASSET_CLASS: EQUITY

* Update changelog
2022-06-15 13:21:47 +02:00
8d2e110e3d Feature/change default host (#1013)
* Change default host to 0.0.0.0

* Update changelog
2022-06-14 17:43:42 +02:00
a8fcf09380 Fix link (#1012) 2022-06-13 17:44:55 +02:00
1071f446a8 Release 1.158.1 (#1010) 2022-06-12 19:21:02 +02:00
03b050d1ac Release 1.158.0 (#1009) 2022-06-12 17:53:09 +02:00
58eeff7001 Feature/expose the environment variable host (#1007)
* Expose environment variable: HOST

* Update changelog
2022-06-12 17:51:26 +02:00
76fb8825e4 Feature/restructure self hosting documentation (#1008)
* Restructure self-hosting documentation

* Update changelog
2022-06-12 17:50:45 +02:00
0f9d142afe Feature/decrease number of attempts of queue jobs (#1006)
* Decrease number of attempts

* Update changelog
2022-06-12 17:39:55 +02:00
bd33855a27 Feature/improve message for data provider errors (#1003)
* Add title

* Update changelog
2022-06-12 17:39:02 +02:00
5329e45e2c Feature/add data dialog to queue jobs view (#1005)
* Add data dialog

* Update changelog
2022-06-12 17:30:26 +02:00
e990ecd12c Feature/improve cash balance label (#1002)
* Improve label

* Update changelog
2022-06-12 10:06:18 +02:00
60 changed files with 4813 additions and 5952 deletions

1
.env
View File

@ -14,4 +14,3 @@ ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING>
ALPHA_VANTAGE_API_KEY=
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer
JWT_SECRET_KEY=<INSERT_RANDOM_STRING>
PORT=3333

View File

@ -5,12 +5,85 @@ 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.164.0 - 23.06.2022
### Added
- Added the positions table including performance to the public page
## 1.163.0 - 22.06.2022
### Changed
- Improved the onboarding for iOS
## 1.162.0 - 18.06.2022
### Added
- Added a _Privacy Policy_ page
### Changed
- Simplified the header
### Fixed
- Fixed an issue with the currency inconsistency in the _Yahoo Finance_ service (convert from `ILA` to `ILS`)
## 1.161.1 - 16.06.2022
### Added
- Added the vertical hover line to inspect data points in the performance chart on the home page
### Changed
- Improved the landing page
- Upgraded `angular` from version `13.3.6` to `14.0.2`
- Upgraded `Nx` from version `14.1.4` to `14.3.5`
- Upgraded `storybook` from version `6.4.22` to `6.5.9`
### Fixed
- Improved the error handling of missing market prices
## 1.160.0 - 15.06.2022
### Fixed
- Fixed the `No data provider has been found` error in the search (regression after `envalid` upgrade to `7.3.1` in Ghostfolio `1.157.0`)
## 1.159.0 - 15.06.2022
### Changed
- Changed the default `HOST` to `0.0.0.0`
- Refactored the endpoint of the public page (filter by equity)
## 1.158.1 - 12.06.2022
### Added
- Extended the queue jobs view in the admin control panel by a data dialog
### Changed
- Exposed the environment variable `HOST`
- Decreased the number of attempts of queue jobs from `20` to `10` (fail earlier)
- Improved the message for data provider errors in the client
- Changed the label from _Balance_ to _Cash Balance_ in the account dialog
- Restructured the documentation for self-hosting
## 1.157.0 - 11.06.2022
### Added
- Extended the queue jobs view in the admin control panel by the number of attempts and the status
### Changed
- Migrated the historical market data gathering to the queue design pattern
- Extended the queue jobs view in the admin control panel by the number of attempts and the status
- Refreshed the cryptocurrencies list to support more coins by default
- Increased the historical data chart of the _Fear & Greed Index_ (market mood) to 180 days
- Upgraded `chart.js` from version `3.7.0` to `3.8.0`

View File

@ -22,7 +22,7 @@ RUN node decorate-angular-cli.js
COPY ./angular.json angular.json
COPY ./nx.json nx.json
COPY ./replace.build.js replace.build.js
COPY ./jest.preset.ts jest.preset.ts
COPY ./jest.preset.js jest.preset.js
COPY ./jest.config.ts jest.config.ts
COPY ./tsconfig.base.json tsconfig.base.json
COPY ./libs libs

View File

@ -35,7 +35,7 @@
Our official **[Ghostfolio Premium](https://ghostfol.io/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. The revenue is used for covering the hosting costs.
If you prefer to run Ghostfolio on your own infrastructure (self-hosting), please find further instructions in the section [Run with Docker](#run-with-docker-self-hosting).
If you prefer to run Ghostfolio on your own infrastructure, please find further instructions in the [Self-hosting](#self-hosting) section.
## Why Ghostfolio?
@ -79,14 +79,17 @@ The backend is based on [NestJS](https://nestjs.com) using [PostgreSQL](https://
The frontend is built with [Angular](https://angular.io) and uses [Angular Material](https://material.angular.io) with utility classes from [Bootstrap](https://getbootstrap.com).
## Run with Docker (self-hosting)
## Self-hosting
### Prerequisites
### Run with Docker Compose
- [Docker](https://www.docker.com/products/docker-desktop)
- A local copy of this Git repository (clone)
#### Prerequisites
### a. Run environment
- Basic knowledge of Docker
- Installation of [Docker](https://www.docker.com/products/docker-desktop)
- Local copy of this Git repository (clone)
#### a. Run environment
Run the following command to start the Docker images from [Docker Hub](https://hub.docker.com/r/ghostfolio/ghostfolio):
@ -94,7 +97,7 @@ Run the following command to start the Docker images from [Docker Hub](https://h
docker-compose --env-file ./.env -f docker/docker-compose.yml up -d
```
#### Setup Database
##### Setup Database
Run the following command to setup the database once Ghostfolio is running:
@ -102,7 +105,7 @@ Run the following command to setup the database once Ghostfolio is running:
docker-compose --env-file ./.env -f docker/docker-compose.yml exec ghostfolio yarn database:setup
```
### b. Build and run environment
#### b. Build and run environment
Run the following commands to build and start the Docker images:
@ -111,7 +114,7 @@ docker-compose --env-file ./.env -f docker/docker-compose.build.yml build
docker-compose --env-file ./.env -f docker/docker-compose.build.yml up -d
```
#### Setup Database
##### Setup Database
Run the following command to setup the database once Ghostfolio is running:
@ -119,7 +122,7 @@ Run the following command to setup the database once Ghostfolio is running:
docker-compose --env-file ./.env -f docker/docker-compose.build.yml exec ghostfolio yarn database:setup
```
### Fetch Historical Data
#### Fetch Historical Data
Open http://localhost:3333 in your browser and accomplish these steps:
@ -127,13 +130,13 @@ Open http://localhost:3333 in your browser and accomplish these steps:
1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
1. Click _Sign out_ and check out the _Live Demo_
### Upgrade Version
#### Upgrade Version
1. Increase the version of the `ghostfolio/ghostfolio` Docker image in `docker/docker-compose.yml`
1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d`
1. Then, run the following command to keep your database schema in sync: `docker-compose --env-file ./.env -f docker/docker-compose.yml exec ghostfolio yarn database:migrate`
## Run with _Unraid_ (self-hosting)
### Run with _Unraid_ (Community)
Please follow the instructions of the Ghostfolio [Unraid Community App](https://unraid.net/community/apps?q=ghostfolio).
@ -183,7 +186,7 @@ yarn database:push
Run `yarn test`
## Public API (experimental)
## Public API
### Import Activities

View File

@ -2,6 +2,7 @@
"version": 1,
"projects": {
"api": {
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"root": "apps/api",
"sourceRoot": "apps/api/src",
"projectType": "application",
@ -56,6 +57,7 @@
"tags": []
},
"client": {
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
@ -189,6 +191,7 @@
"tags": []
},
"client-e2e": {
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"root": "apps/client-e2e",
"sourceRoot": "apps/client-e2e/src",
"projectType": "application",
@ -211,6 +214,7 @@
"implicitDependencies": ["client"]
},
"common": {
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"root": "libs/common",
"sourceRoot": "libs/common/src",
"projectType": "library",
@ -233,6 +237,7 @@
"tags": []
},
"ui": {
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"schematics": {
"@schematics/angular:component": {
@ -258,14 +263,12 @@
}
},
"storybook": {
"builder": "@nrwl/storybook:storybook",
"builder": "@storybook/angular:start-storybook",
"options": {
"uiFramework": "@storybook/angular",
"port": 4400,
"config": {
"configFolder": "libs/ui/.storybook"
},
"projectBuildConfig": "ui:build-storybook"
"configDir": "libs/ui/.storybook",
"browserTarget": "ui:build-storybook",
"compodoc": false
},
"configurations": {
"ci": {
@ -274,15 +277,13 @@
}
},
"build-storybook": {
"builder": "@nrwl/storybook:build",
"builder": "@storybook/angular:build-storybook",
"outputs": ["{options.outputPath}"],
"options": {
"uiFramework": "@storybook/angular",
"outputPath": "dist/storybook/ui",
"config": {
"configFolder": "libs/ui/.storybook"
},
"projectBuildConfig": "ui:build-storybook"
"outputDir": "dist/storybook/ui",
"configDir": "libs/ui/.storybook",
"browserTarget": "ui:build-storybook",
"compodoc": false
},
"configurations": {
"ci": {
@ -294,6 +295,7 @@
"tags": []
},
"ui-e2e": {
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"root": "apps/ui-e2e",
"sourceRoot": "apps/ui-e2e/src",
"projectType": "application",

View File

@ -1,4 +1,4 @@
module.exports = {
export default {
displayName: 'api',
globals: {
@ -13,5 +13,5 @@ module.exports = {
coverageDirectory: '../../coverage/apps/api',
testTimeout: 10000,
testEnvironment: 'node',
preset: '../../jest.preset.ts'
preset: '../../jest.preset.js'
};

View File

@ -316,7 +316,9 @@ export class PortfolioController {
const { holdings } = await this.portfolioService.getDetails(
access.userId,
access.userId
access.userId,
'max',
[{ id: 'EQUITY', type: 'ASSET_CLASS' }]
);
const portfolioPublicDetails: PortfolioPublicDetails = {
@ -325,9 +327,6 @@ export class PortfolioController {
};
const totalValue = Object.values(holdings)
.filter((holding) => {
return holding.assetClass === 'EQUITY';
})
.map((portfolioPosition) => {
return this.exchangeRateDataService.toCurrency(
portfolioPosition.quantity * portfolioPosition.marketPrice,
@ -338,17 +337,17 @@ export class PortfolioController {
.reduce((a, b) => a + b, 0);
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
if (portfolioPosition.assetClass === 'EQUITY') {
portfolioPublicDetails.holdings[symbol] = {
allocationCurrent: portfolioPosition.allocationCurrent,
countries: hasDetails ? portfolioPosition.countries : [],
currency: portfolioPosition.currency,
markets: portfolioPosition.markets,
name: portfolioPosition.name,
sectors: hasDetails ? portfolioPosition.sectors : [],
value: portfolioPosition.value / totalValue
};
}
portfolioPublicDetails.holdings[symbol] = {
allocationCurrent: portfolioPosition.value / totalValue,
countries: hasDetails ? portfolioPosition.countries : [],
currency: portfolioPosition.currency,
markets: portfolioPosition.markets,
name: portfolioPosition.name,
netPerformancePercent: portfolioPosition.netPerformancePercent,
sectors: hasDetails ? portfolioPosition.sectors : [],
symbol: portfolioPosition.symbol,
value: portfolioPosition.value / totalValue
};
}
return portfolioPublicDetails;

View File

@ -273,7 +273,6 @@ export class PortfolioService {
.filter((timelineItem) => timelineItem !== null)
.map((timelineItem) => ({
date: timelineItem.date,
marketPrice: timelineItem.value,
value: timelineItem.netPerformance.toNumber()
}));
@ -394,7 +393,7 @@ export class PortfolioService {
continue;
}
const value = item.quantity.mul(item.marketPrice);
const value = item.quantity.mul(item.marketPrice ?? 0);
const symbolProfile = symbolProfileMap[item.symbol];
const dataProviderResponse = dataProviderResponses[item.symbol];
@ -658,7 +657,7 @@ export class PortfolioService {
netPerformancePercent: position.netPerformancePercentage?.toNumber(),
quantity: quantity.toNumber(),
value: this.exchangeRateDataService.toCurrency(
quantity.mul(marketPrice).toNumber(),
quantity.mul(marketPrice ?? 0).toNumber(),
currency,
userCurrency
)

View File

@ -20,10 +20,11 @@ async function bootstrap() {
})
);
const host = process.env.HOST || '0.0.0.0';
const port = process.env.PORT || 3333;
await app.listen(port, () => {
await app.listen(port, host, () => {
logLogo();
Logger.log(`Listening at http://localhost:${port}`);
Logger.log(`Listening at http://${host}:${port}`);
Logger.log('');
});
}

View File

@ -15,7 +15,7 @@ export class ConfigurationService {
BASE_CURRENCY: str({ default: 'USD' }),
CACHE_TTL: num({ default: 1 }),
DATA_SOURCE_PRIMARY: str({ default: DataSource.YAHOO }),
DATA_SOURCES: json({ default: JSON.stringify([DataSource.YAHOO]) }),
DATA_SOURCES: json({ default: [DataSource.YAHOO] }),
ENABLE_FEATURE_BLOG: bool({ default: false }),
ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }),
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),
@ -31,12 +31,13 @@ export class ConfigurationService {
GOOGLE_SHEETS_ACCOUNT: str({ default: '' }),
GOOGLE_SHEETS_ID: str({ default: '' }),
GOOGLE_SHEETS_PRIVATE_KEY: str({ default: '' }),
HOST: host({ default: '0.0.0.0' }),
JWT_SECRET_KEY: str({}),
MAX_ITEM_IN_CACHE: num({ default: 9999 }),
MAX_ORDERS_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
PORT: port({ default: 3333 }),
RAKUTEN_RAPID_API_KEY: str({ default: '' }),
REDIS_HOST: str({ default: 'localhost' }),
REDIS_HOST: host({ default: 'localhost' }),
REDIS_PASSWORD: str({ default: '' }),
REDIS_PORT: port({ default: 6379 }),
ROOT_URL: str({ default: 'http://localhost:4200' }),

View File

@ -181,6 +181,9 @@ export class YahooFinanceService implements DataProviderInterface {
if (symbol === 'USDGBp') {
// Convert GPB to GBp (pence)
marketPrice = new Big(marketPrice).mul(100).toNumber();
} else if (symbol === 'USDILA') {
// Convert ILS to ILA
marketPrice = new Big(marketPrice).mul(100).toNumber();
}
response[symbol][format(historicalItem.date, DATE_FORMAT)] = {
@ -243,6 +246,18 @@ export class YahooFinanceService implements DataProviderInterface {
.mul(100)
.toNumber()
};
} else if (
symbol === 'USDILS' &&
yahooFinanceSymbols.includes('USDILA=X')
) {
// Convert ILS to ILA
response['USDILA'] = {
...response[symbol],
currency: 'ILA',
marketPrice: new Big(response[symbol].marketPrice)
.mul(100)
.toNumber()
};
}
}

View File

@ -6,7 +6,7 @@ export interface Environment extends CleanedEnvAccessors {
BASE_CURRENCY: string;
CACHE_TTL: number;
DATA_SOURCE_PRIMARY: string;
DATA_SOURCES: string | string[]; // string is not correct, error in envalid?
DATA_SOURCES: string[];
ENABLE_FEATURE_BLOG: boolean;
ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean;
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean;

View File

@ -1,4 +1,4 @@
module.exports = {
export default {
displayName: 'client',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
@ -18,5 +18,5 @@ module.exports = {
'^.+.(ts|mjs|js|html)$': 'jest-preset-angular'
},
transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'],
preset: '../../jest.preset.ts'
preset: '../../jest.preset.js'
};

View File

@ -16,6 +16,13 @@ const routes: Routes = [
(m) => m.ChangelogPageModule
)
},
{
path: 'about/privacy-policy',
loadChildren: () =>
import('./pages/about/privacy-policy/privacy-policy-page.module').then(
(m) => m.PrivacyPolicyPageModule
)
},
{
path: 'account',
loadChildren: () =>

View File

@ -90,6 +90,10 @@ export class AdminJobsComponent implements OnDestroy, OnInit {
});
}
public onViewData(aData: AdminJobs['jobs'][0]['data']) {
alert(JSON.stringify(aData, null, ' '));
}
public onViewStacktrace(aStacktrace: AdminJobs['jobs'][0]['stacktrace']) {
alert(JSON.stringify(aStacktrace, null, ' '));
}

View File

@ -105,6 +105,9 @@
<ion-icon name="ellipsis-vertical"></ion-icon>
</button>
<mat-menu #accountMenu="matMenu" xPosition="before">
<button i18n mat-menu-item (click)="onViewData(job.data)">
View Data
</button>
<button
i18n
mat-menu-item

View File

@ -66,7 +66,9 @@
>Resources</a
>
<a
*ngIf="hasPermissionForSubscription"
*ngIf="
hasPermissionForSubscription && user?.subscription?.type === 'Basic'
"
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
@ -203,7 +205,9 @@
>Resources</a
>
<a
*ngIf="hasPermissionForSubscription"
*ngIf="
hasPermissionForSubscription && user?.subscription?.type === 'Basic'
"
class="d-block d-sm-none"
i18n
mat-menu-item
@ -229,13 +233,7 @@
mat-button
[routerLink]="['/']"
>
<gf-logo
[hideName]="
!currentRoute ||
currentRoute === 'register' ||
currentRoute === 'start'
"
></gf-logo>
<gf-logo [hideName]="currentRoute === 'register'"></gf-logo>
</a>
<span class="spacer"></span>
<a

View File

@ -4,7 +4,9 @@
<div class="row w-100">
<div class="chart-container col">
<gf-line-chart
class="position-absolute"
symbol="Performance"
[currency]="user?.settings?.baseCurrency"
[historicalDataItems]="historicalDataItems"
[locale]="user?.settings?.locale"
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"

View File

@ -25,10 +25,8 @@
gf-line-chart {
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: -1;
}
}
}

View File

@ -81,9 +81,11 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
}
public onShowErrors() {
const errorMessageParts = this.errors.map((error) => {
return `${error.symbol} (${error.dataSource})`;
});
const errorMessageParts = ['Data Provider Errors for'];
for (const error of this.errors) {
errorMessageParts.push(`${error.symbol} (${error.dataSource})`);
}
alert(errorMessageParts.join('\n'));
}

View File

@ -11,7 +11,7 @@
Symbol
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
{{ element.symbol | gfSymbol }}
<span [title]="element.name">{{ element.symbol | gfSymbol }}</span>
</td>
</ng-container>
@ -104,10 +104,13 @@
*matRowDef="let row; columns: displayedColumns"
mat-row
[ngClass]="{
'cursor-pointer': !ignoreAssetSubClasses.includes(row.assetSubClass)
'cursor-pointer':
hasPermissionToShowValues &&
!ignoreAssetSubClasses.includes(row.assetSubClass)
}"
(click)="
!ignoreAssetSubClasses.includes(row.assetSubClass) &&
hasPermissionToShowValues &&
!ignoreAssetSubClasses.includes(row.assetSubClass) &&
onOpenPositionDialog({ dataSource: row.dataSource, symbol: row.symbol })
"
></tr>

View File

@ -27,6 +27,7 @@ import { Subject, Subscription } from 'rxjs';
export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
@Input() baseCurrency: string;
@Input() deviceType: string;
@Input() hasPermissionToShowValues = true;
@Input() locale: string;
@Input() positions: PortfolioPosition[];
@ -54,13 +55,19 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
public ngOnInit() {}
public ngOnChanges() {
this.displayedColumns = [
'symbol',
'value',
'performance',
'allocationInvestment',
'allocationCurrent'
];
this.displayedColumns = ['symbol'];
if (this.hasPermissionToShowValues) {
this.displayedColumns.push('value');
}
this.displayedColumns.push('performance');
if (this.hasPermissionToShowValues) {
this.displayedColumns.push('allocationInvestment');
}
this.displayedColumns.push('allocationCurrent');
this.isLoading = true;

View File

@ -23,7 +23,7 @@ export class ToggleComponent implements OnChanges, OnInit {
@Output() change = new EventEmitter<Pick<ToggleOption, 'value'>>();
public option = new FormControl();
public option = new FormControl<string>(undefined);
public constructor() {}

View File

@ -17,6 +17,7 @@ export class AuthGuard implements CanActivate {
private static PUBLIC_PAGE_ROUTES = [
'/about',
'/about/changelog',
'/about/privacy-policy',
'/blog',
'/de/blog',
'/en/blog',

View File

@ -4,11 +4,11 @@
<h3 class="d-flex justify-content-center mb-3" i18n>About Ghostfolio</h3>
<div class="about-container">
<p>
<strong>Ghostfolio</strong> is a lightweight wealth management
application for individuals to keep track of stocks, ETFs or
cryptocurrencies and make solid, data-driven investment decisions. The
source code is fully available as open source software (OSS). The
project has been initiated by
Ghostfolio is a lightweight wealth management application for
individuals to keep track of stocks, ETFs or cryptocurrencies and make
solid, data-driven investment decisions. The source code is fully
available as open source software (OSS). The project has been
initiated by
<a href="https://dotsilver.ch" title="Website of Thomas Kaul"
>Thomas Kaul</a
>
@ -174,8 +174,8 @@
<div class="row">
<div
class="col-md-6 col-xs-12 my-2"
[ngClass]="{ 'offset-md-3': !hasPermissionForBlog }"
class="col-md-4 col-xs-12 my-2"
[ngClass]="{ 'offset-md-4': !hasPermissionForBlog }"
>
<a
class="py-2 w-100"
@ -186,7 +186,17 @@
>Changelog & License</a
>
</div>
<div *ngIf="hasPermissionForBlog" class="col-md-6 col-xs-12 my-2">
<div *ngIf="hasPermissionForSubscription" class="col-md-4 col-xs-12 my-2">
<a
class="py-2 w-100"
color="primary"
i18n
mat-stroked-button
[routerLink]="['/about', 'privacy-policy']"
>Privacy Policy</a
>
</div>
<div *ngIf="hasPermissionForBlog" class="col-md-4 col-xs-12 my-2">
<a
class="py-2 w-100"
color="primary"

View File

@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { PrivacyPolicyPageComponent } from './privacy-policy-page.component';
const routes: Routes = [
{ path: '', component: PrivacyPolicyPageComponent, canActivate: [AuthGuard] }
];
@NgModule({
exports: [RouterModule],
imports: [RouterModule.forChild(routes)]
})
export class PrivacyPolicyPageRoutingModule {}

View File

@ -0,0 +1,22 @@
import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
@Component({
host: { class: 'page' },
selector: 'gf-privacy-policy-page',
styleUrls: ['./privacy-policy-page.scss'],
templateUrl: './privacy-policy-page.html'
})
export class PrivacyPolicyPageComponent implements OnDestroy {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor() {}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

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

View File

@ -0,0 +1,17 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MarkdownModule } from 'ngx-markdown';
import { PrivacyPolicyPageRoutingModule } from './privacy-policy-page-routing.module';
import { PrivacyPolicyPageComponent } from './privacy-policy-page.component';
@NgModule({
declarations: [PrivacyPolicyPageComponent],
imports: [
CommonModule,
MarkdownModule.forChild(),
PrivacyPolicyPageRoutingModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class PrivacyPolicyPageModule {}

View File

@ -0,0 +1,21 @@
:host {
color: rgb(var(--dark-primary-text));
display: block;
::ng-deep {
markdown {
a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
&:hover {
color: rgba(var(--palette-primary-300), 1);
}
}
}
}
}
:host-context(.is-dark-theme) {
color: rgb(var(--light-primary-text));
}

View File

@ -16,7 +16,9 @@
<div class="pr-1 w-50" i18n>Membership</div>
<div class="pl-1 w-50">
<div class="align-items-center d-flex mb-1">
{{ user?.subscription?.type }}
<a [routerLink]="['/pricing']"
>{{ user?.subscription?.type }}</a
>
<ion-icon
*ngIf="user?.subscription?.type === 'Premium'"
class="ml-1 text-muted"

View File

@ -2,15 +2,6 @@
color: rgb(var(--dark-primary-text));
display: block;
a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
&:hover {
color: rgba(var(--palette-primary-300), 1);
}
}
gf-access-table {
overflow-x: auto;

View File

@ -29,7 +29,7 @@
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Balance</mat-label>
<mat-label i18n>Cash Balance</mat-label>
<input
matInput
name="balance"

View File

@ -1,19 +1,28 @@
<div class="intro-container mb-5">
<div class="intro-inner-container mx-auto">
<div class="h-100 intro w-100"></div>
<div class="container">
<div class="row">
<div class="col text-center">
<h1 class="font-weight-bold intro my-5" i18n>
Manage your wealth like a boss
</h1>
<div>
<a
href="https://www.youtube.com/watch?v=yY6ObSQVJZk"
target="_blank"
title="Watch the Ghostfol.io Trailer on YouTube"
>
<img
alt="Ghostfol.io Trailer"
class="rounded video"
src="./assets/images/video-preview.jpg"
style="max-width: 100%; width: 40rem"
/>
</a>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div
class="align-items-center d-flex flex-column justify-content-center w-100"
>
<gf-logo size="large"></gf-logo>
<p class="lead m-0">Wealth Management Software</p>
</div>
</div>
<div class="button-container row">
<div class="align-items-center col d-flex justify-content-center">
<div class="py-5 text-center">
@ -43,25 +52,12 @@
<div class="row my-5">
<div class="col text-center">
<h2 class="h4 mb-1 text-center">
Protect your <strong>wealth</strong>. Refine your
Protect your <strong>assets</strong>. Refine your
<strong>personal investment strategy</strong>.
</h2>
<p class="lead">
<strong>Ghostfolio</strong> empowers busy people to keep track of
stocks, ETFs or cryptocurrencies and make solid, data-driven investment
decisions.
</p>
<p>
<a
href="https://www.youtube.com/watch?v=yY6ObSQVJZk"
title="Watch the Ghostfol.io Trailer on YouTube"
>
<img
alt="Ghostfol.io Trailer"
src="./assets/images/video-preview.jpg"
style="max-width: 100%; width: 40rem"
/>
</a>
Ghostfolio empowers busy people to keep track of stocks, ETFs or
cryptocurrencies and make solid, data-driven investment decisions.
</p>
</div>
</div>
@ -198,3 +194,19 @@
</a>
</div>
</div>
<div class="container">
<div class="d-block row">
<div class="outro-inner-container mx-auto">
<div class="h-100 w-100"></div>
</div>
</div>
<div class="row">
<div
class="align-items-center d-flex flex-column justify-content-center w-100"
>
<gf-logo size="medium"></gf-logo>
<div>Wealth Management Software</div>
</div>
</div>
</div>

View File

@ -1,3 +1,5 @@
@import '~apps/client/src/styles/ghostfolio-style';
:host {
display: block;
@ -13,19 +15,32 @@
}
}
.intro-container {
margin-top: -5rem;
.intro {
font-size: 4vw;
line-height: 1;
.intro-inner-container {
aspect-ratio: 16 / 9;
max-height: 66vh;
@media (max-width: 575.98px) {
font-size: 10vw;
}
}
.intro {
background-image: url('/assets/intro.jpg');
background-position: top left;
background-repeat: no-repeat;
background-size: contain;
}
.outro-inner-container {
aspect-ratio: 16 / 9;
max-height: 66vh;
div {
background-image: url('/assets/intro.jpg');
background-position: top left;
background-repeat: no-repeat;
background-size: contain;
}
}
.video {
border: 1px solid rgba(var(--dark-dividers));
&:hover {
border-color: rgba(var(--palette-primary-500), 1);
}
}
}
@ -37,9 +52,13 @@
}
}
.intro-container {
.intro {
.outro-inner-container {
div {
background-image: url('/assets/intro-dark.jpg') !important;
}
}
.video {
border-color: rgba(var(--light-dividers));
}
}

View File

@ -6,15 +6,14 @@
</h3>
<div class="mb-4">
<p>
Our official
<strong>Ghostfolio Premium</strong> cloud offering is the easiest way
to get started. Due to the time it saves, this will be the best option
Our official Ghostfolio Premium cloud offering is the easiest way to
get started. Due to the time it saves, this will be the best option
for most people. The revenue is used for covering the hosting costs.
</p>
<p>
If you prefer to run <strong>Ghostfolio</strong> on your own
infrastructure, please find the source code and further instructions
on <a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
If you prefer to run Ghostfolio on your own infrastructure, please
find the source code and further instructions on
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
</p>
</div>
<div class="row">

View File

@ -34,6 +34,10 @@ export class PublicPageComponent implements OnInit {
public positions: {
[symbol: string]: Pick<PortfolioPosition, 'currency' | 'name' | 'value'>;
};
public positionsArray: Pick<
PortfolioPosition,
'currency' | 'name' | 'netPerformancePercent' | 'symbol' | 'value'
>[];
public sectors: {
[name: string]: { name: string; value: number };
};
@ -115,6 +119,7 @@ export class PublicPageComponent implements OnInit {
}
};
this.positions = {};
this.positionsArray = [];
this.sectors = {
[UNKNOWN_KEY]: {
name: UNKNOWN_KEY,
@ -139,6 +144,7 @@ export class PublicPageComponent implements OnInit {
currency: position.currency,
name: position.name
};
this.positionsArray.push(position);
if (position.countries.length > 0) {
this.markets.developedMarkets.value +=

View File

@ -109,6 +109,15 @@
</mat-card>
</div>
</div>
<div class="row">
<div class="col-lg">
<gf-positions-table
[deviceType]="deviceType"
[hasPermissionToShowValues]="false"
[positions]="positionsArray"
></gf-positions-table>
</div>
</div>
<div class="row my-5">
<div class="col-md-10 offset-md-1">
<h2 class="h4 mb-1 text-center">

View File

@ -2,6 +2,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 { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module';
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
import { GfValueModule } from '@ghostfolio/ui/value';
@ -15,6 +16,7 @@ import { PublicPageComponent } from './public-page.component';
imports: [
CommonModule,
GfPortfolioProportionChartModule,
GfPositionsTableModule,
GfValueModule,
GfWorldMapChartModule,
MatButtonModule,

View File

@ -32,7 +32,7 @@ import {
PortfolioSummary,
User
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
import { DataSource, Order as OrderModel } from '@prisma/client';
import { parseISO } from 'date-fns';
@ -115,12 +115,14 @@ export class DataService {
public fetchInfo(): InfoItem {
const info = cloneDeep((window as any).info);
const utmSource = <'ios' | 'trusted-web-activity'>(
window.localStorage.getItem('utm_source')
);
if (window.localStorage.getItem('utm_source') === 'trusted-web-activity') {
info.globalPermissions = info.globalPermissions.filter(
(permission) => permission !== permissions.enableSubscription
);
}
info.globalPermissions = filterGlobalPermissions(
info.globalPermissions,
utmSource
);
return info;
}

View File

@ -0,0 +1,80 @@
Last updated: June 18, 2022
This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You.
We use Your Personal data to provide and improve the Service. By using the Service, You agree to the collection and use of information in accordance with this Privacy Policy.
## Interpretation and Definitions
### Interpretation
The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural.
### Definitions
For the purposes of this Privacy Policy:
- **Account** means a unique account created for You to access our Service or parts of our Service.
- **Application** means the software program provided by the Company downloaded by You on any electronic device, named Ghostfolio App.
- **Company** (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to Ghostfolio.
- **Country** refers to: Switzerland
- **Device** means any device that can access the Service such as a computer, a cellphone or a digital tablet.
- **Personal Data** is any information that relates to an identified or identifiable individual.
- **Service** refers to the Application.
- **Service Provider** means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used.
- **Usage Data** refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit).
- **You** means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.
## Collecting and Using Your Personal Data
### Types of Data Collected
#### Personal Data
While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to identify You. Personally identifiable information may include, but is not limited to:
- Usage Data
- User Id
#### Usage Data
Usage Data is collected automatically when using the Service.
Usage Data may include information such as the time and date of Your visit, the unique user identifier and other diagnostic data.
When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the unique user identifier and other diagnostic data.
### Use of Your Personal Data
The Company may use Personal Data for the following purposes:
- **To provide and maintain our Service**, including to monitor the usage of our Service.
- **For other purposes**: We may use Your information for other purposes, such as data analysis, identifying usage trends, determining the effectiveness of our promotional campaigns and to evaluate and improve our Service, products, services, marketing and your experience.
### Retention of Your Personal Data
The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies.
The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods.
### Disclosure of Your Personal Data
#### Security of Your Personal Data
The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We strive to store no personal data at all to protect Your Personal Data, We cannot guarantee its absolute security.
## Links to Other Websites
Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit.
We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.
## Changes to this Privacy Policy
We may update Our Privacy Policy from time to time.
You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.
## Contact Us
If you have any questions about this Privacy Policy, You can contact us [here](https://ghostfol.io/about).

View File

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

View File

@ -3,7 +3,7 @@ import { LOCALE_ID } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { locale } from '@ghostfolio/common/config';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
@ -11,12 +11,14 @@ import { environment } from './environments/environment';
(async () => {
const response = await fetch('/api/v1/info');
const info: InfoItem = await response.json();
const utmSource = <'ios' | 'trusted-web-activity'>(
window.localStorage.getItem('utm_source')
);
if (window.localStorage.getItem('utm_source') === 'trusted-web-activity') {
info.globalPermissions = info.globalPermissions.filter(
(permission) => permission !== permissions.enableSubscription
);
}
info.globalPermissions = filterGlobalPermissions(
info.globalPermissions,
utmSource
);
(window as any).info = info;

View File

@ -16,5 +16,8 @@
"angularCompilerOptions": {
"strictInjectionParameters": true,
"strictTemplates": false
},
"compilerOptions": {
"target": "es2020"
}
}

View File

@ -1,3 +1,3 @@
const { getJestProjects } = require('@nrwl/jest');
module.exports = { projects: getJestProjects() };
export default { projects: getJestProjects() };

3
jest.preset.js Normal file
View File

@ -0,0 +1,3 @@
const nxPreset = require('@nrwl/jest/preset').default;
module.exports = { ...nxPreset };

View File

@ -1,3 +0,0 @@
const nxPreset = require('@nrwl/jest/preset');
module.exports = { ...nxPreset };

View File

@ -1,4 +1,4 @@
module.exports = {
export default {
displayName: 'common',
globals: {
@ -9,5 +9,5 @@ module.exports = {
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/common',
preset: '../../jest.preset.ts'
preset: '../../jest.preset.js'
};

View File

@ -52,7 +52,7 @@ export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
export const GATHER_ASSET_PROFILE_PROCESS = 'GATHER_ASSET_PROFILE';
export const GATHER_ASSET_PROFILE_PROCESS_OPTIONS: JobOptions = {
attempts: 20,
attempts: 10,
backoff: {
delay: ms('1 minute'),
type: 'exponential'
@ -65,7 +65,7 @@ export const GATHER_ASSET_PROFILE_PROCESS_OPTIONS: JobOptions = {
export const GATHER_HISTORICAL_MARKET_DATA_PROCESS =
'GATHER_HISTORICAL_MARKET_DATA';
export const GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS: JobOptions = {
attempts: 20,
attempts: 10,
backoff: {
delay: ms('1 minute'),
type: 'exponential'

View File

@ -10,7 +10,9 @@ export interface PortfolioPublicDetails {
| 'currency'
| 'markets'
| 'name'
| 'netPerformancePercent'
| 'sectors'
| 'symbol'
| 'value'
>;
};

View File

@ -73,6 +73,28 @@ export function getPermissions(aRole: Role): string[] {
}
}
export function filterGlobalPermissions(
aGlobalPermissions: string[],
aUtmSource: 'ios' | 'trusted-web-activity'
) {
const globalPermissions = aGlobalPermissions;
if (aUtmSource === 'ios') {
return globalPermissions.filter((permission) => {
return (
permission !== permissions.enableSocialLogin &&
permission !== permissions.enableSubscription
);
});
} else if (aUtmSource === 'trusted-web-activity') {
return globalPermissions.filter((permission) => {
return permission !== permissions.enableSubscription;
});
}
return globalPermissions;
}
export function hasPermission(
aPermissions: string[] = [],
aPermission: string

View File

@ -1,4 +1,4 @@
module.exports = {
export default {
displayName: 'ui',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
@ -18,5 +18,5 @@ module.exports = {
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment'
],
preset: '../../jest.preset.ts'
preset: '../../jest.preset.js'
};

View File

@ -41,7 +41,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
public filterGroups$: Subject<FilterGroup[]> = new BehaviorSubject([]);
public filters$: Subject<Filter[]> = new BehaviorSubject([]);
public filters: Observable<Filter[]> = this.filters$.asObservable();
public searchControl = new FormControl();
public searchControl = new FormControl<Filter | string>(undefined);
public selectedFilters: Filter[] = [];
public separatorKeysCodes: number[] = [ENTER, COMMA];
@ -50,7 +50,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
public constructor() {
this.searchControl.valueChanges
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((filterOrSearchTerm: Filter | string) => {
.subscribe((filterOrSearchTerm) => {
if (filterOrSearchTerm) {
const searchTerm =
typeof filterOrSearchTerm === 'string'
@ -80,7 +80,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
input.value = '';
}
this.searchControl.setValue(null);
this.searchControl.setValue(undefined);
}
public onRemoveFilter(aFilter: Filter): void {
@ -99,7 +99,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
);
this.updateFilters();
this.searchInput.nativeElement.value = '';
this.searchControl.setValue(null);
this.searchControl.setValue(undefined);
}
public ngOnDestroy() {

View File

@ -8,7 +8,6 @@ import {
Output,
ViewChild
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
@ -62,7 +61,6 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
public isUUID = isUUID;
public placeholder = '';
public routeQueryParams: Subscription;
public searchControl = new FormControl();
public searchKeywords: string[] = [];
public totalFees: number;
public totalValue: number;

View File

@ -4,7 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { baseCurrency, locale } from '@ghostfolio/common/config';
import { locale } from '@ghostfolio/common/config';
import { Meta, Story, moduleMetadata } from '@storybook/angular';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -42,7 +42,7 @@ const Template: Story<FireCalculatorComponent> = (
export const Simple = Template.bind({});
Simple.args = {
currency: baseCurrency,
currency: 'USD',
fireWealth: 0,
locale: locale
};

View File

@ -51,10 +51,10 @@ export class FireCalculatorComponent
@ViewChild('chartCanvas') chartCanvas;
public calculatorForm = this.formBuilder.group({
annualInterestRate: new FormControl(),
paymentPerPeriod: new FormControl(),
principalInvestmentAmount: new FormControl(),
time: new FormControl()
annualInterestRate: new FormControl<number>(undefined),
paymentPerPeriod: new FormControl<number>(undefined),
principalInvestmentAmount: new FormControl<number>(undefined),
time: new FormControl<number>(undefined)
});
public chart: Chart;
public isLoading = true;
@ -261,15 +261,13 @@ export class FireCalculatorComponent
this.calculatorForm.get('principalInvestmentAmount').value || 0;
// Payment per period
const PMT: number = parseFloat(
this.calculatorForm.get('paymentPerPeriod').value
);
const PMT = this.calculatorForm.get('paymentPerPeriod').value;
// Annual interest rate
const r: number = this.calculatorForm.get('annualInterestRate').value / 100;
// Time
const t: number = parseFloat(this.calculatorForm.get('time').value);
const t = this.calculatorForm.get('time').value;
for (let year = currentYear; year < currentYear + t; year++) {
labels.push(year);

View File

@ -17,7 +17,8 @@
"forceConsistentCasingInFileNames": true,
"strict": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"target": "es2020"
},
"angularCompilerOptions": {
"strictInjectionParameters": true,

17
nx.json
View File

@ -29,17 +29,6 @@
}
}
},
"targetDependencies": {
"build": [
{
"target": "build",
"projects": "dependencies"
}
]
},
"cli": {
"defaultCollection": "@nrwl/nest"
},
"defaultProject": "api",
"generators": {
"@nrwl/angular:application": {
@ -53,5 +42,11 @@
},
"@nrwl/nest": {},
"@nrwl/angular:component": {}
},
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"targetDefaults": {
"build": {
"dependsOn": ["^build"]
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "1.157.0",
"version": "1.164.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"scripts": {
@ -34,7 +34,7 @@
"lint": "nx workspace-lint && ng lint",
"ng": "nx",
"nx": "nx",
"postinstall": "prisma generate && ngcc --properties es2015 browser module main",
"postinstall": "prisma generate && ngcc --properties es2020 browser module main",
"replace-placeholders-in-build": "node ./replace.build.js",
"start": "node dist/apps/api/main",
"start:client": "ng serve client --hmr -o",
@ -50,16 +50,16 @@
"workspace-generator": "nx workspace-generator"
},
"dependencies": {
"@angular/animations": "13.3.6",
"@angular/cdk": "13.3.6",
"@angular/common": "13.3.6",
"@angular/compiler": "13.3.6",
"@angular/core": "13.3.6",
"@angular/forms": "13.3.6",
"@angular/material": "13.3.6",
"@angular/platform-browser": "13.3.6",
"@angular/platform-browser-dynamic": "13.3.6",
"@angular/router": "13.3.6",
"@angular/animations": "14.0.2",
"@angular/cdk": "14.0.1",
"@angular/common": "14.0.2",
"@angular/compiler": "14.0.2",
"@angular/core": "14.0.2",
"@angular/forms": "14.0.2",
"@angular/material": "14.0.1",
"@angular/platform-browser": "14.0.2",
"@angular/platform-browser-dynamic": "14.0.2",
"@angular/router": "14.0.2",
"@codewithdan/observable-store": "2.2.11",
"@dinero.js/currencies": "2.0.0-alpha.8",
"@nestjs/bull": "0.5.5",
@ -71,7 +71,7 @@
"@nestjs/platform-express": "8.2.3",
"@nestjs/schedule": "1.0.2",
"@nestjs/serve-static": "2.2.2",
"@nrwl/angular": "14.1.4",
"@nrwl/angular": "14.3.5",
"@prisma/client": "3.14.0",
"@simplewebauthn/browser": "5.2.1",
"@simplewebauthn/server": "5.2.1",
@ -119,31 +119,31 @@
"zone.js": "0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "13.3.5",
"@angular-eslint/eslint-plugin": "13.0.1",
"@angular-eslint/eslint-plugin-template": "13.0.1",
"@angular-eslint/template-parser": "13.0.1",
"@angular/cli": "13.3.5",
"@angular/compiler-cli": "13.3.6",
"@angular/language-service": "13.3.6",
"@angular/localize": "13.3.6",
"@angular-devkit/build-angular": "14.0.2",
"@angular-eslint/eslint-plugin": "13.2.1",
"@angular-eslint/eslint-plugin-template": "13.2.1",
"@angular-eslint/template-parser": "13.2.1",
"@angular/cli": "~14.0.0",
"@angular/compiler-cli": "14.0.2",
"@angular/language-service": "14.0.2",
"@angular/localize": "14.0.2",
"@nestjs/schematics": "8.0.5",
"@nestjs/testing": "8.2.3",
"@nrwl/cli": "14.1.4",
"@nrwl/cypress": "14.1.4",
"@nrwl/eslint-plugin-nx": "14.1.4",
"@nrwl/jest": "14.1.4",
"@nrwl/nest": "14.1.4",
"@nrwl/node": "14.1.4",
"@nrwl/nx-cloud": "14.0.3",
"@nrwl/storybook": "14.1.4",
"@nrwl/workspace": "14.1.4",
"@nrwl/cli": "14.3.5",
"@nrwl/cypress": "14.3.5",
"@nrwl/eslint-plugin-nx": "14.3.5",
"@nrwl/jest": "14.3.5",
"@nrwl/nest": "14.3.5",
"@nrwl/node": "14.3.5",
"@nrwl/nx-cloud": "14.1.1",
"@nrwl/storybook": "14.3.5",
"@nrwl/workspace": "14.3.5",
"@simplewebauthn/typescript-types": "5.2.1",
"@storybook/addon-essentials": "6.4.22",
"@storybook/angular": "6.4.22",
"@storybook/builder-webpack5": "6.4.22",
"@storybook/core-server": "6.4.22",
"@storybook/manager-webpack5": "6.4.22",
"@storybook/addon-essentials": "6.5.9",
"@storybook/angular": "6.5.9",
"@storybook/builder-webpack5": "6.5.9",
"@storybook/core-server": "6.5.9",
"@storybook/manager-webpack5": "6.5.9",
"@types/big.js": "6.1.2",
"@types/bull": "3.15.8",
"@types/cache-manager": "3.4.2",
@ -166,15 +166,15 @@
"import-sort-parser-typescript": "6.0.0",
"import-sort-style-module": "6.0.0",
"jest": "27.5.1",
"jest-preset-angular": "11.1.1",
"nx": "14.1.4",
"prettier": "2.5.1",
"jest-preset-angular": "11.1.2",
"nx": "14.3.5",
"prettier": "2.7.1",
"replace-in-file": "6.2.0",
"rimraf": "3.0.2",
"tslib": "2.0.0",
"ts-jest": "27.1.4",
"ts-node": "9.1.1",
"typescript": "4.6.4"
"ts-node": "10.8.1",
"typescript": "4.7.3"
},
"engines": {
"node": ">=14"

9937
yarn.lock

File diff suppressed because it is too large Load Diff