Compare commits

..

36 Commits

Author SHA1 Message Date
7c9407d5dc Release 2.20.0 (#2623) 2023-11-08 19:39:38 +01:00
8abb517ac6 Feature/remove loading indicator of unit in overview of home page (#2622)
* Remove loading indicator of unit

* Update changelog
2023-11-08 19:37:45 +01:00
dec1d89c5c Feature/increase timeout in data provider and enhancer health check endpoint (#2621)
* Increase timeout in health check endpoint (data enhancer and provider)

* Update changelog
2023-11-08 18:02:38 +01:00
24e9ecc3e2 Update locales (#2620) 2023-11-08 16:51:44 +01:00
4a1e05b8cd Improve historical market data import (#2581)
* Add form group for historical data import

* Update changelog
2023-11-07 18:16:00 +01:00
39d1a85267 Feature/extend personal finance tools pages 20231107 (#2618)
* Add Monarch Money

* Add YNAB

* Add Allvue Systems
2023-11-07 18:14:54 +01:00
7cb86de7af Feature/remove account type from account database model (#2616)
* Remove account type

* Update changelog
2023-11-07 17:56:18 +01:00
aa078588e8 Release 2.19.0 (#2615) 2023-11-06 18:02:15 +01:00
fcef0a72d5 Bugfix/improve handling of derived currencies (#2604)
* Improve handling of derived currencies

* Update changelog
2023-11-06 17:58:39 +01:00
29987d3e2f Add missing activity types (#2601)
* FEE
* INTEREST
* LIABILITY
2023-11-06 13:09:14 +01:00
6284b4dfe8 Feature/improve localization of fear and greed index (#2612)
* Improve localization

* Update changelog
2023-11-06 13:07:53 +01:00
00342ca1f7 Improve wording (#2603) 2023-11-06 12:15:42 +01:00
234c4fd511 Add CoinGecko (#2611) 2023-11-06 08:33:23 +01:00
669f1fb60c Feature/add database migration to reset account type in account table (#2602)
* Set accountType to NULL

* Update changelog
2023-11-05 18:31:16 +01:00
52df0c62ab Release 2.18.0 (#2600) 2023-11-05 11:53:38 +01:00
e8e1bb83bf Fix get quotes in CoinGecko service (#2595)
* Fix get quotes in CoinGecko service

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-11-05 11:52:09 +01:00
45510702d0 Feature/add hacktoberfest 2023 debriefing blog post (#2599)
* Add blog post: Hacktoberfest 2023 Debriefing

* Update changelog
2023-11-05 11:36:52 +01:00
1b7e3a1e47 Feature/support activities import by isin for yahoo finance (#2597)
* Add support to import activities by isin

* Update changelog
2023-11-05 09:58:57 +01:00
35f98b9d2d Bugfix/handle failing database query for account find many (#2598)
* Handle issue with account.findMany() -> where: { id: { in: [ null ] } }

* Update changelog
2023-11-05 09:57:23 +01:00
e980aed9e7 Reorder functions (#2594) 2023-11-05 08:50:43 +01:00
d993067e9a Feature/extend personal finance tools pages 20231104 2 (#2591)
* Add Vyzer

* Add FinWise
2023-11-05 08:50:27 +01:00
3d09bfdb0c Feature/upgrade angular to version 16.2.12 (#2590)
* Add upgrade guides for Angular

* Upgrade Angular dependencies to version 16.2.12

* Update changelog
2023-11-04 11:59:49 +01:00
3fbc4f500f Add empty columns (#2589) 2023-11-04 10:39:36 +01:00
373201a98f Add major version to docker tags (#2586)
* Add major version to docker tags

* Update changelog
2023-11-04 10:38:35 +01:00
681f88f002 Clean up import (#2492) 2023-11-04 10:19:15 +01:00
8a523a981a Bugfix/fix fees on account level (#2588)
* Fix fees on account level

* Update changelog
2023-11-04 10:17:58 +01:00
81ded53363 Center membership card (#2582) 2023-11-04 10:17:35 +01:00
5272407af8 Feature/extend personal finance tools pages 20231104 (#2587)
* Introduce alias

* Add Rocket Money

* Add 8FIGURES
2023-11-04 10:17:17 +01:00
c48f89d117 Add empty columns (#2583) 2023-11-04 10:11:58 +01:00
046fdd3ae7 Release 2.17.0 (#2579) 2023-11-02 19:36:59 +01:00
e69c7a753c Feature/add edit exchange rate button to admin control (#2577)
* Ad edit button

* Update changelog
2023-11-02 19:35:03 +01:00
5191415b5a Add Intuit Mint (#2578) 2023-11-02 19:34:42 +01:00
a704378702 Refactor interface of getQuotes() to object (#2570) 2023-11-01 13:55:48 +01:00
cf7ce64de7 Bugfix/improve alignment of menu item icons (#2566)
* Improve alignment

* Update changelog
2023-10-31 14:01:33 +01:00
8c1b45f35b Bugfix/fix exception in webauthn page (#2564)
* Remove useBrowserAutofill option in startAuthentication()

* Update changelog
2023-10-30 19:23:55 +01:00
6ad1528d01 Feature/improve language localization for german 20231029 (#2565)
* Update locales

* Update changelog
2023-10-29 19:33:42 +01:00
85 changed files with 14741 additions and 1457 deletions

View File

@ -21,6 +21,7 @@ jobs:
with: with:
images: ghostfolio/ghostfolio images: ghostfolio/ghostfolio
tags: | tags: |
type=semver,pattern={{major}}
type=semver,pattern={{version}} type=semver,pattern={{version}}
- name: Set up QEMU - name: Set up QEMU

View File

@ -5,6 +5,64 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.20.0 - 2023-11-08
### Changed
- Removed the loading indicator of the unit on the overview tab of the home page
- Improved the import of historical market data in the admin control panel
- Increased the timeout in the health check endpoint for data enhancers
- Increased the timeout in the health check endpoint for data providers
- Removed the account type from the `Account` database schema
## 2.19.0 - 2023-11-06
### Added
- Added a data migration to set `accountType` to `NULL` in the account database table
### Changed
- Improved the language localization for the _Fear & Greed Index_ (market mood)
- Improved the language localization for German (`de`)
### Fixed
- Improved the handling of derived currencies (`GBp`, `ILA`, `ZAc`)
## 2.18.0 - 2023-11-05
### Added
- Added support to import activities by `isin` in the _Yahoo Finance_ service
- Added a new tag with the major version to the docker image on _Docker Hub_
- Added a blog post: _Hacktoberfest 2023 Debriefing_
### Changed
- Upgraded `angular` from version `16.2.1` to `16.2.12`
### Fixed
- Fixed an issue to get quotes in the _CoinGecko_ service
- Loosened the validation in the activities import (expects values greater than or equal to 0 for `fee`, `quantity` and `unitPrice`)
- Handled an issue with a failing database query (`account.findMany()`) related to activities without account
## 2.17.0 - 2023-11-02
### Added
- Added a button to edit the exchange rates in the admin control panel
### Changed
- Improved the language localization for German (`de`)
### Fixed
- Fixed an issue in the biometric authentication
- Fixed the alignment of the icons in various menus
## 2.16.0 - 2023-10-29 ## 2.16.0 - 2023-10-29
### Changed ### Changed
@ -313,7 +371,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added health check endpoints for data enhancers - Added a health check endpoint for data enhancers
### Changed ### Changed
@ -489,7 +547,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the usability of the login dialog - Improved the usability of the login dialog
- Disabled the caching in the health check endpoints for data providers - Disabled the caching in the health check endpoint for data providers
- Improved the content of the Frequently Asked Questions (FAQ) page - Improved the content of the Frequently Asked Questions (FAQ) page
- Upgraded `prisma` from version `4.15.0` to `4.16.2` - Upgraded `prisma` from version `4.15.0` to `4.16.2`
@ -877,7 +935,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added a fallback to historical market data if a data provider does not provide live data - Added a fallback to historical market data if a data provider does not provide live data
- Added a general health check endpoint - Added a general health check endpoint
- Added health check endpoints for data providers - Added a health check endpoint for data providers
### Changed ### Changed

View File

@ -20,6 +20,12 @@ Use `*ngIf="user?.settings?.isExperimentalFeatures"` in HTML template
## Dependencies ## Dependencies
### Angular
#### Upgrade (minor versions)
1. Run `npx npm-check-updates --upgrade --target "minor" --filter "/@angular.*/"`
### Nx ### Nx
#### Upgrade #### Upgrade

View File

@ -231,16 +231,16 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TO
``` ```
| Field | Type | Description | | Field | Type | Description |
| ---------- | ------------------- | -------------------------------------------------- | | ---------- | ------------------- | ----------------------------------------------------------------------------- |
| accountId | string (`optional`) | Id of the account | | accountId | string (`optional`) | Id of the account |
| comment | string (`optional`) | Comment of the activity | | comment | string (`optional`) | Comment of the activity |
| currency | string | `CHF` \| `EUR` \| `USD` etc. | | currency | string | `CHF` \| `EUR` \| `USD` etc. |
| dataSource | string | `MANUAL` (for type `ITEM`) \| `YAHOO` | | dataSource | string | `COINGECKO` \| `MANUAL` (for type `ITEM`) \| `YAHOO` |
| date | string | Date in the format `ISO-8601` | | date | string | Date in the format `ISO-8601` |
| fee | number | Fee of the activity | | fee | number | Fee of the activity |
| quantity | number | Quantity of the activity | | quantity | number | Quantity of the activity |
| symbol | string | Symbol of the activity (suitable for `dataSource`) | | symbol | string | Symbol of the activity (suitable for `dataSource`) |
| type | string | `BUY` \| `DIVIDEND` \| `ITEM` \| `SELL` | | type | string | `BUY` \| `DIVIDEND` \| `FEE` \| `INTEREST` \| `ITEM` \| `LIABILITY` \| `SELL` |
| unitPrice | number | Price per unit of the activity | | unitPrice | number | Price per unit of the activity |
#### Response #### Response

View File

@ -1,4 +1,3 @@
import { AccountType } from '@prisma/client';
import { Transform, TransformFnParams } from 'class-transformer'; import { Transform, TransformFnParams } from 'class-transformer';
import { import {
IsBoolean, IsBoolean,
@ -10,10 +9,6 @@ import {
import { isString } from 'lodash'; import { isString } from 'lodash';
export class CreateAccountDto { export class CreateAccountDto {
@IsOptional()
@IsString()
accountType?: AccountType;
@IsNumber() @IsNumber()
balance: number; balance: number;

View File

@ -1,4 +1,3 @@
import { AccountType } from '@prisma/client';
import { Transform, TransformFnParams } from 'class-transformer'; import { Transform, TransformFnParams } from 'class-transformer';
import { import {
IsBoolean, IsBoolean,
@ -10,10 +9,6 @@ import {
import { isString } from 'lodash'; import { isString } from 'lodash';
export class UpdateAccountDto { export class UpdateAccountDto {
@IsOptional()
@IsString()
accountType?: AccountType;
@IsNumber() @IsNumber()
balance: number; balance: number;

View File

@ -23,7 +23,13 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { MarketDataPreset } from '@ghostfolio/common/types'; import { MarketDataPreset } from '@ghostfolio/common/types';
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { AssetSubClass, Prisma, Property, SymbolProfile } from '@prisma/client'; import {
AssetSubClass,
DataSource,
Prisma,
Property,
SymbolProfile
} from '@prisma/client';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
@ -94,9 +100,17 @@ export class AdminService {
return currency !== DEFAULT_CURRENCY; return currency !== DEFAULT_CURRENCY;
}) })
.map((currency) => { .map((currency) => {
const label1 = DEFAULT_CURRENCY;
const label2 = currency;
return { return {
label1: DEFAULT_CURRENCY, label1,
label2: currency, label2,
dataSource:
DataSource[
this.configurationService.get('DATA_SOURCE_EXCHANGE_RATES')
],
symbol: `${label1}${label2}`,
value: this.exchangeRateDataService.toCurrency( value: this.exchangeRateDataService.toCurrency(
1, 1,
DEFAULT_CURRENCY, DEFAULT_CURRENCY,

View File

@ -13,7 +13,6 @@ import {
IsISO8601, IsISO8601,
IsNumber, IsNumber,
IsOptional, IsOptional,
IsPositive,
IsString, IsString,
Min Min
} from 'class-validator'; } from 'class-validator';
@ -54,7 +53,7 @@ export class CreateOrderDto {
fee: number; fee: number;
@IsNumber() @IsNumber()
@IsPositive() @Min(0)
quantity: number; quantity: number;
@IsString() @IsString()
@ -68,7 +67,7 @@ export class CreateOrderDto {
type: Type; type: Type;
@IsNumber() @IsNumber()
@IsPositive() @Min(0)
unitPrice: number; unitPrice: number;
@IsBoolean() @IsBoolean()

View File

@ -8,12 +8,10 @@ import {
import { Transform, TransformFnParams } from 'class-transformer'; import { Transform, TransformFnParams } from 'class-transformer';
import { import {
IsArray, IsArray,
IsBoolean,
IsEnum, IsEnum,
IsISO8601, IsISO8601,
IsNumber, IsNumber,
IsOptional, IsOptional,
IsPositive,
IsString, IsString,
Min Min
} from 'class-validator'; } from 'class-validator';
@ -56,7 +54,7 @@ export class UpdateOrderDto {
id: string; id: string;
@IsNumber() @IsNumber()
@IsPositive() @Min(0)
quantity: number; quantity: number;
@IsString() @IsString()
@ -70,6 +68,6 @@ export class UpdateOrderDto {
type: Type; type: Type;
@IsNumber() @IsNumber()
@IsPositive() @Min(0)
unitPrice: number; unitPrice: number;
} }

View File

@ -1892,7 +1892,11 @@ export class PortfolioService {
}); });
} else { } else {
const accountIds = uniq( const accountIds = uniq(
orders.map(({ accountId }) => { orders
.filter(({ accountId }) => {
return accountId;
})
.map(({ accountId }) => {
return accountId; return accountId;
}) })
); );

View File

@ -54,6 +54,14 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-8figures</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-allvue-systems</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-altoo</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -90,6 +98,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-finary</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-finary</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-finwise</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-folishare</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-folishare</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -102,6 +114,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-gospatz</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-gospatz</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-intuit-mint</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-justetf</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-justetf</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -118,6 +134,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-maybe-finance</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-maybe-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monarch-money</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monse</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monse</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -142,6 +162,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-projectionlab</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-projectionlab</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-rocket-money</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-seeking-alpha</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-seeking-alpha</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -174,6 +198,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-utluna</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-utluna</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-vyzer</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthica</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -182,6 +210,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-yeekatee</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-yeekatee</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-ynab</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ueber-uns</loc> <loc>https://ghostfol.io/de/ueber-uns</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -286,6 +318,10 @@
<loc>https://ghostfol.io/en/blog/2023/09/hacktoberfest-2023</loc> <loc>https://ghostfol.io/en/blog/2023/09/hacktoberfest-2023</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/blog/2023/11/hacktoberfest-2023-debriefing</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/faq</loc> <loc>https://ghostfol.io/en/faq</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -320,6 +356,14 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-8figures</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-allvue-systems</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-altoo</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -356,6 +400,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-finary</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-finary</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-finwise</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-folishare</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-folishare</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -368,6 +416,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-gospatz</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-gospatz</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-intuit-mint</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-justetf</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-justetf</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -384,6 +436,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-maybe-finance</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-maybe-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monarch-money</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monse</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monse</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -408,6 +464,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-projectionlab</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-projectionlab</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-rocket-money</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-seeking-alpha</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-seeking-alpha</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -440,6 +500,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-utluna</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-utluna</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-vyzer</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthica</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -448,6 +512,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-yeekatee</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-yeekatee</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-ynab</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/es</loc> <loc>https://ghostfol.io/es</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -614,6 +682,14 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-8figures</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-allvue-systems</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-altoo</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -650,6 +726,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-finary</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-finary</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-finwise</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-folishare</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-folishare</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -662,6 +742,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-gospatz</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-gospatz</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-intuit-mint</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-justetf</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-justetf</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -678,6 +762,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-maybe-finance</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-maybe-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monarch-money</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monse</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monse</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -702,6 +790,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-projectionlab</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-projectionlab</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-rocket-money</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-seeking-alpha</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-seeking-alpha</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -734,6 +826,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-utluna</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-utluna</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-vyzer</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthica</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -742,6 +838,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-yeekatee</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-yeekatee</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-ynab</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl</loc> <loc>https://ghostfol.io/nl</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -754,6 +854,14 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-8figures</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-allvue-systems</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-altoo</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -790,6 +898,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-finary</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-finary</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-finwise</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-folishare</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-folishare</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -802,6 +914,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-gospatz</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-gospatz</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-intuit-mint</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-justetf</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-justetf</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -818,6 +934,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-maybe-finance</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-maybe-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monarch-money</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monse</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monse</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -842,6 +962,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-projectionlab</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-projectionlab</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-rocket-money</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-seeking-alpha</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-seeking-alpha</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -874,6 +998,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-utluna</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-utluna</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-vyzer</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthica</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -882,6 +1010,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-yeekatee</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-yeekatee</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-ynab</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/functionaliteiten</loc> <loc>https://ghostfol.io/nl/functionaliteiten</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>

View File

@ -75,6 +75,10 @@ const locales = {
'/en/blog/2023/09/hacktoberfest-2023': { '/en/blog/2023/09/hacktoberfest-2023': {
featureGraphicPath: 'assets/images/blog/hacktoberfest-2023.png', featureGraphicPath: 'assets/images/blog/hacktoberfest-2023.png',
title: `Hacktoberfest 2023 - ${title}` title: `Hacktoberfest 2023 - ${title}`
},
'/en/blog/2023/11/hacktoberfest-2023-debriefing': {
featureGraphicPath: 'assets/images/blog/hacktoberfest-2023.png',
title: `Hacktoberfest 2023 Debriefing - ${title}`
} }
}; };

View File

@ -5,6 +5,7 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@ -105,9 +106,13 @@ export class AlphaVantageService implements DataProviderInterface {
return DataSource.ALPHA_VANTAGE; return DataSource.ALPHA_VANTAGE;
} }
public async getQuotes( public async getQuotes({
aSymbols: string[] requestTimeout = DEFAULT_REQUEST_TIMEOUT,
): Promise<{ [symbol: string]: IDataProviderResponse }> { symbols
}: {
requestTimeout?: number;
symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
return {}; return {};
} }

View File

@ -134,13 +134,17 @@ export class CoinGeckoService implements DataProviderInterface {
return DataSource.COINGECKO; return DataSource.COINGECKO;
} }
public async getQuotes( public async getQuotes({
aSymbols: string[] requestTimeout = DEFAULT_REQUEST_TIMEOUT,
): Promise<{ [symbol: string]: IDataProviderResponse }> { symbols
const results: { [symbol: string]: IDataProviderResponse } = {}; }: {
requestTimeout?: number;
symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {};
if (aSymbols.length <= 0) { if (symbols.length <= 0) {
return {}; return response;
} }
try { try {
@ -148,10 +152,10 @@ export class CoinGeckoService implements DataProviderInterface {
setTimeout(() => { setTimeout(() => {
abortController.abort(); abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT); }, requestTimeout);
const response = await got( const quotes = await got(
`${this.URL}/simple/price?ids=${aSymbols.join( `${this.URL}/simple/price?ids=${symbols.join(
',' ','
)}&vs_currencies=${DEFAULT_CURRENCY.toLowerCase()}`, )}&vs_currencies=${DEFAULT_CURRENCY.toLowerCase()}`,
{ {
@ -160,22 +164,20 @@ export class CoinGeckoService implements DataProviderInterface {
} }
).json<any>(); ).json<any>();
for (const symbol in response) { for (const symbol in quotes) {
if (Object.prototype.hasOwnProperty.call(response, symbol)) { response[symbol] = {
results[symbol] = {
currency: DEFAULT_CURRENCY, currency: DEFAULT_CURRENCY,
dataProviderInfo: this.getDataProviderInfo(), dataProviderInfo: this.getDataProviderInfo(),
dataSource: DataSource.COINGECKO, dataSource: DataSource.COINGECKO,
marketPrice: response[symbol][DEFAULT_CURRENCY.toLowerCase()], marketPrice: quotes[symbol][DEFAULT_CURRENCY.toLowerCase()],
marketState: 'open' marketState: 'open'
}; };
} }
}
} catch (error) { } catch (error) {
Logger.error(error, 'CoinGeckoService'); Logger.error(error, 'CoinGeckoService');
} }
return results; return response;
} }
public getTestSymbol() { public getTestSymbol() {

View File

@ -2,6 +2,7 @@ import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/in
import { HttpException, Inject, Injectable } from '@nestjs/common'; import { HttpException, Inject, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import ms from 'ms';
@Injectable() @Injectable()
export class DataEnhancerService { export class DataEnhancerService {
@ -24,6 +25,7 @@ export class DataEnhancerService {
try { try {
const assetProfile = await dataEnhancer.enhance({ const assetProfile = await dataEnhancer.enhance({
requestTimeout: ms('30 seconds'),
response: { response: {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF' assetSubClass: 'ETF'

View File

@ -15,9 +15,11 @@ export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
) {} ) {}
public async enhance({ public async enhance({
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
response, response,
symbol symbol
}: { }: {
requestTimeout?: number;
response: Partial<SymbolProfile>; response: Partial<SymbolProfile>;
symbol: string; symbol: string;
}): Promise<Partial<SymbolProfile>> { }): Promise<Partial<SymbolProfile>> {
@ -45,7 +47,7 @@ export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
setTimeout(() => { setTimeout(() => {
abortController.abort(); abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT); }, requestTimeout);
const mappings = await got const mappings = await got
.post(`${OpenFigiDataEnhancerService.baseUrl}/v3/mapping`, { .post(`${OpenFigiDataEnhancerService.baseUrl}/v3/mapping`, {

View File

@ -21,9 +21,11 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
}; };
public async enhance({ public async enhance({
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
response, response,
symbol symbol
}: { }: {
requestTimeout?: number;
response: Partial<SymbolProfile>; response: Partial<SymbolProfile>;
symbol: string; symbol: string;
}): Promise<Partial<SymbolProfile>> { }): Promise<Partial<SymbolProfile>> {
@ -37,7 +39,7 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
setTimeout(() => { setTimeout(() => {
abortController.abort(); abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT); }, requestTimeout);
const profile = await got( const profile = await got(
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`, `${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`,

View File

@ -1,6 +1,10 @@
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { DEFAULT_CURRENCY, UNKNOWN_KEY } from '@ghostfolio/common/config'; import {
DEFAULT_CURRENCY,
DEFAULT_REQUEST_TIMEOUT,
UNKNOWN_KEY
} from '@ghostfolio/common/config';
import { isCurrency } from '@ghostfolio/common/helper'; import { isCurrency } from '@ghostfolio/common/helper';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { import {
@ -10,6 +14,7 @@ import {
Prisma, Prisma,
SymbolProfile SymbolProfile
} from '@prisma/client'; } from '@prisma/client';
import { isISIN } from 'class-validator';
import { countries } from 'countries-list'; import { countries } from 'countries-list';
import yahooFinance from 'yahoo-finance2'; import yahooFinance from 'yahoo-finance2';
import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-iface'; import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-iface';
@ -71,9 +76,11 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
} }
public async enhance({ public async enhance({
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
response, response,
symbol symbol
}: { }: {
requestTimeout?: number;
response: Partial<SymbolProfile>; response: Partial<SymbolProfile>;
symbol: string; symbol: string;
}): Promise<Partial<SymbolProfile>> { }): Promise<Partial<SymbolProfile>> {
@ -156,7 +163,20 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
const response: Partial<SymbolProfile> = {}; const response: Partial<SymbolProfile> = {};
try { try {
const symbol = this.convertToYahooFinanceSymbol(aSymbol); let symbol = aSymbol;
if (isISIN(symbol)) {
try {
const { quotes } = await yahooFinance.search(symbol);
if (quotes.length === 1) {
symbol = quotes[0].symbol;
}
} catch {}
} else {
symbol = this.convertToYahooFinanceSymbol(symbol);
}
const assetProfile = await yahooFinance.quoteSummary(symbol, { const assetProfile = await yahooFinance.quoteSummary(symbol, {
modules: ['price', 'summaryProfile', 'topHoldings'] modules: ['price', 'summaryProfile', 'topHoldings']
}); });
@ -176,7 +196,7 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
shortName: assetProfile.price.shortName, shortName: assetProfile.price.shortName,
symbol: assetProfile.price.symbol symbol: assetProfile.price.symbol
}); });
response.symbol = aSymbol; response.symbol = assetProfile.price.symbol;
if (assetSubClass === AssetSubClass.MUTUALFUND) { if (assetSubClass === AssetSubClass.MUTUALFUND) {
response.sectors = []; response.sectors = [];

View File

@ -17,6 +17,7 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
import { DataSource, MarketData, SymbolProfile } from '@prisma/client'; import { DataSource, MarketData, SymbolProfile } from '@prisma/client';
import { format, isValid } from 'date-fns'; import { format, isValid } from 'date-fns';
import { groupBy, isEmpty, isNumber } from 'lodash'; import { groupBy, isEmpty, isNumber } from 'lodash';
import ms from 'ms';
@Injectable() @Injectable()
export class DataProviderService { export class DataProviderService {
@ -52,6 +53,7 @@ export class DataProviderService {
symbol symbol
} }
], ],
requestTimeout: ms('30 seconds'),
useCache: false useCache: false
}); });
@ -236,9 +238,11 @@ export class DataProviderService {
public async getQuotes({ public async getQuotes({
items, items,
requestTimeout,
useCache = true useCache = true
}: { }: {
items: UniqueAsset[]; items: UniqueAsset[];
requestTimeout?: number;
useCache?: boolean; useCache?: boolean;
}): Promise<{ }): Promise<{
[symbol: string]: IDataProviderResponse; [symbol: string]: IDataProviderResponse;
@ -311,7 +315,9 @@ export class DataProviderService {
i + maximumNumberOfSymbolsPerRequest i + maximumNumberOfSymbolsPerRequest
); );
const promise = Promise.resolve(dataProvider.getQuotes(symbolsChunk)); const promise = Promise.resolve(
dataProvider.getQuotes({ requestTimeout, symbols: symbolsChunk })
);
promises.push( promises.push(
promise.then(async (result) => { promise.then(async (result) => {

View File

@ -131,28 +131,34 @@ export class EodHistoricalDataService implements DataProviderInterface {
return DataSource.EOD_HISTORICAL_DATA; return DataSource.EOD_HISTORICAL_DATA;
} }
public async getQuotes( public async getQuotes({
aSymbols: string[] requestTimeout = DEFAULT_REQUEST_TIMEOUT,
): Promise<{ [symbol: string]: IDataProviderResponse }> { symbols
const symbols = aSymbols.map((symbol) => { }: {
return this.convertToEodSymbol(symbol); requestTimeout?: number;
}); symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
let response: { [symbol: string]: IDataProviderResponse } = {};
if (symbols.length <= 0) { if (symbols.length <= 0) {
return {}; return response;
} }
const eodHistoricalDataSymbols = symbols.map((symbol) => {
return this.convertToEodSymbol(symbol);
});
try { try {
const abortController = new AbortController(); const abortController = new AbortController();
setTimeout(() => { setTimeout(() => {
abortController.abort(); abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT); }, requestTimeout);
const realTimeResponse = await got( const realTimeResponse = await got(
`${this.URL}/real-time/${symbols[0]}?api_token=${ `${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?api_token=${
this.apiKey this.apiKey
}&fmt=json&s=${symbols.join(',')}`, }&fmt=json&s=${eodHistoricalDataSymbols.join(',')}`,
{ {
// @ts-ignore // @ts-ignore
signal: abortController.signal signal: abortController.signal
@ -160,10 +166,12 @@ export class EodHistoricalDataService implements DataProviderInterface {
).json<any>(); ).json<any>();
const quotes = const quotes =
symbols.length === 1 ? [realTimeResponse] : realTimeResponse; eodHistoricalDataSymbols.length === 1
? [realTimeResponse]
: realTimeResponse;
const searchResponse = await Promise.all( const searchResponse = await Promise.all(
symbols eodHistoricalDataSymbols
.filter((symbol) => { .filter((symbol) => {
return !symbol.endsWith('.FOREX'); return !symbol.endsWith('.FOREX');
}) })
@ -176,7 +184,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
return items[0]; return items[0];
}); });
const response = quotes.reduce( response = quotes.reduce(
( (
result: { [symbol: string]: IDataProviderResponse }, result: { [symbol: string]: IDataProviderResponse },
{ close, code, timestamp } { close, code, timestamp }

View File

@ -113,13 +113,17 @@ export class FinancialModelingPrepService implements DataProviderInterface {
return DataSource.FINANCIAL_MODELING_PREP; return DataSource.FINANCIAL_MODELING_PREP;
} }
public async getQuotes( public async getQuotes({
aSymbols: string[] requestTimeout = DEFAULT_REQUEST_TIMEOUT,
): Promise<{ [symbol: string]: IDataProviderResponse }> { symbols
const results: { [symbol: string]: IDataProviderResponse } = {}; }: {
requestTimeout?: number;
symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {};
if (aSymbols.length <= 0) { if (symbols.length <= 0) {
return {}; return response;
} }
try { try {
@ -127,10 +131,10 @@ export class FinancialModelingPrepService implements DataProviderInterface {
setTimeout(() => { setTimeout(() => {
abortController.abort(); abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT); }, requestTimeout);
const response = await got( const response = await got(
`${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`, `${this.URL}/quote/${symbols.join(',')}?apikey=${this.apiKey}`,
{ {
// @ts-ignore // @ts-ignore
signal: abortController.signal signal: abortController.signal
@ -138,7 +142,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
).json<any>(); ).json<any>();
for (const { price, symbol } of response) { for (const { price, symbol } of response) {
results[symbol] = { response[symbol] = {
currency: DEFAULT_CURRENCY, currency: DEFAULT_CURRENCY,
dataProviderInfo: this.getDataProviderInfo(), dataProviderInfo: this.getDataProviderInfo(),
dataSource: DataSource.FINANCIAL_MODELING_PREP, dataSource: DataSource.FINANCIAL_MODELING_PREP,
@ -150,7 +154,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
Logger.error(error, 'FinancialModelingPrepService'); Logger.error(error, 'FinancialModelingPrepService');
} }
return results; return response;
} }
public getTestSymbol() { public getTestSymbol() {

View File

@ -7,6 +7,7 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
@ -99,18 +100,22 @@ export class GoogleSheetsService implements DataProviderInterface {
return DataSource.GOOGLE_SHEETS; return DataSource.GOOGLE_SHEETS;
} }
public async getQuotes( public async getQuotes({
aSymbols: string[] requestTimeout = DEFAULT_REQUEST_TIMEOUT,
): Promise<{ [symbol: string]: IDataProviderResponse }> { symbols
if (aSymbols.length <= 0) { }: {
return {}; requestTimeout?: number;
symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {};
if (symbols.length <= 0) {
return response;
} }
try { try {
const response: { [symbol: string]: IDataProviderResponse } = {};
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
aSymbols.map((symbol) => { symbols.map((symbol) => {
return { return {
symbol, symbol,
dataSource: this.getName() dataSource: this.getName()
@ -129,7 +134,7 @@ export class GoogleSheetsService implements DataProviderInterface {
const marketPrice = parseFloat(row['marketPrice']); const marketPrice = parseFloat(row['marketPrice']);
const symbol = row['symbol']; const symbol = row['symbol'];
if (aSymbols.includes(symbol)) { if (symbols.includes(symbol)) {
response[symbol] = { response[symbol] = {
marketPrice, marketPrice,
currency: symbolProfiles.find((symbolProfile) => { currency: symbolProfiles.find((symbolProfile) => {

View File

@ -2,9 +2,11 @@ import { SymbolProfile } from '@prisma/client';
export interface DataEnhancerInterface { export interface DataEnhancerInterface {
enhance({ enhance({
requestTimeout,
response, response,
symbol symbol
}: { }: {
requestTimeout?: number;
response: Partial<SymbolProfile>; response: Partial<SymbolProfile>;
symbol: string; symbol: string;
}): Promise<Partial<SymbolProfile>>; }): Promise<Partial<SymbolProfile>>;

View File

@ -36,9 +36,13 @@ export interface DataProviderInterface {
getName(): DataSource; getName(): DataSource;
getQuotes( getQuotes({
aSymbols: string[] requestTimeout,
): Promise<{ [symbol: string]: IDataProviderResponse }>; symbols
}: {
requestTimeout?: number;
symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }>;
getTestSymbol(): string; getTestSymbol(): string;

View File

@ -133,18 +133,22 @@ export class ManualService implements DataProviderInterface {
return DataSource.MANUAL; return DataSource.MANUAL;
} }
public async getQuotes( public async getQuotes({
aSymbols: string[] requestTimeout = DEFAULT_REQUEST_TIMEOUT,
): Promise<{ [symbol: string]: IDataProviderResponse }> { symbols
}: {
requestTimeout?: number;
symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: IDataProviderResponse } = {};
if (aSymbols.length <= 0) { if (symbols.length <= 0) {
return response; return response;
} }
try { try {
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
aSymbols.map((symbol) => { symbols.map((symbol) => {
return { symbol, dataSource: this.getName() }; return { symbol, dataSource: this.getName() };
}) })
); );
@ -154,10 +158,10 @@ export class ManualService implements DataProviderInterface {
orderBy: { orderBy: {
date: 'desc' date: 'desc'
}, },
take: aSymbols.length, take: symbols.length,
where: { where: {
symbol: { symbol: {
in: aSymbols in: symbols
} }
} }
}); });

View File

@ -87,15 +87,19 @@ export class RapidApiService implements DataProviderInterface {
return DataSource.RAPID_API; return DataSource.RAPID_API;
} }
public async getQuotes( public async getQuotes({
aSymbols: string[] requestTimeout = DEFAULT_REQUEST_TIMEOUT,
): Promise<{ [symbol: string]: IDataProviderResponse }> { symbols
if (aSymbols.length <= 0) { }: {
requestTimeout?: number;
symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
if (symbols.length <= 0) {
return {}; return {};
} }
try { try {
const symbol = aSymbols[0]; const symbol = symbols[0];
if (symbol === ghostfolioFearAndGreedIndexSymbol) { if (symbol === ghostfolioFearAndGreedIndexSymbol) {
const fgi = await this.getFearAndGreedIndex(); const fgi = await this.getFearAndGreedIndex();

View File

@ -6,7 +6,10 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import {
DEFAULT_CURRENCY,
DEFAULT_REQUEST_TIMEOUT
} from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
@ -30,7 +33,7 @@ export class YahooFinanceService implements DataProviderInterface {
public async getAssetProfile( public async getAssetProfile(
aSymbol: string aSymbol: string
): Promise<Partial<SymbolProfile>> { ): Promise<Partial<SymbolProfile>> {
const { assetClass, assetSubClass, currency, name } = const { assetClass, assetSubClass, currency, name, symbol } =
await this.yahooFinanceDataEnhancerService.getAssetProfile(aSymbol); await this.yahooFinanceDataEnhancerService.getAssetProfile(aSymbol);
return { return {
@ -38,8 +41,8 @@ export class YahooFinanceService implements DataProviderInterface {
assetSubClass, assetSubClass,
currency, currency,
name, name,
dataSource: this.getName(), symbol,
symbol: aSymbol dataSource: this.getName()
}; };
} }
@ -156,20 +159,24 @@ export class YahooFinanceService implements DataProviderInterface {
return DataSource.YAHOO; return DataSource.YAHOO;
} }
public async getQuotes( public async getQuotes({
aSymbols: string[] requestTimeout = DEFAULT_REQUEST_TIMEOUT,
): Promise<{ [symbol: string]: IDataProviderResponse }> { symbols
if (aSymbols.length <= 0) { }: {
return {}; requestTimeout?: number;
symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {};
if (symbols.length <= 0) {
return response;
} }
const yahooFinanceSymbols = aSymbols.map((symbol) => const yahooFinanceSymbols = symbols.map((symbol) =>
this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(symbol) this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(symbol)
); );
try { try {
const response: { [symbol: string]: IDataProviderResponse } = {};
let quotes: Pick< let quotes: Pick<
Quote, Quote,
'currency' | 'marketState' | 'regularMarketPrice' | 'symbol' 'currency' | 'marketState' | 'regularMarketPrice' | 'symbol'

View File

@ -95,6 +95,30 @@ export class ExchangeRateDataService {
const [currency1, currency2] = symbol.match(/.{1,3}/g); const [currency1, currency2] = symbol.match(/.{1,3}/g);
const [date] = Object.keys(result[symbol]); const [date] = Object.keys(result[symbol]);
// Add derived currencies
if (currency2 === 'GBP') {
resultExtended[`${currency1}GBp`] = {
[date]: {
marketPrice:
result[`${currency1}${currency2}`][date].marketPrice * 100
}
};
} else if (currency2 === 'ILS') {
resultExtended[`${currency1}ILA`] = {
[date]: {
marketPrice:
result[`${currency1}${currency2}`][date].marketPrice * 100
}
};
} else if (currency2 === 'ZAR') {
resultExtended[`${currency1}ZAc`] = {
[date]: {
marketPrice:
result[`${currency1}${currency2}`][date].marketPrice * 100
}
};
}
// Calculate the opposite direction // Calculate the opposite direction
resultExtended[`${currency2}${currency1}`] = { resultExtended[`${currency2}${currency1}`] = {
[date]: { [date]: {

View File

@ -254,16 +254,20 @@
</button> </button>
<mat-menu #accountMenu="matMenu" xPosition="before"> <mat-menu #accountMenu="matMenu" xPosition="before">
<button mat-menu-item (click)="onUpdateAccount(element)"> <button mat-menu-item (click)="onUpdateAccount(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline"></ion-icon> <ion-icon class="mr-2" name="create-outline"></ion-icon>
<span i18n>Edit</span> <span i18n>Edit</span>
</span>
</button> </button>
<button <button
mat-menu-item mat-menu-item
[disabled]="element.isDefault || element.transactionCount > 0" [disabled]="element.isDefault || element.transactionCount > 0"
(click)="onDeleteAccount(element.id)" (click)="onDeleteAccount(element.id)"
> >
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline"></ion-icon> <ion-icon class="mr-2" name="trash-outline"></ion-icon>
<span i18n>Delete</span> <span i18n>Delete</span>
</span>
</button> </button>
</mat-menu> </mat-menu>
</td> </td>

View File

@ -143,12 +143,24 @@
<ion-icon name="ellipsis-horizontal"></ion-icon> <ion-icon name="ellipsis-horizontal"></ion-icon>
</button> </button>
<mat-menu #assetProfileActionsMenu="matMenu" xPosition="before"> <mat-menu #assetProfileActionsMenu="matMenu" xPosition="before">
<button
mat-menu-item
(click)="onOpenAssetProfileDialog({ dataSource: element.dataSource, symbol: element.symbol })"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline"></ion-icon>
<span i18n>Edit</span>
</span>
</button>
<button <button
mat-menu-item mat-menu-item
[disabled]="element.activitiesCount !== 0" [disabled]="element.activitiesCount !== 0"
(click)="onDeleteProfileData({dataSource: element.dataSource, symbol: element.symbol})" (click)="onDeleteProfileData({dataSource: element.dataSource, symbol: element.symbol})"
> >
<ng-container i18n>Delete</ng-container> <span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
<span i18n>Delete</span>
</span>
</button> </button>
</mat-menu> </mat-menu>
</td> </td>

View File

@ -8,6 +8,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { FormBuilder, FormControl, Validators } from '@angular/forms'; import { FormBuilder, FormControl, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto'; import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
@ -25,8 +26,8 @@ import {
} from '@prisma/client'; } from '@prisma/client';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { parse as csvToJson } from 'papaparse'; import { parse as csvToJson } from 'papaparse';
import { Subject } from 'rxjs'; import { EMPTY, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { catchError, takeUntil } from 'rxjs/operators';
import { AssetProfileDialogParams } from './interfaces/interfaces'; import { AssetProfileDialogParams } from './interfaces/interfaces';
@ -50,6 +51,9 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
assetClass: new FormControl<AssetClass>(undefined), assetClass: new FormControl<AssetClass>(undefined),
assetSubClass: new FormControl<AssetSubClass>(undefined), assetSubClass: new FormControl<AssetSubClass>(undefined),
comment: '', comment: '',
historicalData: this.formBuilder.group({
csvString: ''
}),
name: ['', Validators.required], name: ['', Validators.required],
scraperConfiguration: '', scraperConfiguration: '',
symbolMapping: '' symbolMapping: ''
@ -59,7 +63,6 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
public countries: { public countries: {
[code: string]: { name: string; value: number }; [code: string]: { name: string; value: number };
}; };
public historicalDataAsCsvString: string;
public isBenchmark = false; public isBenchmark = false;
public marketDataDetails: MarketData[] = []; public marketDataDetails: MarketData[] = [];
public sectors: { public sectors: {
@ -78,7 +81,8 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams, @Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams,
private dataService: DataService, private dataService: DataService,
public dialogRef: MatDialogRef<AssetProfileDialog>, public dialogRef: MatDialogRef<AssetProfileDialog>,
private formBuilder: FormBuilder private formBuilder: FormBuilder,
private snackBar: MatSnackBar
) {} ) {}
public ngOnInit(): void { public ngOnInit(): void {
@ -88,9 +92,6 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
} }
public initialize() { public initialize() {
this.historicalDataAsCsvString =
AssetProfileDialog.HISTORICAL_DATA_TEMPLATE;
this.adminService this.adminService
.fetchAdminMarketDataBySymbol({ .fetchAdminMarketDataBySymbol({
dataSource: this.data.dataSource, dataSource: this.data.dataSource,
@ -128,10 +129,13 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
} }
this.assetProfileForm.setValue({ this.assetProfileForm.setValue({
name: this.assetProfile.name, assetClass: this.assetProfile.assetClass ?? null,
assetClass: this.assetProfile.assetClass, assetSubClass: this.assetProfile.assetSubClass ?? null,
assetSubClass: this.assetProfile.assetSubClass,
comment: this.assetProfile?.comment ?? '', comment: this.assetProfile?.comment ?? '',
historicalData: {
csvString: AssetProfileDialog.HISTORICAL_DATA_TEMPLATE
},
name: this.assetProfile.name ?? this.assetProfile.symbol,
scraperConfiguration: JSON.stringify( scraperConfiguration: JSON.stringify(
this.assetProfile?.scraperConfiguration ?? {} this.assetProfile?.scraperConfiguration ?? {}
), ),
@ -163,11 +167,16 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
} }
public onImportHistoricalData() { public onImportHistoricalData() {
const marketData = csvToJson(this.historicalDataAsCsvString, { try {
const marketData = csvToJson(
this.assetProfileForm.controls['historicalData'].controls['csvString']
.value,
{
dynamicTyping: true, dynamicTyping: true,
header: true, header: true,
skipEmptyLines: true skipEmptyLines: true
}).data; }
).data;
this.adminService this.adminService
.postMarketData({ .postMarketData({
@ -179,10 +188,25 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
}, },
symbol: this.data.symbol symbol: this.data.symbol
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(
catchError(({ error, message }) => {
this.snackBar.open(`${error}: ${message[0]}`, undefined, {
duration: 3000
});
return EMPTY;
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe(() => { .subscribe(() => {
this.initialize(); this.initialize();
}); });
} catch {
this.snackBar.open(
$localize`Oops! Could not parse historical data.`,
undefined,
{ duration: 3000 }
);
}
} }
public onMarketDataChanged(withRefresh: boolean = false) { public onMarketDataChanged(withRefresh: boolean = false) {

View File

@ -52,7 +52,7 @@
(marketDataChanged)="onMarketDataChanged($event)" (marketDataChanged)="onMarketDataChanged($event)"
></gf-admin-market-data-detail> ></gf-admin-market-data-detail>
<div class="mt-3"> <div class="mt-3" formGroupName="historicalData">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label> <mat-label>
<ng-container i18n>Historical Data</ng-container> (CSV) <ng-container i18n>Historical Data</ng-container> (CSV)
@ -60,11 +60,9 @@
<textarea <textarea
cdkAutosizeMaxRows="5" cdkAutosizeMaxRows="5"
cdkTextareaAutosize cdkTextareaAutosize
formControlName="csvString"
matInput matInput
placeholder="e.g. 20230601;1.61"
type="text" type="text"
[ngModelOptions]="{standalone: true}"
[(ngModel)]="historicalDataAsCsvString"
(keyup.enter)="$event.stopPropagation()" (keyup.enter)="$event.stopPropagation()"
></textarea> ></textarea>
</mat-form-field> </mat-form-field>
@ -75,6 +73,7 @@
color="accent" color="accent"
mat-flat-button mat-flat-button
type="button" type="button"
[disabled]="!assetProfileForm.controls['historicalData']?.controls['csvString'].touched || assetProfileForm.controls['historicalData']?.controls['csvString']?.value === ''"
(click)="onImportHistoricalData()" (click)="onImportHistoricalData()"
> >
<ng-container i18n>Import</ng-container> <ng-container i18n>Import</ng-container>
@ -179,13 +178,13 @@
</ng-container> </ng-container>
</div> </div>
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3"> <div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Name</mat-label> <mat-label i18n>Name</mat-label>
<input formControlName="name" matInput type="text" /> <input formControlName="name" matInput type="text" />
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3"> <div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Asset Class</mat-label> <mat-label i18n>Asset Class</mat-label>
<mat-select formControlName="assetClass"> <mat-select formControlName="assetClass">
<mat-option [value]="null"></mat-option> <mat-option [value]="null"></mat-option>
@ -198,7 +197,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3"> <div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Asset Sub Class</mat-label> <mat-label i18n>Asset Sub Class</mat-label>
<mat-select formControlName="assetSubClass"> <mat-select formControlName="assetSubClass">
<mat-option [value]="null"></mat-option> <mat-option [value]="null"></mat-option>

View File

@ -8,6 +8,7 @@ import { MatDialogModule } from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module'; 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 { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
@ -28,6 +29,7 @@ import { AssetProfileDialog } from './asset-profile-dialog.component';
MatInputModule, MatInputModule,
MatMenuModule, MatMenuModule,
MatSelectModule, MatSelectModule,
MatSnackBarModule,
ReactiveFormsModule, ReactiveFormsModule,
TextFieldModule TextFieldModule
], ],

View File

@ -55,6 +55,18 @@
</td> </td>
<td class="pl-1">{{ exchangeRate.label2 }}</td> <td class="pl-1">{{ exchangeRate.label2 }}</td>
<td> <td>
<a
class="h-100 mx-1 no-min-width px-2"
mat-button
[queryParams]="{
assetProfileDialog: true,
dataSource: exchangeRate.dataSource,
symbol: exchangeRate.symbol
}"
[routerLink]="['/admin', 'market-data']"
>
<ion-icon name="create-outline"></ion-icon>
</a>
<button <button
*ngIf="customCurrencies.includes(exchangeRate.label2)" *ngIf="customCurrencies.includes(exchangeRate.label2)"
class="h-100 mx-1 no-min-width px-2" class="h-100 mx-1 no-min-width px-2"

View File

@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { RouterModule } from '@angular/router';
import { CacheService } from '@ghostfolio/client/services/cache.service'; import { CacheService } from '@ghostfolio/client/services/cache.service';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
@ -21,7 +22,8 @@ import { AdminOverviewComponent } from './admin-overview.component';
MatCardModule, MatCardModule,
MatSelectModule, MatSelectModule,
MatSlideToggleModule, MatSlideToggleModule,
ReactiveFormsModule ReactiveFormsModule,
RouterModule
], ],
providers: [CacheService], providers: [CacheService],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]

View File

@ -86,12 +86,16 @@
</button> </button>
<mat-menu #platformMenu="matMenu" xPosition="before"> <mat-menu #platformMenu="matMenu" xPosition="before">
<button mat-menu-item (click)="onUpdatePlatform(element)"> <button mat-menu-item (click)="onUpdatePlatform(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline"></ion-icon> <ion-icon class="mr-2" name="create-outline"></ion-icon>
<span i18n>Edit</span> <span i18n>Edit</span>
</span>
</button> </button>
<button mat-menu-item (click)="onDeletePlatform(element.id)"> <button mat-menu-item (click)="onDeletePlatform(element.id)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline"></ion-icon> <ion-icon class="mr-2" name="trash-outline"></ion-icon>
<span i18n>Delete</span> <span i18n>Delete</span>
</span>
</button> </button>
</mat-menu> </mat-menu>
</td> </td>

View File

@ -66,12 +66,16 @@
</button> </button>
<mat-menu #tagMenu="matMenu" xPosition="before"> <mat-menu #tagMenu="matMenu" xPosition="before">
<button mat-menu-item (click)="onUpdateTag(element)"> <button mat-menu-item (click)="onUpdateTag(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline"></ion-icon> <ion-icon class="mr-2" name="create-outline"></ion-icon>
<span i18n>Edit</span> <span i18n>Edit</span>
</span>
</button> </button>
<button mat-menu-item (click)="onDeleteTag(element.id)"> <button mat-menu-item (click)="onDeleteTag(element.id)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline"></ion-icon> <ion-icon class="mr-2" name="trash-outline"></ion-icon>
<span i18n>Delete</span> <span i18n>Delete</span>
</span>
</button> </button>
</mat-menu> </mat-menu>
</td> </td>

View File

@ -203,16 +203,20 @@
mat-menu-item mat-menu-item
(click)="onImpersonateUser(element.id)" (click)="onImpersonateUser(element.id)"
> >
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="contract-outline"></ion-icon> <ion-icon class="mr-2" name="contract-outline"></ion-icon>
<span i18n>Impersonate User</span> <span i18n>Impersonate User</span>
</span>
</button> </button>
<button <button
mat-menu-item mat-menu-item
[disabled]="element.id === user?.id" [disabled]="element.id === user?.id"
(click)="onDeleteUser(element.id)" (click)="onDeleteUser(element.id)"
> >
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline"></ion-icon> <ion-icon class="mr-2" name="trash-outline"></ion-icon>
<span i18n>Delete User</span> <span i18n>Delete User</span>
</span>
</button> </button>
</mat-menu> </mat-menu>
</td> </td>

View File

@ -6,6 +6,7 @@ import {
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { resolveFearAndGreedIndex } from '@ghostfolio/common/helper'; import { resolveFearAndGreedIndex } from '@ghostfolio/common/helper';
import { translate } from '@ghostfolio/ui/i18n';
@Component({ @Component({
selector: 'gf-fear-and-greed-index', selector: 'gf-fear-and-greed-index',
@ -24,9 +25,9 @@ export class FearAndGreedIndexComponent implements OnChanges, OnInit {
public ngOnInit() {} public ngOnInit() {}
public ngOnChanges() { public ngOnChanges() {
const { emoji, text } = resolveFearAndGreedIndex(this.fearAndGreedIndex); const { emoji, key } = resolveFearAndGreedIndex(this.fearAndGreedIndex);
this.fearAndGreedIndexEmoji = emoji; this.fearAndGreedIndexEmoji = emoji;
this.fearAndGreedIndexText = text; this.fearAndGreedIndexText = translate(key);
} }
} }

View File

@ -33,6 +33,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
public isLoadingPerformance = true; public isLoadingPerformance = true;
public performance: PortfolioPerformance; public performance: PortfolioPerformance;
public showDetails = false; public showDetails = false;
public unit: string;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -69,6 +70,10 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
this.unit = this.hasImpersonationId
? '%'
: this.user?.settings?.baseCurrency;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });

View File

@ -86,7 +86,6 @@
<div class="col"> <div class="col">
<gf-portfolio-performance <gf-portfolio-performance
class="pb-4" class="pb-4"
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType" [deviceType]="deviceType"
[errors]="errors" [errors]="errors"
[isAllTimeHigh]="isAllTimeHigh" [isAllTimeHigh]="isAllTimeHigh"
@ -95,6 +94,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[performance]="performance" [performance]="performance"
[showDetails]="showDetails" [showDetails]="showDetails"
[unit]="unit"
></gf-portfolio-performance> ></gf-portfolio-performance>
<div *ngIf="showDetails" class="text-center"> <div *ngIf="showDetails" class="text-center">
<gf-toggle <gf-toggle

View File

@ -35,19 +35,9 @@
<span #value id="value"></span> <span #value id="value"></span>
</div> </div>
<div class="flex-grow-1 px-1"> <div class="flex-grow-1 px-1">
<ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse"
[theme]="{
height: '1.3rem',
width: '2.5rem'
}"
></ngx-skeleton-loader>
<div *ngIf="!isLoading">
{{ unit }} {{ unit }}
</div> </div>
</div> </div>
</div>
<div *ngIf="showDetails" class="row"> <div *ngIf="showDetails" class="row">
<div class="d-flex col justify-content-end"> <div class="d-flex col justify-content-end">
<gf-value <gf-value

View File

@ -25,7 +25,6 @@ import { isNumber } from 'lodash';
styleUrls: ['./portfolio-performance.component.scss'] styleUrls: ['./portfolio-performance.component.scss']
}) })
export class PortfolioPerformanceComponent implements OnChanges, OnInit { export class PortfolioPerformanceComponent implements OnChanges, OnInit {
@Input() baseCurrency: string;
@Input() deviceType: string; @Input() deviceType: string;
@Input() errors: ResponseError['errors']; @Input() errors: ResponseError['errors'];
@Input() isAllTimeHigh: boolean; @Input() isAllTimeHigh: boolean;
@ -34,11 +33,10 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
@Input() locale: string; @Input() locale: string;
@Input() performance: PortfolioPerformance; @Input() performance: PortfolioPerformance;
@Input() showDetails: boolean; @Input() showDetails: boolean;
@Input() unit: string;
@ViewChild('value') value: ElementRef; @ViewChild('value') value: ElementRef;
public unit: string;
public constructor() {} public constructor() {}
public ngOnInit() {} public ngOnInit() {}
@ -50,8 +48,6 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
} }
} else { } else {
if (isNumber(this.performance?.currentValue)) { if (isNumber(this.performance?.currentValue)) {
this.unit = this.baseCurrency;
new CountUp('value', this.performance?.currentValue, { new CountUp('value', this.performance?.currentValue, {
decimal: getNumberFormatDecimal(this.locale), decimal: getNumberFormatDecimal(this.locale),
decimalPlaces: decimalPlaces:
@ -63,8 +59,6 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
separator: getNumberFormatGroup(this.locale) separator: getNumberFormatGroup(this.locale)
}).start(); }).start();
} else if (this.performance?.currentValue === null) { } else if (this.performance?.currentValue === null) {
this.unit = '%';
new CountUp( new CountUp(
'value', 'value',
this.performance?.currentNetPerformancePercent * 100, this.performance?.currentNetPerformancePercent * 100,

View File

@ -1,6 +1,5 @@
<div class="container"> <div class="align-items-center container d-flex h-100 justify-content-center">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Membership</h1> <div class="row w-100">
<div class="row">
<div class="col"> <div class="col">
<div class="align-items-center d-flex flex-column"> <div class="align-items-center d-flex flex-column">
<gf-membership-card <gf-membership-card
@ -34,7 +33,7 @@
>&nbsp;<span i18n>per year</span> >&nbsp;<span i18n>per year</span>
</div> </div>
</ng-container> </ng-container>
<div class="align-items-center d-flex justfiy-content-center mt-4"> <div class="align-items-center d-flex justify-content-center mt-4">
<a <a
*ngIf="!user?.subscription?.expiresAt" *ngIf="!user?.subscription?.expiresAt"
class="mx-1" class="mx-1"

View File

@ -1,6 +1,7 @@
:host { :host {
color: rgb(var(--dark-primary-text)); color: rgb(var(--dark-primary-text));
display: block; display: block;
height: 100%;
} }
:host-context(.is-dark-theme) { :host-context(.is-dark-theme) {

View File

@ -0,0 +1,15 @@
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@Component({
host: { class: 'page' },
imports: [MatButtonModule, RouterModule],
selector: 'gf-hacktoberfest-2023-debriefing-page',
standalone: true,
templateUrl: './hacktoberfest-2023-debriefing-page.html'
})
export class Hacktoberfest2023DebriefingPageComponent {
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
}

View File

@ -0,0 +1,283 @@
<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">Hacktoberfest 2023 Debriefing</h1>
<div class="mb-3 text-muted"><small>2023-11-05</small></div>
<img
alt="Hacktoberfest 2023 with Ghostfolio Teaser"
class="rounded w-100"
src="../assets/images/blog/hacktoberfest-2023.png"
title="Hacktoberfest 2023 with Ghostfolio"
/>
</div>
<section class="mb-4">
<p>
As <a href="https://hacktoberfest.com">Hacktoberfest</a> has come to
an end, its time to look back and share our learnings.
Hacktoberfest is a month-long celebration of open source software
(OSS) projects, their maintainers, and the entire community of
contributors. Each October, open source maintainers from all over
the world give extra attention to new contributors while guiding
them through their first pull requests on
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>. This
year the event celebrated its 10th anniversary. At Ghostfolio, we
have participated in the event for the
<a href="../en/blog/2023/09/hacktoberfest-2023">second time</a> this
year.
</p>
<p>
In this debriefing, well take a closer look at our journey during
Hacktoberfest, exploring the facts and figures, key takeaways, and
the impact on Ghostfolio.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Hacktoberfest with Ghostfolio</h2>
<p>
<a href="https://ghostfol.io">Ghostfolio</a> is a modern web
application for managing personal finances. The software aggregates
your assets and empowers informed decision-making to help you
balance your portfolio or plan for future investments.
</p>
<p>
Our experience at Ghostfolio during Hacktoberfest 2023 has been
truly remarkable. Despite the absence of T-shirt rewards this year,
our expectations were exceeded in every way.
<a [routerLink]="routerLinkAbout">We</a> had the privilege of
collaborating with
<a
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
>20 talented developers</a
>
from around the world, each bringing their unique skills and
backgrounds to our fintech project.
</p>
<p>
<figure class="figure">
<a
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
>
<img
alt="Screenshot of the Ghostfolios Hacktoberfest 2023 Insights"
class="figure-img img-fluid rounded"
src="../assets/images/blog/hacktoberfest-2023-insights.png"
title="Screenshot of the Ghostfolios Hacktoberfest 2023 Insights"
/>
</a>
<figcaption class="figure-caption text-center">
Screenshot of the Ghostfolios Hacktoberfest 2023 Insights
</figcaption>
</figure>
</p>
<p>
All these contributions made during Hacktoberfest have a significant
impact on Ghostfolio. As many as 100 new
<a [routerLink]="routerLinkFeatures">features</a> and improvements
have been merged to enhance the user experience and software
platform management.
</p>
<p class="text-center">
<img
alt="Hacktoberfest 2023 Badges"
class="figure-img img-fluid rounded w-50"
src="../assets/images/blog/hacktoberfest-2023-badges.png"
title="Hacktoberfest 2023 Badges"
/>
</p>
<p>
The lessons learned through this global collaboration highlight the
collective spirit of the open source community, even without
traditional incentives. It serves as a driving force for Ghostfolio
to evolve into an exceptional piece of open source wealth management
software. Hacktoberfest 2023 has been an amazing and enlightening
journey for everyone involved.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Key Takeaways</h2>
<p>
Weve gathered some valuable takeaways from our experience to
consider for upcoming years. These insights can help us and fellow
open source projects make the most of Hacktoberfest:
</p>
<ul class="list-unstyled">
<li>
<h3 class="h5">Prepare early</h3>
<p>
Prospective contributors often begin looking for tasks as early
as the end of September. Preparing your project, organizing
issues, and setting clear goals in advance can help you attract
and engage more participants.
</p>
</li>
<li>
<h3 class="h5">Meet formal requirements</h3>
<p>
Ensure that your project aligns with Hacktoberfests formal
requirements and rules. Properly tag issues, provide clear
guidelines for contributions, and make sure your project is
accessible to newcomers.
</p>
</li>
<li>
<h3 class="h5">Allocate sufficient time</h3>
<p>
Being responsive and providing support to participants is
crucial. Allocate enough time to answer questions, review and
merge pull requests, and offer guidance to first-time
contributors. This level of support can significantly enhance
the experience for both contributors and maintainers.
</p>
</li>
<li>
<h3 class="h5">Provide clarity in descriptions</h3>
<p>
When creating issues for Hacktoberfest, be as clear as possible
in your descriptions. Including screenshots, links to code
examples, and detailed instructions can make it easier for
contributors to understand and complete the task successfully.
</p>
</li>
<li>
<h3 class="h5">Isolate tasks</h3>
<p>
Ideally, focus on tasks that are approachable for beginners, as
the setup and frameworks used can already be quite challenging.
This way, you can attract a wider range of contributors.
</p>
</li>
<li>
<h3 class="h5">Embrace learning</h3>
<p>
Understand that not every attempt will result in a successful
contribution. Its okay if a contributors pull request doesnt
make it into the project. Encourage a learning mindset and
provide constructive feedback, helping contributors grow and
improve over time.
</p>
</li>
</ul>
<p>
By following these takeaways, we can ensure a rewarding experience
for everyone taking part in future Hacktoberfest events.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Thank you</h2>
<p>
Our experience with Hacktoberfest truly showcases the magic of open
source. The sense of community, mentorship, and our shared
dedication to software development is what motivates us at
Ghostfolio. As we look forward to future collaborations to make
personal finance and investing more accessible, we simply want to
thank everyone involved.
</p>
<p>
Keep coding and sharing your learnings!<br />
Thomas from Ghostfolio
</p>
</section>
<section class="mb-4">
<ul class="list-inline">
<li class="list-inline-item">
<span class="badge badge-light">Code</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Collaboration</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Community</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Development</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Feature</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Finance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Fintech</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Ghostfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">GitHub</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Hacktoberfest</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Investing</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Investment</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Learning</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Lessons learned</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Mindset</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">October</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Open Source</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">OSS</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Personal Finance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Portfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Programming</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">Takeaways</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">User Experience</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">UX</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Wealth</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Wealth Management</span>
</li>
</ul>
</section>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
</li>
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
Hacktoberfest 2023 Debriefing
</li>
</ol>
</nav>
</article>
</div>
</div>
</div>

View File

@ -163,6 +163,15 @@ const routes: Routes = [
'./2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component' './2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component'
).then((c) => c.Hacktoberfest2023PageComponent), ).then((c) => c.Hacktoberfest2023PageComponent),
title: 'Hacktoberfest 2023' title: 'Hacktoberfest 2023'
},
{
canActivate: [AuthGuard],
path: '2023/11/hacktoberfest-2023-debriefing',
loadComponent: () =>
import(
'./2023/11/hacktoberfest-2023/hacktoberfest-2023-debriefing-page.component'
).then((c) => c.Hacktoberfest2023DebriefingPageComponent),
title: 'Hacktoberfest 2023 Debriefing'
} }
]; ];

View File

@ -8,6 +8,32 @@
finance</small finance</small
> >
</h1> </h1>
<mat-card appearance="outlined" 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="../en/blog/2023/11/hacktoberfest-2023-debriefing"
>
<div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate">
Hacktoberfest 2023 Debriefing
</div>
<div class="d-flex text-muted">2023-11-05</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 appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-content> <mat-card-content>
<div class="container p-0"> <div class="container p-0">

View File

@ -16,10 +16,10 @@ const routes: Routes = [
.filter(({ key }) => { .filter(({ key }) => {
return key !== 'ghostfolio'; return key !== 'ghostfolio';
}) })
.map(({ component, key, name }) => { .map(({ alias, component, key, name }) => {
return { return {
canActivate: [AuthGuard], canActivate: [AuthGuard],
path: $localize`open-source-alternative-to` + `-${key}`, path: $localize`open-source-alternative-to` + `-${alias ?? key}`,
loadComponent: () => loadComponent: () =>
import(`./products/${key}-page.component`).then(() => component), import(`./products/${key}-page.component`).then(() => component),
title: $localize`Open Source Alternative to ${name}` title: $localize`Open Source Alternative to ${name}`

View File

@ -12,8 +12,12 @@ import { products } from './products';
export class PersonalFinanceToolsPageComponent implements OnDestroy { export class PersonalFinanceToolsPageComponent implements OnDestroy {
public pathAlternativeTo = $localize`open-source-alternative-to` + '-'; public pathAlternativeTo = $localize`open-source-alternative-to` + '-';
public pathResources = '/' + $localize`resources`; public pathResources = '/' + $localize`resources`;
public products = products.filter(({ key }) => { public products = products
.filter(({ key }) => {
return key !== 'ghostfolio'; return key !== 'ghostfolio';
})
.sort((a, b) => {
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
}); });
public routerLinkAbout = ['/' + $localize`about`]; public routerLinkAbout = ['/' + $localize`about`];

View File

@ -28,8 +28,8 @@
<div class="flex-nowrap no-gutters row"> <div class="flex-nowrap no-gutters row">
<a <a
class="d-flex overflow-hidden w-100" class="d-flex overflow-hidden w-100"
title="Compare Ghostfolio to {{ product.name }}" title="Compare Ghostfolio to {{ product.name }} - {{ product.slogan }}"
[routerLink]="[pathResources, 'personal-finance-tools', pathAlternativeTo + product.key]" [routerLink]="[pathResources, 'personal-finance-tools', pathAlternativeTo + (product.alias ?? product.key)]"
> >
<div class="flex-grow-1 overflow-hidden"> <div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate" i18n> <div class="h6 m-0 text-truncate" i18n>

View File

@ -1,5 +1,6 @@
import { Product } from '@ghostfolio/common/interfaces'; import { Product } from '@ghostfolio/common/interfaces';
import { AllvueSystemsPageComponent } from './products/allvue-systems-page.component';
import { AltooPageComponent } from './products/altoo-page.component'; import { AltooPageComponent } from './products/altoo-page.component';
import { BeanvestPageComponent } from './products/beanvest-page.component'; import { BeanvestPageComponent } from './products/beanvest-page.component';
import { CapitallyPageComponent } from './products/capitally-page.component'; import { CapitallyPageComponent } from './products/capitally-page.component';
@ -7,21 +8,26 @@ import { CapMonPageComponent } from './products/capmon-page.component';
import { CopilotMoneyPageComponent } from './products/copilot-money-page.component'; import { CopilotMoneyPageComponent } from './products/copilot-money-page.component';
import { DeltaPageComponent } from './products/delta-page.component'; import { DeltaPageComponent } from './products/delta-page.component';
import { DivvyDiaryPageComponent } from './products/divvydiary-page.component'; import { DivvyDiaryPageComponent } from './products/divvydiary-page.component';
import { EightFiguresPageComponent } from './products/eightfigures-page.component';
import { ExirioPageComponent } from './products/exirio-page.component'; import { ExirioPageComponent } from './products/exirio-page.component';
import { FinaryPageComponent } from './products/finary-page.component'; import { FinaryPageComponent } from './products/finary-page.component';
import { FinWisePageComponent } from './products/finwise-page.component';
import { FolisharePageComponent } from './products/folishare-page.component'; import { FolisharePageComponent } from './products/folishare-page.component';
import { GetquinPageComponent } from './products/getquin-page.component'; import { GetquinPageComponent } from './products/getquin-page.component';
import { GoSpatzPageComponent } from './products/gospatz-page.component'; import { GoSpatzPageComponent } from './products/gospatz-page.component';
import { IntuitMintPageComponent } from './products/intuit-mint-page.component';
import { JustEtfPageComponent } from './products/justetf-page.component'; import { JustEtfPageComponent } from './products/justetf-page.component';
import { KuberaPageComponent } from './products/kubera-page.component'; import { KuberaPageComponent } from './products/kubera-page.component';
import { MarketsShPageComponent } from './products/markets.sh-page.component'; import { MarketsShPageComponent } from './products/markets.sh-page.component';
import { MaybeFinancePageComponent } from './products/maybe-finance-page.component'; import { MaybeFinancePageComponent } from './products/maybe-finance-page.component';
import { MonarchMoneyPageComponent } from './products/monarch-money-page.component';
import { MonsePageComponent } from './products/monse-page.component'; import { MonsePageComponent } from './products/monse-page.component';
import { ParqetPageComponent } from './products/parqet-page.component'; import { ParqetPageComponent } from './products/parqet-page.component';
import { PlannixPageComponent } from './products/plannix-page.component'; import { PlannixPageComponent } from './products/plannix-page.component';
import { PortfolioDividendTrackerPageComponent } from './products/portfolio-dividend-tracker-page.component'; import { PortfolioDividendTrackerPageComponent } from './products/portfolio-dividend-tracker-page.component';
import { PortseidoPageComponent } from './products/portseido-page.component'; import { PortseidoPageComponent } from './products/portseido-page.component';
import { ProjectionLabPageComponent } from './products/projectionlab-page.component'; import { ProjectionLabPageComponent } from './products/projectionlab-page.component';
import { RocketMoneyPageComponent } from './products/rocket-money-page.component';
import { SeekingAlphaPageComponent } from './products/seeking-alpha-page.component'; import { SeekingAlphaPageComponent } from './products/seeking-alpha-page.component';
import { SharesightPageComponent } from './products/sharesight-page.component'; import { SharesightPageComponent } from './products/sharesight-page.component';
import { SimplePortfolioPageComponent } from './products/simple-portfolio-page.component'; import { SimplePortfolioPageComponent } from './products/simple-portfolio-page.component';
@ -30,8 +36,10 @@ import { StocklePageComponent } from './products/stockle-page.component';
import { StockMarketEyePageComponent } from './products/stockmarketeye-page.component'; import { StockMarketEyePageComponent } from './products/stockmarketeye-page.component';
import { SumioPageComponent } from './products/sumio-page.component'; import { SumioPageComponent } from './products/sumio-page.component';
import { UtlunaPageComponent } from './products/utluna-page.component'; import { UtlunaPageComponent } from './products/utluna-page.component';
import { VyzerPageComponent } from './products/vyzer-page.component';
import { WealthicaPageComponent } from './products/wealthica-page.component'; import { WealthicaPageComponent } from './products/wealthica-page.component';
import { YeekateePageComponent } from './products/yeekatee-page.component'; import { YeekateePageComponent } from './products/yeekatee-page.component';
import { YnabPageComponent } from './products/ynab-page.component';
export const products: Product[] = [ export const products: Product[] = [
{ {
@ -57,6 +65,16 @@ export const products: Product[] = [
slogan: 'Open Source Wealth Management', slogan: 'Open Source Wealth Management',
useAnonymously: true useAnonymously: true
}, },
{
component: AllvueSystemsPageComponent,
founded: 2019,
hasFreePlan: false,
hasSelfHostingAbility: false,
key: 'allvue-systems',
name: 'Allvue Systems',
origin: $localize`United States`,
slogan: 'Investment Software Suite'
},
{ {
component: AltooPageComponent, component: AltooPageComponent,
founded: 2017, founded: 2017,
@ -93,7 +111,7 @@ export const products: Product[] = [
key: 'capmon', key: 'capmon',
name: 'CapMon.org', name: 'CapMon.org',
origin: $localize`Germany`, origin: $localize`Germany`,
note: 'Sunset in 2023', note: 'CapMon.org has discontinued in 2023',
slogan: 'Next Generation Assets Tracking' slogan: 'Next Generation Assets Tracking'
}, },
{ {
@ -130,6 +148,15 @@ export const products: Product[] = [
pricingPerYear: '€65', pricingPerYear: '€65',
slogan: 'Your personal Dividend Calendar' slogan: 'Your personal Dividend Calendar'
}, },
{
alias: '8figures',
component: EightFiguresPageComponent,
founded: 2022,
key: 'eightfigures',
name: '8FIGURES',
origin: $localize`United States`,
slogan: 'Portfolio Tracker Designed by Professional Investors'
},
{ {
component: ExirioPageComponent, component: ExirioPageComponent,
founded: 2020, founded: 2020,
@ -150,6 +177,16 @@ export const products: Product[] = [
origin: $localize`United States`, origin: $localize`United States`,
slogan: 'Real-Time Portfolio Tracker & Stock Tracker' slogan: 'Real-Time Portfolio Tracker & Stock Tracker'
}, },
{
component: FinWisePageComponent,
founded: 2023,
hasFreePlan: true,
key: 'finwise',
name: 'FinWise',
origin: $localize`South Africa`,
pricingPerYear: '€69.99',
slogan: 'Personal finances, simplified'
},
{ {
component: FolisharePageComponent, component: FolisharePageComponent,
hasFreePlan: true, hasFreePlan: true,
@ -182,6 +219,17 @@ export const products: Product[] = [
origin: $localize`Germany`, origin: $localize`Germany`,
slogan: 'Volle Kontrolle über deine Investitionen' slogan: 'Volle Kontrolle über deine Investitionen'
}, },
{
component: IntuitMintPageComponent,
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'intuit-mint',
name: 'Intuit Mint',
note: 'Intuit Mint has discontinued in 2023',
origin: $localize`United States`,
pricingPerYear: '$60',
slogan: 'Managing money, made simple'
},
{ {
component: JustEtfPageComponent, component: JustEtfPageComponent,
founded: 2011, founded: 2011,
@ -224,12 +272,23 @@ export const products: Product[] = [
key: 'maybe-finance', key: 'maybe-finance',
languages: ['English'], languages: ['English'],
name: 'Maybe Finance', name: 'Maybe Finance',
note: 'Sunset in 2023', note: 'Maybe Finance has discontinued in 2023',
origin: $localize`United States`, origin: $localize`United States`,
pricingPerYear: '$145', pricingPerYear: '$145',
region: $localize`United States`, region: $localize`United States`,
slogan: 'Your financial future, in your control' slogan: 'Your financial future, in your control'
}, },
{
component: MonarchMoneyPageComponent,
founded: 2019,
hasFreePlan: false,
hasSelfHostingAbility: false,
key: 'monarch-money',
name: 'Monarch Money',
origin: $localize`United States`,
pricingPerYear: '$99.99',
slogan: 'The modern way to manage your money'
},
{ {
component: MonsePageComponent, component: MonsePageComponent,
hasFreePlan: false, hasFreePlan: false,
@ -295,6 +354,15 @@ export const products: Product[] = [
pricingPerYear: '$108', pricingPerYear: '$108',
slogan: 'Build Financial Plans You Love.' slogan: 'Build Financial Plans You Love.'
}, },
{
component: RocketMoneyPageComponent,
founded: 2015,
hasSelfHostingAbility: false,
key: 'rocket-money',
name: 'Rocket Money',
origin: $localize`United States`,
slogan: 'Track your net worth'
},
{ {
component: SeekingAlphaPageComponent, component: SeekingAlphaPageComponent,
founded: 2004, founded: 2004,
@ -352,7 +420,7 @@ export const products: Product[] = [
key: 'stockmarketeye', key: 'stockmarketeye',
name: 'StockMarketEye', name: 'StockMarketEye',
origin: $localize`France`, origin: $localize`France`,
note: 'Sunset in 2023', note: 'StockMarketEye has discontinued in 2023',
slogan: 'A Powerful Portfolio & Investment Tracking App' slogan: 'A Powerful Portfolio & Investment Tracking App'
}, },
{ {
@ -377,6 +445,16 @@ export const products: Product[] = [
slogan: 'Your Portfolio. Revealed.', slogan: 'Your Portfolio. Revealed.',
useAnonymously: true useAnonymously: true
}, },
{
component: VyzerPageComponent,
founded: 2020,
hasFreePlan: true,
key: 'vyzer',
name: 'Vyzer',
origin: $localize`United States`,
pricingPerYear: '$348',
slogan: 'Virtual Family Office for Smart Wealth Management'
},
{ {
component: WealthicaPageComponent, component: WealthicaPageComponent,
founded: 2015, founded: 2015,
@ -398,5 +476,16 @@ export const products: Product[] = [
origin: $localize`Switzerland`, origin: $localize`Switzerland`,
region: $localize`Switzerland`, region: $localize`Switzerland`,
slogan: 'Connect. Share. Invest.' slogan: 'Connect. Share. Invest.'
},
{
component: YnabPageComponent,
founded: 2004,
hasFreePlan: false,
hasSelfHostingAbility: false,
key: 'ynab',
name: 'YNAB (You Need a Budget)',
origin: $localize`United States`,
pricingPerYear: '$99',
slogan: 'Change Your Relationship With Money'
} }
]; ];

View File

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-allvue-systems-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class AllvueSystemsPageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'allvue-systems';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

View File

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-eightfigures-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class EightFiguresPageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'eightfigures';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

View File

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-finwise-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class FinWisePageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'finwise';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

View File

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-intuit-mint-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class IntuitMintPageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'intuit-mint';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

View File

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-monarch-money-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class MonarchMoneyPageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'monarch-money';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

View File

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-rocket-money-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class RocketMoneyPageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'rocket-money';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

View File

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-vyzer-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class VyzerPageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'vyzer';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

View File

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-ynab-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class YnabPageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'ynab';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

View File

@ -46,12 +46,10 @@ export class WebAuthnService {
switchMap((attOps) => { switchMap((attOps) => {
return startRegistration(attOps); return startRegistration(attOps);
}), }),
switchMap((attResp) => { switchMap((credential) => {
return this.http.post<AuthDeviceDto>( return this.http.post<AuthDeviceDto>(
`/api/v1/auth/webauthn/verify-attestation`, `/api/v1/auth/webauthn/verify-attestation`,
{ { credential }
credential: attResp
}
); );
}), }),
tap((authDevice) => tap((authDevice) =>
@ -65,6 +63,7 @@ export class WebAuthnService {
public deregister() { public deregister() {
const deviceId = this.getDeviceId(); const deviceId = this.getDeviceId();
return this.http return this.http
.delete<AuthDeviceDto>(`/api/v1/auth-device/${deviceId}`) .delete<AuthDeviceDto>(`/api/v1/auth-device/${deviceId}`)
.pipe( .pipe(
@ -82,20 +81,21 @@ export class WebAuthnService {
public login() { public login() {
const deviceId = this.getDeviceId(); const deviceId = this.getDeviceId();
return this.http return this.http
.post<PublicKeyCredentialRequestOptionsJSON>( .post<PublicKeyCredentialRequestOptionsJSON>(
`/api/v1/auth/webauthn/generate-assertion-options`, `/api/v1/auth/webauthn/generate-assertion-options`,
{ deviceId } { deviceId }
) )
.pipe( .pipe(
switchMap((requestOptionsJSON) => switchMap((requestOptionsJSON) => {
startAuthentication(requestOptionsJSON, true) return startAuthentication(requestOptionsJSON);
), }),
switchMap((assertionResponse) => { switchMap((credential) => {
return this.http.post<{ authToken: string }>( return this.http.post<{ authToken: string }>(
`/api/v1/auth/webauthn/verify-assertion`, `/api/v1/auth/webauthn/verify-assertion`,
{ {
credential: assertionResponse, credential,
deviceId deviceId
} }
); );

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,10 @@ import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
import { Benchmark, UniqueAsset } from './interfaces'; import { Benchmark, UniqueAsset } from './interfaces';
import { ColorScheme } from './types'; import { ColorScheme } from './types';
export const DATE_FORMAT = 'yyyy-MM-dd';
export const DATE_FORMAT_MONTHLY = 'MMMM yyyy';
export const DATE_FORMAT_YEARLY = 'yyyy';
const NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g; const NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
export function capitalize(aString: string) { export function capitalize(aString: string) {
@ -240,10 +244,6 @@ export function groupBy<T, K extends keyof T>(
return map; return map;
} }
export function isCurrency(aSymbol = '') {
return currencies[aSymbol];
}
export function interpolate(template: string, context: any) { export function interpolate(template: string, context: any) {
return template?.replace(/[$]{([^}]+)}/g, (_, objectPath) => { return template?.replace(/[$]{([^}]+)}/g, (_, objectPath) => {
const properties = objectPath.split('.'); const properties = objectPath.split('.');
@ -254,44 +254,10 @@ export function interpolate(template: string, context: any) {
}); });
} }
export function resetHours(aDate: Date) { export function isCurrency(aSymbol = '') {
const year = getYear(aDate); return currencies[aSymbol];
const month = getMonth(aDate);
const day = getDate(aDate);
return new Date(Date.UTC(year, month, day));
} }
export function resolveFearAndGreedIndex(aValue: number) {
if (aValue <= 25) {
return { emoji: '🥵', text: 'Extreme Fear' };
} else if (aValue <= 45) {
return { emoji: '😨', text: 'Fear' };
} else if (aValue <= 55) {
return { emoji: '😐', text: 'Neutral' };
} else if (aValue < 75) {
return { emoji: '😜', text: 'Greed' };
} else {
return { emoji: '🤪', text: 'Extreme Greed' };
}
}
export function resolveMarketCondition(
aMarketCondition: Benchmark['marketCondition']
) {
if (aMarketCondition === 'BEAR_MARKET') {
return { emoji: '🐻' };
} else if (aMarketCondition === 'BULL_MARKET') {
return { emoji: '🐮' };
} else {
return { emoji: '⚪' };
}
}
export const DATE_FORMAT = 'yyyy-MM-dd';
export const DATE_FORMAT_MONTHLY = 'MMMM yyyy';
export const DATE_FORMAT_YEARLY = 'yyyy';
export function parseDate(date: string): Date | null { export function parseDate(date: string): Date | null {
// Transform 'yyyyMMdd' format to supported format by parse function // Transform 'yyyyMMdd' format to supported format by parse function
if (date?.length === 8) { if (date?.length === 8) {
@ -334,3 +300,37 @@ export function parseSymbol({ dataSource, symbol }: UniqueAsset) {
export function prettifySymbol(aSymbol: string): string { export function prettifySymbol(aSymbol: string): string {
return aSymbol?.replace(ghostfolioScraperApiSymbolPrefix, ''); return aSymbol?.replace(ghostfolioScraperApiSymbolPrefix, '');
} }
export function resetHours(aDate: Date) {
const year = getYear(aDate);
const month = getMonth(aDate);
const day = getDate(aDate);
return new Date(Date.UTC(year, month, day));
}
export function resolveFearAndGreedIndex(aValue: number) {
if (aValue <= 25) {
return { emoji: '🥵', key: 'EXTREME_FEAR', text: 'Extreme Fear' };
} else if (aValue <= 45) {
return { emoji: '😨', key: 'FEAR', text: 'Fear' };
} else if (aValue <= 55) {
return { emoji: '😐', key: 'NEUTRAL', text: 'Neutral' };
} else if (aValue < 75) {
return { emoji: '😜', key: 'GREED', text: 'Greed' };
} else {
return { emoji: '🤪', key: 'EXTREME_GREED', text: 'Extreme Greed' };
}
}
export function resolveMarketCondition(
aMarketCondition: Benchmark['marketCondition']
) {
if (aMarketCondition === 'BEAR_MARKET') {
return { emoji: '🐻' };
} else if (aMarketCondition === 'BULL_MARKET') {
return { emoji: '🐮' };
} else {
return { emoji: '⚪' };
}
}

View File

@ -1,5 +1,11 @@
import { UniqueAsset } from './unique-asset.interface';
export interface AdminData { export interface AdminData {
exchangeRates: { label1: string; label2: string; value: number }[]; exchangeRates: ({
label1: string;
label2: string;
value: number;
} & UniqueAsset)[];
settings: { [key: string]: boolean | object | string | string[] }; settings: { [key: string]: boolean | object | string | string[] };
transactionCount: number; transactionCount: number;
userCount: number; userCount: number;

View File

@ -1,4 +1,5 @@
export interface Product { export interface Product {
alias?: string;
component: any; component: any;
founded?: number; founded?: number;
hasFreePlan?: boolean; hasFreePlan?: boolean;

View File

@ -30,8 +30,10 @@
[disabled]="dataSource.data.length === 0" [disabled]="dataSource.data.length === 0"
(click)="onImportDividends()" (click)="onImportDividends()"
> >
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="color-wand-outline"></ion-icon> <ion-icon class="mr-2" name="color-wand-outline"></ion-icon>
<ng-container i18n>Import Dividends</ng-container>... <ng-container i18n>Import Dividends</ng-container>...
</span>
</button> </button>
<button <button
*ngIf="hasPermissionToExportActivities" *ngIf="hasPermissionToExportActivities"
@ -40,8 +42,10 @@
[disabled]="dataSource.data.length === 0" [disabled]="dataSource.data.length === 0"
(click)="onExport()" (click)="onExport()"
> >
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon> <ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
<span i18n>Export Activities</span> <span i18n>Export Activities</span>
</span>
</button> </button>
<button <button
*ngIf="hasPermissionToExportActivities" *ngIf="hasPermissionToExportActivities"
@ -50,16 +54,20 @@
[disabled]="!hasDrafts" [disabled]="!hasDrafts"
(click)="onExportDrafts()" (click)="onExportDrafts()"
> >
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon> <ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
<span i18n>Export Drafts as ICS</span> <span i18n>Export Drafts as ICS</span>
</span>
</button> </button>
<button <button
class="align-items-center d-flex" class="align-items-center d-flex"
mat-menu-item mat-menu-item
(click)="onDeleteAllActivities()" (click)="onDeleteAllActivities()"
> >
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline"></ion-icon> <ion-icon class="mr-2" name="trash-outline"></ion-icon>
<span i18n>Delete all Activities</span> <span i18n>Delete all Activities</span>
</span>
</button> </button>
</mat-menu> </mat-menu>
</div> </div>
@ -440,8 +448,10 @@
mat-menu-item mat-menu-item
(click)="onImport()" (click)="onImport()"
> >
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon> <ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
<ng-container i18n>Import Activities</ng-container>... <ng-container i18n>Import Activities</ng-container>...
</span>
</button> </button>
<button <button
*ngIf="hasPermissionToCreateActivity" *ngIf="hasPermissionToCreateActivity"
@ -449,8 +459,10 @@
[disabled]="dataSource.data.length === 0" [disabled]="dataSource.data.length === 0"
(click)="onImportDividends()" (click)="onImportDividends()"
> >
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="color-wand-outline"></ion-icon> <ion-icon class="mr-2" name="color-wand-outline"></ion-icon>
<ng-container i18n>Import Dividends</ng-container>... <ng-container i18n>Import Dividends</ng-container>...
</span>
</button> </button>
<button <button
*ngIf="hasPermissionToExportActivities" *ngIf="hasPermissionToExportActivities"
@ -459,8 +471,10 @@
[disabled]="dataSource.data.length === 0" [disabled]="dataSource.data.length === 0"
(click)="onExport()" (click)="onExport()"
> >
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon> <ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
<span i18n>Export Activities</span> <span i18n>Export Activities</span>
</span>
</button> </button>
<button <button
*ngIf="hasPermissionToExportActivities" *ngIf="hasPermissionToExportActivities"
@ -469,8 +483,10 @@
[disabled]="!hasDrafts" [disabled]="!hasDrafts"
(click)="onExportDrafts()" (click)="onExportDrafts()"
> >
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon> <ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
<span i18n>Export Drafts as ICS</span> <span i18n>Export Drafts as ICS</span>
</span>
</button> </button>
</mat-menu> </mat-menu>
</th> </th>
@ -486,24 +502,32 @@
</button> </button>
<mat-menu #activityMenu="matMenu" xPosition="before"> <mat-menu #activityMenu="matMenu" xPosition="before">
<button mat-menu-item (click)="onUpdateActivity(element)"> <button mat-menu-item (click)="onUpdateActivity(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline"></ion-icon> <ion-icon class="mr-2" name="create-outline"></ion-icon>
<span i18n>Edit</span> <span i18n>Edit</span>
</span>
</button> </button>
<button mat-menu-item (click)="onCloneActivity(element)"> <button mat-menu-item (click)="onCloneActivity(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="copy-outline"></ion-icon> <ion-icon class="mr-2" name="copy-outline"></ion-icon>
<span i18n>Clone</span> <span i18n>Clone</span>
</span>
</button> </button>
<button <button
mat-menu-item mat-menu-item
[disabled]="!element.isDraft" [disabled]="!element.isDraft"
(click)="onExportDraft(element.id)" (click)="onExportDraft(element.id)"
> >
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon> <ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
<span i18n>Export Draft as ICS</span> <span i18n>Export Draft as ICS</span>
</span>
</button> </button>
<button mat-menu-item (click)="onDeleteActivity(element.id)"> <button mat-menu-item (click)="onDeleteActivity(element.id)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline"></ion-icon> <ion-icon class="mr-2" name="trash-outline"></ion-icon>
<span i18n>Delete</span> <span i18n>Delete</span>
</span>
</button> </button>
</mat-menu> </mat-menu>
</td> </td>

View File

@ -57,7 +57,14 @@ const locales = {
Europe: $localize`Europe`, Europe: $localize`Europe`,
'North America': $localize`North America`, 'North America': $localize`North America`,
Oceania: $localize`Oceania`, Oceania: $localize`Oceania`,
'South America': $localize`South America` 'South America': $localize`South America`,
// Fear and Greed Index
EXTREME_FEAR: $localize`Extreme Fear`,
EXTREME_GREED: $localize`Extreme Greed`,
FEAR: $localize`Fear`,
GREED: $localize`Greed`,
NEUTRAL: $localize`Neutral`
}; };
export function translate(aKey: string): string { export function translate(aKey: string): string {

View File

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.16.0", "version": "2.20.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",
@ -53,17 +53,17 @@
"workspace-generator": "nx workspace-generator" "workspace-generator": "nx workspace-generator"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "16.2.1", "@angular/animations": "16.2.12",
"@angular/cdk": "16.2.1", "@angular/cdk": "16.2.11",
"@angular/common": "16.2.1", "@angular/common": "16.2.12",
"@angular/compiler": "16.2.1", "@angular/compiler": "16.2.12",
"@angular/core": "16.2.1", "@angular/core": "16.2.12",
"@angular/forms": "16.2.1", "@angular/forms": "16.2.12",
"@angular/material": "16.2.1", "@angular/material": "16.2.11",
"@angular/platform-browser": "16.2.1", "@angular/platform-browser": "16.2.12",
"@angular/platform-browser-dynamic": "16.2.1", "@angular/platform-browser-dynamic": "16.2.12",
"@angular/router": "16.2.1", "@angular/router": "16.2.12",
"@angular/service-worker": "16.2.1", "@angular/service-worker": "16.2.12",
"@codewithdan/observable-store": "2.2.15", "@codewithdan/observable-store": "2.2.15",
"@dfinity/agent": "0.15.7", "@dfinity/agent": "0.15.7",
"@dfinity/auth-client": "0.15.7", "@dfinity/auth-client": "0.15.7",
@ -133,17 +133,17 @@
"zone.js": "0.13.1" "zone.js": "0.13.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "16.2.0", "@angular-devkit/build-angular": "16.2.9",
"@angular-devkit/core": "16.2.0", "@angular-devkit/core": "16.2.9",
"@angular-devkit/schematics": "16.2.0", "@angular-devkit/schematics": "16.2.9",
"@angular-eslint/eslint-plugin": "16.1.0", "@angular-eslint/eslint-plugin": "16.2.0",
"@angular-eslint/eslint-plugin-template": "16.1.0", "@angular-eslint/eslint-plugin-template": "16.2.0",
"@angular-eslint/template-parser": "16.1.0", "@angular-eslint/template-parser": "16.2.0",
"@angular/cli": "16.2.0", "@angular/cli": "16.2.9",
"@angular/compiler-cli": "16.2.1", "@angular/compiler-cli": "16.2.12",
"@angular/language-service": "16.2.1", "@angular/language-service": "16.2.12",
"@angular/localize": "16.2.1", "@angular/localize": "16.2.12",
"@angular/pwa": "16.2.0", "@angular/pwa": "16.2.9",
"@nestjs/schematics": "10.0.1", "@nestjs/schematics": "10.0.1",
"@nestjs/testing": "10.1.3", "@nestjs/testing": "10.1.3",
"@nx/angular": "17.0.2", "@nx/angular": "17.0.2",

View File

@ -0,0 +1,2 @@
-- AlterTable
UPDATE "Account" SET "accountType" = NULL;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Account" DROP COLUMN "accountType";

View File

@ -21,7 +21,6 @@ model Access {
} }
model Account { model Account {
accountType AccountType?
balance Float @default(0) balance Float @default(0)
balances AccountBalance[] balances AccountBalance[]
comment String? comment String?

View File

@ -1,5 +1,5 @@
Date,Code,Currency,Price,Quantity,Action,Fee,Note Date,Code,Currency,Price,Quantity,Action,Fee,Note
16-09-2021,MSFT,USD,298.580,5,buy,19.00,My first order 🤓 16-09-2021,MSFT,USD,298.580,5,buy,19.00,My first order 🤓
17/11/2021,MSFT,USD,0.62,5,dividend,0.00 17/11/2021,MSFT,USD,0.62,5,dividend,0.00,
01.01.2022,Penthouse Apartment,USD,500000.0,1,<invalid>,0.00 01.01.2022,Penthouse Apartment,USD,500000.0,1,<invalid>,0.00,
20500606,MSFT,USD,0.00,0,buy,0.00 20500606,MSFT,USD,0.00,0,buy,0.00,

1 Date,Code,Currency,Price,Quantity,Action,Fee,Note Date Code Currency Price Quantity Action Fee Note
2 16-09-2021,MSFT,USD,298.580,5,buy,19.00,My first order 🤓 16-09-2021 MSFT USD 298.580 5 buy 19.00 My first order 🤓
3 17/11/2021,MSFT,USD,0.62,5,dividend,0.00 17/11/2021 MSFT USD 0.62 5 dividend 0.00
4 01.01.2022,Penthouse Apartment,USD,500000.0,1,<invalid>,0.00 01.01.2022 Penthouse Apartment USD 500000.0 1 <invalid> 0.00
5 20500606,MSFT,USD,0.00,0,buy,0.00 20500606 MSFT USD 0.00 0 buy 0.00

View File

@ -1,6 +1,6 @@
Date,Code,DataSource,Currency,Price,Quantity,Action,Fee,Note Date,Code,DataSource,Currency,Price,Quantity,Action,Fee,Note
01-09-2021,Account Opening Fee,MANUAL,USD,0,0,fee,49, 01-09-2021,Account Opening Fee,MANUAL,USD,0,0,fee,49,
16-09-2021,MSFT,YAHOO,USD,298.580,5,buy,19.00,My first order 🤓 16-09-2021,MSFT,YAHOO,USD,298.580,5,buy,19.00,My first order 🤓
17/11/2021,MSFT,YAHOO,USD,0.62,5,dividend,0.00 17/11/2021,MSFT,YAHOO,USD,0.62,5,dividend,0.00,
01.01.2022,Penthouse Apartment,MANUAL,USD,500000.0,1,item,0.00 01.01.2022,Penthouse Apartment,MANUAL,USD,500000.0,1,item,0.00,
20500606,MSFT,YAHOO,USD,0.00,0,buy,0.00 20500606,US5949181045,YAHOO,USD,0.00,0,buy,0.00,

1 Date,Code,DataSource,Currency,Price,Quantity,Action,Fee,Note Date Code DataSource Currency Price Quantity Action Fee Note
2 01-09-2021,Account Opening Fee,MANUAL,USD,0,0,fee,49, 01-09-2021 Account Opening Fee MANUAL USD 0 0 fee 49
3 16-09-2021,MSFT,YAHOO,USD,298.580,5,buy,19.00,My first order 🤓 16-09-2021 MSFT YAHOO USD 298.580 5 buy 19.00 My first order 🤓
4 17/11/2021,MSFT,YAHOO,USD,0.62,5,dividend,0.00 17/11/2021 MSFT YAHOO USD 0.62 5 dividend 0.00
5 01.01.2022,Penthouse Apartment,MANUAL,USD,500000.0,1,item,0.00 01.01.2022 Penthouse Apartment MANUAL USD 500000.0 1 item 0.00
6 20500606,MSFT,YAHOO,USD,0.00,0,buy,0.00 20500606 US5949181045 YAHOO USD 0.00 0 buy 0.00

View File

@ -24,7 +24,7 @@
"currency": "USD", "currency": "USD",
"dataSource": "YAHOO", "dataSource": "YAHOO",
"date": "2050-06-05T22:00:00.000Z", "date": "2050-06-05T22:00:00.000Z",
"symbol": "MSFT" "symbol": "US5949181045"
}, },
{ {
"accountId": null, "accountId": null,

415
yarn.lock
View File

@ -20,12 +20,12 @@
"@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9" "@jridgewell/trace-mapping" "^0.3.9"
"@angular-devkit/architect@0.1602.0": "@angular-devkit/architect@0.1602.9":
version "0.1602.0" version "0.1602.9"
resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1602.0.tgz#941996f8afbad9d46134618904a89b13dd7388fb" resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1602.9.tgz#37a3f558c244abff815decf134b10a976104f47d"
integrity sha512-ZRmUTBeD+uGr605eOHnsovEn6f1mOBI+kxP64DRvagNweX5TN04s3iyQ8jmLSAHQD9ush31LFxv3dVNxv3ceXQ== integrity sha512-U3vfb/e2sFfg0D9FyyRBXRPP7g4FBFtGK8Q3JPmvAVsHHwi5AUFRNR7YBChB/T5TMNY077HcTyEirVh2FeUpdA==
dependencies: dependencies:
"@angular-devkit/core" "16.2.0" "@angular-devkit/core" "16.2.9"
rxjs "7.8.1" rxjs "7.8.1"
"@angular-devkit/architect@^0.1600.0-next.6": "@angular-devkit/architect@^0.1600.0-next.6":
@ -36,15 +36,15 @@
"@angular-devkit/core" "16.0.6" "@angular-devkit/core" "16.0.6"
rxjs "7.8.1" rxjs "7.8.1"
"@angular-devkit/build-angular@16.2.0": "@angular-devkit/build-angular@16.2.9":
version "16.2.0" version "16.2.9"
resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-16.2.0.tgz#196c66813e15ff53c7f89cfef7662593cdc1d6b4" resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-16.2.9.tgz#bc81c385b590d63b5174cc010adcda58d39f9939"
integrity sha512-miylwjOqvlKmYrzS84bjRaJrecZxOXH9xsPVvQE8VBe8UKePJjRAL6yyOqXUOGtzlch2YmT98RAnuni7y0FEAw== integrity sha512-S1C4UYxRVyNt3C0wCxbT2jZ1dN5i37kS0mol3PQjbR8gQ0GQzHmzhjTBl1oImo8aouET9yhrk9etk65oat4mBQ==
dependencies: dependencies:
"@ampproject/remapping" "2.2.1" "@ampproject/remapping" "2.2.1"
"@angular-devkit/architect" "0.1602.0" "@angular-devkit/architect" "0.1602.9"
"@angular-devkit/build-webpack" "0.1602.0" "@angular-devkit/build-webpack" "0.1602.9"
"@angular-devkit/core" "16.2.0" "@angular-devkit/core" "16.2.9"
"@babel/core" "7.22.9" "@babel/core" "7.22.9"
"@babel/generator" "7.22.9" "@babel/generator" "7.22.9"
"@babel/helper-annotate-as-pure" "7.22.5" "@babel/helper-annotate-as-pure" "7.22.5"
@ -56,7 +56,7 @@
"@babel/runtime" "7.22.6" "@babel/runtime" "7.22.6"
"@babel/template" "7.22.5" "@babel/template" "7.22.5"
"@discoveryjs/json-ext" "0.5.7" "@discoveryjs/json-ext" "0.5.7"
"@ngtools/webpack" "16.2.0" "@ngtools/webpack" "16.2.9"
"@vitejs/plugin-basic-ssl" "1.0.1" "@vitejs/plugin-basic-ssl" "1.0.1"
ansi-colors "4.1.3" ansi-colors "4.1.3"
autoprefixer "10.4.14" autoprefixer "10.4.14"
@ -86,7 +86,7 @@
parse5-html-rewriting-stream "7.0.0" parse5-html-rewriting-stream "7.0.0"
picomatch "2.3.1" picomatch "2.3.1"
piscina "4.0.0" piscina "4.0.0"
postcss "8.4.27" postcss "8.4.31"
postcss-loader "7.3.3" postcss-loader "7.3.3"
resolve-url-loader "5.0.0" resolve-url-loader "5.0.0"
rxjs "7.8.1" rxjs "7.8.1"
@ -108,12 +108,12 @@
optionalDependencies: optionalDependencies:
esbuild "0.18.17" esbuild "0.18.17"
"@angular-devkit/build-webpack@0.1602.0": "@angular-devkit/build-webpack@0.1602.9":
version "0.1602.0" version "0.1602.9"
resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1602.0.tgz#417ec43b435c19b630c0c734d8d91e29889784e8" resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1602.9.tgz#1ee62fae04b47473a029d7ab87f2bb64a8f3b76f"
integrity sha512-KdSr6iAcO30i/LIGL8mYi+d1buVXuDCp2dptzEJ4vxReOMFJca90KLwb+tVHEqqnDb0WkNfWm8Ii2QYh2FrNyA== integrity sha512-+3IxovfBPR2Vy730mGa0SVKkd5LQVom85gjXOs7WcnnnZmfc1q/BtFlqTgW1UWvTxP8IQdm7UYWVclQfL/WExw==
dependencies: dependencies:
"@angular-devkit/architect" "0.1602.0" "@angular-devkit/architect" "0.1602.9"
rxjs "7.8.1" rxjs "7.8.1"
"@angular-devkit/core@16.0.1": "@angular-devkit/core@16.0.1":
@ -172,6 +172,18 @@
rxjs "7.8.1" rxjs "7.8.1"
source-map "0.7.4" source-map "0.7.4"
"@angular-devkit/core@16.2.9":
version "16.2.9"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.9.tgz#81c5c95de8c423634bf93f616683045c6cdd4dd0"
integrity sha512-dcHWjHBNGm3yCeNz19y8A1At4KgyC6XHNnbFL0y+nnZYiaESXjUoXJYKASedI6A+Bpl0HNq2URhH6bL6Af3+4w==
dependencies:
ajv "8.12.0"
ajv-formats "2.1.1"
jsonc-parser "3.2.0"
picomatch "2.3.1"
rxjs "7.8.1"
source-map "0.7.4"
"@angular-devkit/schematics@16.0.1": "@angular-devkit/schematics@16.0.1":
version "16.0.1" version "16.0.1"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.0.1.tgz#d49387e9e41c9cce98b155da51b0e193333dd178" resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.0.1.tgz#d49387e9e41c9cce98b155da51b0e193333dd178"
@ -216,72 +228,83 @@
ora "5.4.1" ora "5.4.1"
rxjs "7.8.1" rxjs "7.8.1"
"@angular-eslint/bundled-angular-compiler@16.1.0": "@angular-devkit/schematics@16.2.9":
version "16.1.0" version "16.2.9"
resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.1.0.tgz#59fd1ff6423b02d6fa7eeb9ea30581a839471f2c" resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.2.9.tgz#71eed819c1665068d717d75f912f5ea689c201f9"
integrity sha512-5EFAWXuFJADr3imo/ZYshY8s0K7U7wyysnE2LXnpT9PAi5rmkzt70UNZNRuamCbXr4tdIiu+fXWOj7tUuJKnnw== integrity sha512-lB51CGCILpcSI37CwKUAGDLxMqh7zmuRbiPo9s9mSkCM4ccqxFlaL+VFTq2/laneARD6aikpOHnkVm5myNzQPw==
"@angular-eslint/eslint-plugin-template@16.1.0":
version "16.1.0"
resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-16.1.0.tgz#3d88fba2baff4debf2d332fc3d2eea53a32b4efe"
integrity sha512-wQHWR5vqWGgO7mqoG5ixXeplIlz/OmxBJE9QMLPTZE8GdaTx8+F/5J37OWh84zCpD3mOa/FHYZxBDm2MfUmA1Q==
dependencies: dependencies:
"@angular-eslint/bundled-angular-compiler" "16.1.0" "@angular-devkit/core" "16.2.9"
"@angular-eslint/utils" "16.1.0" jsonc-parser "3.2.0"
magic-string "0.30.1"
ora "5.4.1"
rxjs "7.8.1"
"@angular-eslint/bundled-angular-compiler@16.2.0":
version "16.2.0"
resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.2.0.tgz#09d0637d738850a2c6f0523f19632e992f790102"
integrity sha512-ct9orDYxkMl2+uvM7UBfgV28Dq57V4dEs+Drh7cD673JIMa6sXbgmd0QEtm8W3cmyK/jcTzmuoufxbH7hOxd6g==
"@angular-eslint/eslint-plugin-template@16.2.0":
version "16.2.0"
resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-16.2.0.tgz#5d1dd0f450020c9bc8d9cbd5fcbf173b15ff3bd3"
integrity sha512-YFdQ6hHX6NlQj0lfogZwfyKjU8pqkJU+Zsk0ehjlXP8VfKFVmDeQT5/Xr6Df9C8pveC3hvq6Jgd8vo67S9Enxg==
dependencies:
"@angular-eslint/bundled-angular-compiler" "16.2.0"
"@angular-eslint/utils" "16.2.0"
"@typescript-eslint/type-utils" "5.62.0" "@typescript-eslint/type-utils" "5.62.0"
"@typescript-eslint/utils" "5.62.0" "@typescript-eslint/utils" "5.62.0"
aria-query "5.3.0" aria-query "5.3.0"
axobject-query "3.1.1" axobject-query "3.2.1"
"@angular-eslint/eslint-plugin@16.1.0": "@angular-eslint/eslint-plugin@16.2.0":
version "16.1.0" version "16.2.0"
resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin/-/eslint-plugin-16.1.0.tgz#23492eaad1d44dd90793cf0534c7177a028af226" resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin/-/eslint-plugin-16.2.0.tgz#2d61d087d208f347c9c472ecd9b0eee1fae1b21b"
integrity sha512-BFzzJJlgQgWc8avdSBkaDWAzNSUqcwWy0L1iZSBdXGoIOxj72kLbwe99emb8M+rUfCveljQkeM2pcYu8XLbJIA== integrity sha512-zdiAIox1T+B71HL+A8m+1jWdU34nvPGLhCRw/uZNwHzknsF4tYzNQ9W7T/SC/g/2s1yT2yNosEVNJSGSFvunJg==
dependencies: dependencies:
"@angular-eslint/utils" "16.1.0" "@angular-eslint/utils" "16.2.0"
"@typescript-eslint/utils" "5.62.0" "@typescript-eslint/utils" "5.62.0"
"@angular-eslint/template-parser@16.1.0": "@angular-eslint/template-parser@16.2.0":
version "16.1.0" version "16.2.0"
resolved "https://registry.yarnpkg.com/@angular-eslint/template-parser/-/template-parser-16.1.0.tgz#c919c26aa1154b88d1403f4b8e657613c29fe3cf" resolved "https://registry.yarnpkg.com/@angular-eslint/template-parser/-/template-parser-16.2.0.tgz#eccd1a2424b001a585107ec4db8eda726bdc9a6d"
integrity sha512-DOQtzVehtbO7+BQ+FMOXRsxGRjHb3ve6M+S4qASKTiI+twtONjRODcHezD3N4PDkjpKPbOnk7YnFsHur5csUNw== integrity sha512-v2jVKTy2wN7iM9nHpBkxLn2wfL8jSl4IlPrXThIqj8No2VHtpLQZPKuXbGPUXQX05VS2Mj5feScQ36ZVGS8Rbw==
dependencies: dependencies:
"@angular-eslint/bundled-angular-compiler" "16.1.0" "@angular-eslint/bundled-angular-compiler" "16.2.0"
eslint-scope "^7.0.0" eslint-scope "^7.0.0"
"@angular-eslint/utils@16.1.0": "@angular-eslint/utils@16.2.0":
version "16.1.0" version "16.2.0"
resolved "https://registry.yarnpkg.com/@angular-eslint/utils/-/utils-16.1.0.tgz#46e6aafc8b4ca0f6e86cca9ec36f61034f984974" resolved "https://registry.yarnpkg.com/@angular-eslint/utils/-/utils-16.2.0.tgz#8e6f0c372200049b22bca37e5537034b6f8618d2"
integrity sha512-u5XscYUq1F/7RuwyVIV2a280QL27lyQz434VYR+Np/oO21NGj5jxoRKb55xhXT9EFVs5Sy4JYeEUp6S75J/cUw== integrity sha512-NxMRwnlIgzmbJQfWkfd9y3Sz0hzjFdK5LH44i+3D5NhpPdZ6SzwHAjMYWoYsmmNQX5tlDXoicYF9Mz9Wz8DJ/A==
dependencies: dependencies:
"@angular-eslint/bundled-angular-compiler" "16.1.0" "@angular-eslint/bundled-angular-compiler" "16.2.0"
"@typescript-eslint/utils" "5.62.0" "@typescript-eslint/utils" "5.62.0"
"@angular/animations@16.2.1": "@angular/animations@16.2.12":
version "16.2.1" version "16.2.12"
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-16.2.1.tgz#cf50ebcedb63d4f043ed529042aa74aec6ce02aa" resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-16.2.12.tgz#27744d8176e09e70e0f6d837c3abcfcee843a936"
integrity sha512-XVabK9fRKJaYPhW5wn8ySL4KL45N5Np+xOssWhLPDRDBdZjl62MExfpvMkamdkos6E1n1IGsy9wSemjnR4WKhg== integrity sha512-MD0ElviEfAJY8qMOd6/jjSSvtqER2RDAi0lxe6EtUacC1DHCYkaPrKW4vLqY+tmZBg1yf+6n+uS77pXcHHcA3w==
dependencies: dependencies:
tslib "^2.3.0" tslib "^2.3.0"
"@angular/cdk@16.2.1": "@angular/cdk@16.2.11":
version "16.2.1" version "16.2.11"
resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-16.2.1.tgz#f191e16d36881a9d53e0ba461104457c68f36324" resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-16.2.11.tgz#5ca115142947c09c06c2dac17e64abd7eb272fbc"
integrity sha512-rRVdAdfuQ34Eq7na/q2SIO6Me2p/rtU2zeQOW6wrNf6KJfWSTbU6RvNw09cDygAQLp/WmwQvWLhkjWNWGDSf0w== integrity sha512-FcJ9xd9ptjULdScnBNg7YkVnY9NKePFfmvvs2zt841Hd489L8BUkTUdbvtCLhMJTTSN+k+D+RYFhevZuhPKVVg==
dependencies: dependencies:
tslib "^2.3.0" tslib "^2.3.0"
optionalDependencies: optionalDependencies:
parse5 "^7.1.2" parse5 "^7.1.2"
"@angular/cli@16.2.0": "@angular/cli@16.2.9":
version "16.2.0" version "16.2.9"
resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-16.2.0.tgz#c3ab30c7e177f5a18cc44e8d10c024a15636dc63" resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-16.2.9.tgz#36b5ef29ed9cfc56865befa5b5add6f7d043e21c"
integrity sha512-xT8vJOyw6Rc2364XDW2jHagLgKu7342ktd/lt+c0u6R+AB2XVFMePR7VceLohX9N/vRUsbQ0nVSZr+ru/hA+HA== integrity sha512-wkpV/Ni26LUeDmhee2TPXXEq3feEdZMSG8+nkfUK9kqIcxm0IjI1GLPeiVOX7aQobuKNe2cCAFNwsrXWjj+2og==
dependencies: dependencies:
"@angular-devkit/architect" "0.1602.0" "@angular-devkit/architect" "0.1602.9"
"@angular-devkit/core" "16.2.0" "@angular-devkit/core" "16.2.9"
"@angular-devkit/schematics" "16.2.0" "@angular-devkit/schematics" "16.2.9"
"@schematics/angular" "16.2.0" "@schematics/angular" "16.2.9"
"@yarnpkg/lockfile" "1.1.0" "@yarnpkg/lockfile" "1.1.0"
ansi-colors "4.1.3" ansi-colors "4.1.3"
ini "4.1.1" ini "4.1.1"
@ -297,19 +320,19 @@
symbol-observable "4.0.0" symbol-observable "4.0.0"
yargs "17.7.2" yargs "17.7.2"
"@angular/common@16.2.1": "@angular/common@16.2.12":
version "16.2.1" version "16.2.12"
resolved "https://registry.yarnpkg.com/@angular/common/-/common-16.2.1.tgz#ed475c55cf3c21360f6561cdc0794480c7e0d258" resolved "https://registry.yarnpkg.com/@angular/common/-/common-16.2.12.tgz#aa1d1522701833f1998001caa1ac95c3ac11d077"
integrity sha512-druackA5JQpvfS8cD8DFtPRXGRKbhx3mQ778t1n6x3fXpIdGaAX+nSAgAKhIoF7fxWmu0KuHGzb+3BFlZRyTXw== integrity sha512-B+WY/cT2VgEaz9HfJitBmgdk4I333XG/ybC98CMC4Wz8E49T8yzivmmxXB3OD6qvjcOB6ftuicl6WBqLbZNg2w==
dependencies: dependencies:
tslib "^2.3.0" tslib "^2.3.0"
"@angular/compiler-cli@16.2.1": "@angular/compiler-cli@16.2.12":
version "16.2.1" version "16.2.12"
resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-16.2.1.tgz#e55647004d23ff23a3cda547bfdfe01cdde17073" resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-16.2.12.tgz#e24b4bdaf23047b23d7b39e295b7d25b38c5734c"
integrity sha512-A5SyNZTZnXSCL5JVXHKbYj9p2dRYoeFnb6hGQFt2AuCcpUjVIIdwHtre3YzkKe5sFwepPctdoRe2fRXlTfTRjA== integrity sha512-pWSrr152562ujh6lsFZR8NfNc5Ljj+zSTQO44DsuB0tZjwEpnRcjJEgzuhGXr+CoiBf+jTSPZKemtSktDk5aaA==
dependencies: dependencies:
"@babel/core" "7.22.5" "@babel/core" "7.23.2"
"@jridgewell/sourcemap-codec" "^1.4.14" "@jridgewell/sourcemap-codec" "^1.4.14"
chokidar "^3.0.0" chokidar "^3.0.0"
convert-source-map "^1.5.1" convert-source-map "^1.5.1"
@ -318,10 +341,10 @@
tslib "^2.3.0" tslib "^2.3.0"
yargs "^17.2.1" yargs "^17.2.1"
"@angular/compiler@16.2.1": "@angular/compiler@16.2.12":
version "16.2.1" version "16.2.12"
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-16.2.1.tgz#26849828e9292d1833e95bb4e0b41ccbdbbb0b4a" resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-16.2.12.tgz#d13366f190706c270b925495fbc12c29097e6b6c"
integrity sha512-dPauu+ESn79d66U9nBvnunNuBk/UMqnm7iL9Q31J8OKYN/4vrKbsO57pmULOft/GRAYsE3FdLBH0NkocFZKIMQ== integrity sha512-6SMXUgSVekGM7R6l1Z9rCtUGtlg58GFmgbpMCsGf+VXxP468Njw8rjT2YZkf5aEPxEuRpSHhDYjqz7n14cwCXQ==
dependencies: dependencies:
tslib "^2.3.0" tslib "^2.3.0"
@ -330,10 +353,10 @@
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-9.0.0.tgz#87e0bef4c369b6cadae07e3a4295778fc93799d5" resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-9.0.0.tgz#87e0bef4c369b6cadae07e3a4295778fc93799d5"
integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ== integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ==
"@angular/core@16.2.1": "@angular/core@16.2.12":
version "16.2.1" version "16.2.12"
resolved "https://registry.yarnpkg.com/@angular/core/-/core-16.2.1.tgz#a108bcf5bc500753aec03867a495840d72220698" resolved "https://registry.yarnpkg.com/@angular/core/-/core-16.2.12.tgz#f664204275ee5f5eb46bddc0867e7a514731605f"
integrity sha512-Y+0jssQnJPovxMv9cDKYlp6BBHeFBLOHd/+FPv5IIGD1c7NwBP/TImJxCaIV78a57xnO8L0SFacDg/kULzvKrg== integrity sha512-GLLlDeke/NjroaLYOks0uyzFVo6HyLl7VOm0K1QpLXnYvW63W9Ql/T3yguRZa7tRkOAeFZ3jw+1wnBD4O8MoUA==
dependencies: dependencies:
tslib "^2.3.0" tslib "^2.3.0"
@ -342,31 +365,31 @@
resolved "https://registry.yarnpkg.com/@angular/core/-/core-9.0.0.tgz#227dc53e1ac81824f998c6e76000b7efc522641e" resolved "https://registry.yarnpkg.com/@angular/core/-/core-9.0.0.tgz#227dc53e1ac81824f998c6e76000b7efc522641e"
integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w== integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w==
"@angular/forms@16.2.1": "@angular/forms@16.2.12":
version "16.2.1" version "16.2.12"
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-16.2.1.tgz#d9223151cc2f7bad05baaba134bb793c4e5cb4be" resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-16.2.12.tgz#a533ad61a65080281e709ca68840a1da9f189afc"
integrity sha512-cCygiLfBAsVHdtKmNptlk2IgXu0wjRc8kSiiSnJkfK6U/NiNg8ADMiN7iYgKW2TD1ZRw+7dYZV856lxEy2n0+A== integrity sha512-1Eao89hlBgLR3v8tU91vccn21BBKL06WWxl7zLpQmG6Hun+2jrThgOE4Pf3os4fkkbH4Apj0tWL2fNIWe/blbw==
dependencies: dependencies:
tslib "^2.3.0" tslib "^2.3.0"
"@angular/language-service@16.2.1": "@angular/language-service@16.2.12":
version "16.2.1" version "16.2.12"
resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-16.2.1.tgz#700e0dbacbfd302d7d855e83082348faf56c33c2" resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-16.2.12.tgz#e81d9667ec96eac214b0dd54275bdfb835db3f3f"
integrity sha512-B1eFYDUiXlx1xNf4rB1I+ACgD/aUE2M6HPET10FydFgPfzolX/xRdeUGYaAoEje4M9P9a93ovGeTPmg5TAUnLg== integrity sha512-sZwB+ZEjChx9EYcqPaS4OnhC/q5RcedZjIdM9mCxuU/MtseURRYRI/8Hnm1RHo9qyc5PmsQpg7p9Vp/5hXLUjw==
"@angular/localize@16.2.1": "@angular/localize@16.2.12":
version "16.2.1" version "16.2.12"
resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-16.2.1.tgz#f397070b98f291f195089176f8e40d93cd09a4c0" resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-16.2.12.tgz#9e8c5c1d80574800fe159f9216654051a54abe19"
integrity sha512-IMlDEuDNYtVTZ135ATm+YAksdCaFjkOsrtTPu3aIg08Dsyqw7awZ1lEmmmSpiflOqEfPjgHScLWhUMhER70aUg== integrity sha512-sNIHDlZKENPQqx64qGF99g2sOCy9i9O4VOmjKD/FZbeE8O5qBbaQlkwOlFoQIt35/cnvtAtf7oQF6tqmiVtS2w==
dependencies: dependencies:
"@babel/core" "7.22.5" "@babel/core" "7.23.2"
fast-glob "3.3.0" fast-glob "3.3.0"
yargs "^17.2.1" yargs "^17.2.1"
"@angular/material@16.2.1": "@angular/material@16.2.11":
version "16.2.1" version "16.2.11"
resolved "https://registry.yarnpkg.com/@angular/material/-/material-16.2.1.tgz#bbad3fcba9797f101ebcd9283f00c28675bb5d2a" resolved "https://registry.yarnpkg.com/@angular/material/-/material-16.2.11.tgz#ee65e34b5e3d27f7e4bfcceb388a95dd53afaf5e"
integrity sha512-WwjKgYBkZA9EUEOMEFR00ZMFXPs9xLOca3+8njEs/SyeqE0p02H5cnjAaekQfUkcxhwFz1WfJMftI01ODS/S5A== integrity sha512-hrkRD9/38++nIyo3k/KQpxsIaWm+FOJVmoJa83qvwZZt+fHKfT7xaNvRPZ+L2oqFaAvH5ivnL1u1nDuDyWz/0w==
dependencies: dependencies:
"@material/animation" "15.0.0-canary.bc9ae6c9c.0" "@material/animation" "15.0.0-canary.bc9ae6c9c.0"
"@material/auto-init" "15.0.0-canary.bc9ae6c9c.0" "@material/auto-init" "15.0.0-canary.bc9ae6c9c.0"
@ -417,40 +440,40 @@
"@material/typography" "15.0.0-canary.bc9ae6c9c.0" "@material/typography" "15.0.0-canary.bc9ae6c9c.0"
tslib "^2.3.0" tslib "^2.3.0"
"@angular/platform-browser-dynamic@16.2.1": "@angular/platform-browser-dynamic@16.2.12":
version "16.2.1" version "16.2.12"
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.1.tgz#46804d112d210c5317972954a5f3db3ff9275363" resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.12.tgz#14488188c06013eb4153ac6e0603975f8b679f70"
integrity sha512-dKMCSrbD/joOMXM1mhDOKNDZ1BxwO9r9uu5ZxY0L/fWm/ousgMucNikLr38vBudgWM8CN6BuabzkxWKcqi3k4g== integrity sha512-ya54jerNgreCVAR278wZavwjrUWImMr2F8yM5n9HBvsMBbFaAQ83anwbOEiHEF2BlR+gJiEBLfpuPRMw20pHqw==
dependencies: dependencies:
tslib "^2.3.0" tslib "^2.3.0"
"@angular/platform-browser@16.2.1": "@angular/platform-browser@16.2.12":
version "16.2.1" version "16.2.12"
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-16.2.1.tgz#c15de68d4d0675a2247c1e8eb2da11ff20eb8cee" resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-16.2.12.tgz#66b5611066cb3f8bb55f035658e978b50720f3b0"
integrity sha512-SH8zRiRAcw0B5/tVlEc5U/lN5F8g+JizSuu7BQvpCAQEDkM6IjF9LP36Bjav7JuadItbWLfT6peWYa1sJvax2w== integrity sha512-NnH7ju1iirmVEsUq432DTm0nZBGQsBrU40M3ZeVHMQ2subnGiyUs3QyzDz8+VWLL/T5xTxWLt9BkDn65vgzlIQ==
dependencies: dependencies:
tslib "^2.3.0" tslib "^2.3.0"
"@angular/pwa@16.2.0": "@angular/pwa@16.2.9":
version "16.2.0" version "16.2.9"
resolved "https://registry.yarnpkg.com/@angular/pwa/-/pwa-16.2.0.tgz#b8356012f667312e10d856e8663e106757a5d29c" resolved "https://registry.yarnpkg.com/@angular/pwa/-/pwa-16.2.9.tgz#5ed9fe75d9b3b5db41f4ad9bff5188f9d91474c9"
integrity sha512-zZsglQIinUT0OFddJHC4XleauZlPmwMhXY7uUpL4nVo/hANsyF5GuEskiHx3mujU+O4a3QmeLHXzjnOeb5pxHQ== integrity sha512-40YwGZW7HXCtUUeCekcBGd1JwZAJUQEHps4vyJGM8TB51gixBuo61LRcwWbj86BOxzi7PYMmt4PCHDOgOXrZ3A==
dependencies: dependencies:
"@angular-devkit/schematics" "16.2.0" "@angular-devkit/schematics" "16.2.9"
"@schematics/angular" "16.2.0" "@schematics/angular" "16.2.9"
parse5-html-rewriting-stream "7.0.0" parse5-html-rewriting-stream "7.0.0"
"@angular/router@16.2.1": "@angular/router@16.2.12":
version "16.2.1" version "16.2.12"
resolved "https://registry.yarnpkg.com/@angular/router/-/router-16.2.1.tgz#26658d5af1dbce81ac32a3c1ddda93273c11e7eb" resolved "https://registry.yarnpkg.com/@angular/router/-/router-16.2.12.tgz#2f4cae64ddb7f998832aa340dd3f843cfb85cbc8"
integrity sha512-C0WfcktsC25G37unxdH/5I7PbkVBSEB1o+0DJK9/HG97r1yzEkptF6fbRIzDBTS7dX0NfWN/PTAKF0ep7YlHvA== integrity sha512-aU6QnYSza005V9P3W6PpkieL56O0IHps96DjqI1RS8yOJUl3THmokqYN4Fm5+HXy4f390FN9i6ftadYQDKeWmA==
dependencies: dependencies:
tslib "^2.3.0" tslib "^2.3.0"
"@angular/service-worker@16.2.1": "@angular/service-worker@16.2.12":
version "16.2.1" version "16.2.12"
resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-16.2.1.tgz#58c8c7447b82e0afa807b3d4a28f499fd483821c" resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-16.2.12.tgz#359e72693de7d1e8015d1beb02689753ede96de6"
integrity sha512-9AYBYQ19aMQN3AoZgpd4T3qmHVM7nHvjqotSATwwWU/+sbcfdaasdJE4mRP3z6cRbIwYHTbNQJl6pJT/2jDWbw== integrity sha512-o0z0s4c76NmRASa+mUHn/q6vUKQNa06mGmLBDKm84vRQ1sQ2TJv+R1p8K9WkiM5mGy6tjQCDOgaz13TcxMFWOQ==
dependencies: dependencies:
tslib "^2.3.0" tslib "^2.3.0"
@ -486,27 +509,6 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.2.tgz#6a12ced93455827037bfb5ed8492820d60fc32cc" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.2.tgz#6a12ced93455827037bfb5ed8492820d60fc32cc"
integrity sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ== integrity sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==
"@babel/core@7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.5.tgz#d67d9747ecf26ee7ecd3ebae1ee22225fe902a89"
integrity sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==
dependencies:
"@ampproject/remapping" "^2.2.0"
"@babel/code-frame" "^7.22.5"
"@babel/generator" "^7.22.5"
"@babel/helper-compilation-targets" "^7.22.5"
"@babel/helper-module-transforms" "^7.22.5"
"@babel/helpers" "^7.22.5"
"@babel/parser" "^7.22.5"
"@babel/template" "^7.22.5"
"@babel/traverse" "^7.22.5"
"@babel/types" "^7.22.5"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.2"
semver "^6.3.0"
"@babel/core@7.22.9": "@babel/core@7.22.9":
version "7.22.9" version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.9.tgz#bd96492c68822198f33e8a256061da3cf391f58f" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.9.tgz#bd96492c68822198f33e8a256061da3cf391f58f"
@ -528,7 +530,7 @@
json5 "^2.2.2" json5 "^2.2.2"
semver "^6.3.1" semver "^6.3.1"
"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.2.2", "@babel/core@^7.20.2", "@babel/core@^7.22.0", "@babel/core@^7.22.9": "@babel/core@7.23.2", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.2.2", "@babel/core@^7.20.2", "@babel/core@^7.22.0", "@babel/core@^7.22.9":
version "7.23.2" version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.2.tgz#ed10df0d580fff67c5f3ee70fd22e2e4c90a9f94" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.2.tgz#ed10df0d580fff67c5f3ee70fd22e2e4c90a9f94"
integrity sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ== integrity sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==
@ -580,7 +582,7 @@
"@jridgewell/trace-mapping" "^0.3.17" "@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1" jsesc "^2.5.1"
"@babel/generator@^7.21.5", "@babel/generator@^7.22.5", "@babel/generator@^7.22.9", "@babel/generator@^7.23.0", "@babel/generator@^7.7.2": "@babel/generator@^7.21.5", "@babel/generator@^7.22.9", "@babel/generator@^7.23.0", "@babel/generator@^7.7.2":
version "7.23.0" version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420"
integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==
@ -792,7 +794,7 @@
"@babel/template" "^7.22.15" "@babel/template" "^7.22.15"
"@babel/types" "^7.22.19" "@babel/types" "^7.22.19"
"@babel/helpers@^7.21.5", "@babel/helpers@^7.22.5", "@babel/helpers@^7.22.6", "@babel/helpers@^7.23.2": "@babel/helpers@^7.21.5", "@babel/helpers@^7.22.6", "@babel/helpers@^7.23.2":
version "7.23.2" version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.2.tgz#2832549a6e37d484286e15ba36a5330483cac767" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.2.tgz#2832549a6e37d484286e15ba36a5330483cac767"
integrity sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ== integrity sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==
@ -1915,7 +1917,7 @@
"@babel/parser" "^7.22.15" "@babel/parser" "^7.22.15"
"@babel/types" "^7.22.15" "@babel/types" "^7.22.15"
"@babel/traverse@^7.0.0-beta.54", "@babel/traverse@^7.16.0", "@babel/traverse@^7.21.5", "@babel/traverse@^7.22.5", "@babel/traverse@^7.22.8", "@babel/traverse@^7.23.2": "@babel/traverse@^7.0.0-beta.54", "@babel/traverse@^7.16.0", "@babel/traverse@^7.21.5", "@babel/traverse@^7.22.8", "@babel/traverse@^7.23.2":
version "7.23.2" version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8"
integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==
@ -3808,10 +3810,10 @@
dependencies: dependencies:
tslib "2.6.1" tslib "2.6.1"
"@ngtools/webpack@16.2.0": "@ngtools/webpack@16.2.9":
version "16.2.0" version "16.2.9"
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-16.2.0.tgz#b3c2b2668faac35bbcc6c81a4bc016347d141349" resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-16.2.9.tgz#51332974727b48c05b8dd37ce6963cb7bf670890"
integrity sha512-c9jv4r7GnLTpnPOeF+a9yAm/3/2wwl9lMBU32i9hlY+q/Hqde4PiL95bUOLnRRL1I64DV7BFTlSZqSPgDpFXZQ== integrity sha512-rOclD7FfT4OSwVA0nDnULbJS6TORJ0+sQiuT2ebaNFErYr3LOm6Zut05tnmzFw8q1cePrILbG+xpnbggNr9Pyw==
"@nodelib/fs.scandir@2.1.5": "@nodelib/fs.scandir@2.1.5":
version "2.1.5" version "2.1.5"
@ -4677,6 +4679,15 @@
"@angular-devkit/schematics" "16.2.0" "@angular-devkit/schematics" "16.2.0"
jsonc-parser "3.2.0" jsonc-parser "3.2.0"
"@schematics/angular@16.2.9":
version "16.2.9"
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-16.2.9.tgz#0173f714eaf803baa56c93756d3996b9beff4655"
integrity sha512-uiU2YbZRVHgk1N1DDsek/5CKhfpZ8myJYNJk8eHV5LswnXOP3aqvH23VhneaAgOYwK5fISC7eMG0pLVKMvFfZQ==
dependencies:
"@angular-devkit/core" "16.2.9"
"@angular-devkit/schematics" "16.2.9"
jsonc-parser "3.2.0"
"@schematics/angular@^16.0.0-next.6": "@schematics/angular@^16.0.0-next.6":
version "16.2.8" version "16.2.8"
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-16.2.8.tgz#d4c236767e89c536c2c15951394cac20f07bfc1f" resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-16.2.8.tgz#d4c236767e89c536c2c15951394cac20f07bfc1f"
@ -7399,12 +7410,12 @@ axobject-query@2.0.2:
dependencies: dependencies:
ast-types-flow "0.0.7" ast-types-flow "0.0.7"
axobject-query@3.1.1: axobject-query@3.2.1:
version "3.1.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a"
integrity sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg== integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==
dependencies: dependencies:
deep-equal "^2.0.5" dequal "^2.0.3"
babel-core@^7.0.0-bridge.0: babel-core@^7.0.0-bridge.0:
version "7.0.0-bridge.0" version "7.0.0-bridge.0"
@ -9442,30 +9453,6 @@ dedent@^1.0.0:
resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff" resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff"
integrity sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg== integrity sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==
deep-equal@^2.0.5:
version "2.2.2"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.2.tgz#9b2635da569a13ba8e1cc159c2f744071b115daa"
integrity sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==
dependencies:
array-buffer-byte-length "^1.0.0"
call-bind "^1.0.2"
es-get-iterator "^1.1.3"
get-intrinsic "^1.2.1"
is-arguments "^1.1.1"
is-array-buffer "^3.0.2"
is-date-object "^1.0.5"
is-regex "^1.1.4"
is-shared-array-buffer "^1.0.2"
isarray "^2.0.5"
object-is "^1.1.5"
object-keys "^1.1.1"
object.assign "^4.1.4"
regexp.prototype.flags "^1.5.0"
side-channel "^1.0.4"
which-boxed-primitive "^1.0.2"
which-collection "^1.0.1"
which-typed-array "^1.1.9"
deep-is@^0.1.3, deep-is@~0.1.3: deep-is@^0.1.3, deep-is@~0.1.3:
version "0.1.4" version "0.1.4"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
@ -10043,21 +10030,6 @@ es-abstract@^1.22.1:
unbox-primitive "^1.0.2" unbox-primitive "^1.0.2"
which-typed-array "^1.1.13" which-typed-array "^1.1.13"
es-get-iterator@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6"
integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==
dependencies:
call-bind "^1.0.2"
get-intrinsic "^1.1.3"
has-symbols "^1.0.3"
is-arguments "^1.1.1"
is-map "^2.0.2"
is-set "^2.0.2"
is-string "^1.0.7"
isarray "^2.0.5"
stop-iteration-iterator "^1.0.0"
es-module-lexer@^1.2.1: es-module-lexer@^1.2.1:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1"
@ -12308,7 +12280,7 @@ inquirer@^6.2.2:
strip-ansi "^5.1.0" strip-ansi "^5.1.0"
through "^2.3.6" through "^2.3.6"
internal-slot@^1.0.4, internal-slot@^1.0.5: internal-slot@^1.0.5:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.6.tgz#37e756098c4911c5e912b8edbf71ed3aa116f930" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.6.tgz#37e756098c4911c5e912b8edbf71ed3aa116f930"
integrity sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg== integrity sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==
@ -12395,7 +12367,7 @@ is-accessor-descriptor@^1.0.0:
dependencies: dependencies:
kind-of "^6.0.0" kind-of "^6.0.0"
is-arguments@^1.0.4, is-arguments@^1.1.1: is-arguments@^1.0.4:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
@ -12489,7 +12461,7 @@ is-data-descriptor@^1.0.0:
dependencies: dependencies:
kind-of "^6.0.0" kind-of "^6.0.0"
is-date-object@^1.0.1, is-date-object@^1.0.5: is-date-object@^1.0.1:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
@ -12612,11 +12584,6 @@ is-lambda@^1.0.1:
resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5"
integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==
is-map@^2.0.1, is-map@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==
is-nan@^1.3.2: is-nan@^1.3.2:
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
@ -12696,11 +12663,6 @@ is-regex@^1.1.4:
call-bind "^1.0.2" call-bind "^1.0.2"
has-tostringtag "^1.0.0" has-tostringtag "^1.0.0"
is-set@^2.0.1, is-set@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec"
integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==
is-shared-array-buffer@^1.0.2: is-shared-array-buffer@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79"
@ -12749,11 +12711,6 @@ is-unicode-supported@^0.1.0:
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
is-weakmap@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
is-weakref@^1.0.2: is-weakref@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
@ -12761,14 +12718,6 @@ is-weakref@^1.0.2:
dependencies: dependencies:
call-bind "^1.0.2" call-bind "^1.0.2"
is-weakset@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d"
integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==
dependencies:
call-bind "^1.0.2"
get-intrinsic "^1.1.1"
is-what@^3.14.1: is-what@^3.14.1:
version "3.14.1" version "3.14.1"
resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1"
@ -15946,16 +15895,7 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@8.4.27: postcss@8.4.31, postcss@^8.2.14, postcss@^8.4.14, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.24, postcss@^8.4.26:
version "8.4.27"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
dependencies:
nanoid "^3.3.6"
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.2.14, postcss@^8.4.14, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.24, postcss@^8.4.26:
version "8.4.31" version "8.4.31"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
@ -16485,7 +16425,7 @@ regex-parser@^2.2.11:
resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58"
integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==
regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: regexp.prototype.flags@^1.5.1:
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e"
integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==
@ -17491,13 +17431,6 @@ statuses@2.0.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
stop-iteration-iterator@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4"
integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==
dependencies:
internal-slot "^1.0.4"
store2@^2.14.2: store2@^2.14.2:
version "2.14.2" version "2.14.2"
resolved "https://registry.yarnpkg.com/store2/-/store2-2.14.2.tgz#56138d200f9fe5f582ad63bc2704dbc0e4a45068" resolved "https://registry.yarnpkg.com/store2/-/store2-2.14.2.tgz#56138d200f9fe5f582ad63bc2704dbc0e4a45068"
@ -19012,22 +18945,12 @@ which-boxed-primitive@^1.0.2:
is-string "^1.0.5" is-string "^1.0.5"
is-symbol "^1.0.3" is-symbol "^1.0.3"
which-collection@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
dependencies:
is-map "^2.0.1"
is-set "^2.0.1"
is-weakmap "^2.0.1"
is-weakset "^2.0.1"
which-module@^2.0.0: which-module@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
which-typed-array@^1.1.11, which-typed-array@^1.1.13, which-typed-array@^1.1.2, which-typed-array@^1.1.9: which-typed-array@^1.1.11, which-typed-array@^1.1.13, which-typed-array@^1.1.2:
version "1.1.13" version "1.1.13"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.13.tgz#870cd5be06ddb616f504e7b039c4c24898184d36" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.13.tgz#870cd5be06ddb616f504e7b039c4c24898184d36"
integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow== integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==