Browse Source

Merge branch 'main' into fix/enter-key-login-dialog

pull/5751/head
jjs2099 2 weeks ago
committed by unknown
parent
commit
7d1398959b
  1. 183
      CHANGELOG.md
  2. 10
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts
  3. 20
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  4. 21
      apps/api/src/services/data-provider/data-provider.service.ts
  5. 7
      apps/api/src/services/data-provider/errors/asset-profile-invalid.error.ts
  6. 105
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  7. 5
      apps/client/src/app/app-routing.module.ts
  8. 188
      apps/client/src/app/app.component.html
  9. 18
      apps/client/src/app/app.component.scss
  10. 35
      apps/client/src/app/app.component.ts
  11. 7
      apps/client/src/app/app.module.ts
  12. 181
      apps/client/src/app/components/footer/footer.component.html
  13. 16
      apps/client/src/app/components/footer/footer.component.scss
  14. 74
      apps/client/src/app/components/footer/footer.component.ts
  15. 53
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html
  16. 10
      libs/common/src/lib/helper.ts
  17. 3
      libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts
  18. 62
      libs/ui/src/lib/holdings-table/holdings-table.component.stories.ts
  19. 293
      libs/ui/src/lib/mocks/holdings.ts
  20. 351
      libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts
  21. 7
      package-lock.json
  22. 1
      package.json
  23. 17489
      yarn.lock

183
CHANGELOG.md

@ -1,12 +1,189 @@
## 2.198.0 - 2025-10-16
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Fixed
- Make Enter key behave like Sign In in the access token login dialog (#5751)
### Changed
- Disabled zoom in PWA
## 2.208.0 - 2025-10-11
### Added
- Added support for configuring the safe withdrawal rate in the _FIRE_ section (experimental)
### Changed
- Changed the _As seen in_ section on the landing page to an animated carousel
- Refactored `transactionCount` to `activitiesCount` in the endpoint `GET api/v1/portfolio/holding/:dataSource/:symbol`
- Refactored various components to use self-closing tags
- Removed the deprecated endpoint `GET api/v1/portfolio/position/:dataSource/:symbol`
- Removed the deprecated endpoint `PUT api/v1/portfolio/position/:dataSource/:symbol/tags`
- Improved the language localization for German (`de`)
- Upgraded `prisma` from version `6.16.1` to `6.16.3`
### Fixed
- Fixed the server startup message to properly display IPv6 addresses
- Enabled IPv6 connectivity for _Redis_ in the job queue module by setting the address family
- Fixed an issue where importing custom asset profiles failed due to validation errors
## 2.207.0 - 2025-10-08
### Added
- Added support to edit a granted access (experimental)
- Introduced tabs to the asset profile details dialog in the admin control panel
- Added support for a date range query parameter in the data gathering endpoint
- Added a _Storybook_ story for the activities table component
### Changed
- Improved the spacing around the buttons in the holding detail dialog
- Extended the _Storybook_ stories of the accounts table component by a loading state story
- Refactored the auth page to standalone
- Improved the language localization for German (`de`)
### Fixed
- Fixed the word wrap in the menu of the access table component
- Fixed the word wrap in the menu of the activities table component
- Fixed the word wrap in the menu of the asset profile details dialog in the admin control panel
## 2.206.0 - 2025-10-04
### Changed
- Localized the number formatting in the settings dialog to customize the rule thresholds of the _X-ray_ page
- Improved the usability of the assistant by preselecting the first search result
- Improved the usability of the _Cancel_ / _Close_ buttons in the create watchlist item dialog
- Refactored the `fireWealth` from `number` type to a structured object in the summary of the portfolio details endpoint
- Refactored the _Open Startup_ (`/open`) page to standalone
- Refactored the file drop directive to standalone
- Refactored the symbol pipe to standalone
### Fixed
- Fixed the behavior of the 'Login with Access Token' dialog so that pressing 'Enter' performs the same action as clicking 'Sign In'
- Handled an exception in the get asset profile functionality of the _Financial Modeling Prep_ service
- Added the missing `CommonModule` import in the import activities dialog
## 2.205.0 - 2025-10-01
### Changed
- Restricted the selection of the retirement date picker in the _FIRE_ calculator to a future date
- Improved the support for mutual funds in the _Financial Modeling Prep_ service (get asset profiles)
- Improved the language localization for German (`de`)
- Upgraded `prisma` from version `6.16.1` to `6.16.3`
## 2.204.0 - 2025-09-30
### Added
- Added (keydown.enter) handler in the login form template to trigger onLoginWithAccessToken() directly
- Added the safe withdrawal rate to the user settings (experimental)
### Changed
- Improved the number formatting of the y-axis labels in the investment chart component
- Localized the number formatting of the y-axis labels in the line chart component
- Improved the wording of the 4% rule in the _FIRE_ section
- Improved the usability of the create asset profile dialog in the market data section of the admin control panel
- Improved the language localization for German (`de`)
### Fixed
- Improved the table headers’ alignment of the activities table
## 2.203.0 - 2025-09-27
### Added
- Added support for column sorting to the queue jobs table in the admin control panel
- Added a blog post: _Hacktoberfest 2025_
### Changed
- Removed the deprecated `ITEM` activity type
## 2.202.0 - 2025-09-26
### Added
- Added `settings` to the `Access` model
### Changed
- Extended the tags selector component to support form control
- Changed the deprecated `ITEM` activity type to `VALUABLE` in the create or update activity dialog
### Fixed
- Fixed an issue where the save button was not enabled after editing tags in the create or update activity dialog
- Fixed an issue in the investment calculation when selling all units of a holding
## 2.201.0 - 2025-09-24
### Added
- Added the symbol to the benchmark component
- Added the latest activities to the public page (experimental)
- Added pagination to the activities table of the activities import dialog
- Added an option to configure the account column of the activities table component
### Changed
- Hid the account column from the activities table of the account detail dialog to avoid redundant information
- Renamed the show access token dialog component to user account registration dialog component
- Refreshed the cryptocurrencies list
- Improved the language localization for German (`de`)
- Upgraded `countup.js` from version `2.8.2` to `2.9.0`
### Fixed
- Fixed an issue with `unitPriceInAssetProfileCurrency` in the value redaction interceptor for the impersonation mode
## 2.200.0 - 2025-09-17
### Changed
- Refactored the show access token dialog component to standalone
- Upgraded `prisma` from version `6.15.0` to `6.16.1`
### Fixed
- Removed a temporary element from the activities table component
## 2.199.0 - 2025-09-14
### Added
- Extended the content of the performance calculation method by dividends on the Frequently Asked Questions (FAQ) page
- Added a _Storybook_ story for the entity logo image component
### Changed
- Improved the search in the _Yahoo Finance_ service
- Moved the holdings table into the holdings section on the public page
- Migrated to the _Prisma Configuration File_ approach (`prisma.config.ts`)
- Refactored the login with access token dialog component to standalone
- Prefixed the `crypto`, `fs` and `path` imports with `node:`
- Upgraded `yahoo-finance2` from version `3.8.0` to `3.10.0`
### Fixed
- Fixed a pagination issue in the market data endpoint by adding `id` as a secondary sort criterion to ensure consistent ordering in the admin control panel
- Fixed a pagination issue in the user endpoint by adding `id` as a secondary sort criterion to ensure consistent ordering in the admin control panel
## 2.198.0 - 2025-09-11
### Changed

10
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts

@ -1,5 +1,6 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { AssetProfileInvalidError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-invalid.error';
import { parseDate } from '@ghostfolio/common/helper';
import {
DataProviderGhostfolioAssetProfileResponse,
@ -66,7 +67,14 @@ export class GhostfolioController {
});
return assetProfile;
} catch {
} catch (error) {
if (error instanceof AssetProfileInvalidError) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR

20
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts

@ -40,10 +40,7 @@ export class GhostfolioService {
private readonly propertyService: PropertyService
) {}
public async getAssetProfile({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbol
}: GetAssetProfileParams) {
public async getAssetProfile({ symbol }: GetAssetProfileParams) {
let result: DataProviderGhostfolioAssetProfileResponse = {};
try {
@ -51,12 +48,15 @@ export class GhostfolioService {
for (const dataProviderService of this.getDataProviderServices()) {
promises.push(
dataProviderService
.getAssetProfile({
requestTimeout,
symbol
})
.then(async (assetProfile) => {
this.dataProviderService
.getAssetProfiles([
{
symbol,
dataSource: dataProviderService.getName()
}
])
.then(async (assetProfiles) => {
const assetProfile = assetProfiles[symbol];
const dataSourceOrigin = DataSource.GHOSTFOLIO;
if (assetProfile) {

21
apps/api/src/services/data-provider/data-provider.service.ts

@ -35,6 +35,8 @@ import { eachDayOfInterval, format, isValid } from 'date-fns';
import { groupBy, isEmpty, isNumber, uniqWith } from 'lodash';
import ms from 'ms';
import { AssetProfileInvalidError } from './errors/asset-profile-invalid.error';
@Injectable()
export class DataProviderService implements OnModuleInit {
private dataProviderMapping: { [dataProviderName: string]: string };
@ -106,9 +108,9 @@ export class DataProviderService implements OnModuleInit {
);
promises.push(
promise.then((symbolProfile) => {
if (symbolProfile) {
response[symbol] = symbolProfile;
promise.then((assetProfile) => {
if (isCurrency(assetProfile?.currency)) {
response[symbol] = assetProfile;
}
})
);
@ -117,6 +119,12 @@ export class DataProviderService implements OnModuleInit {
try {
await Promise.all(promises);
if (isEmpty(response)) {
throw new AssetProfileInvalidError(
'No valid asset profiles have been found'
);
}
} catch (error) {
Logger.error(error, 'DataProviderService');
@ -645,8 +653,11 @@ export class DataProviderService implements OnModuleInit {
const filteredItems = lookupItems
.filter(({ currency }) => {
// Only allow symbols with supported currency
return currency ? true : false;
if (includeIndices) {
return true;
}
return currency ? isCurrency(currency) : false;
})
.map((lookupItem) => {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {

7
apps/api/src/services/data-provider/errors/asset-profile-invalid.error.ts

@ -0,0 +1,7 @@
export class AssetProfileInvalidError extends Error {
public constructor(message: string) {
super(message);
this.name = 'AssetProfileInvalidError';
}
}

105
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -12,6 +12,7 @@ import {
IDataProviderHistoricalResponse,
IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import {
DEFAULT_CURRENCY,
REPLACE_NAME_PARTS
@ -49,7 +50,8 @@ export class FinancialModelingPrepService implements DataProviderInterface {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly cryptocurrencyService: CryptocurrencyService
private readonly cryptocurrencyService: CryptocurrencyService,
private readonly prismaService: PrismaService
) {
this.apiKey = this.configurationService.get(
'API_KEY_FINANCIAL_MODELING_PREP'
@ -100,6 +102,10 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}
).then((res) => res.json());
if (!assetProfile) {
throw new Error(`${symbol} not found`);
}
const { assetClass, assetSubClass } =
this.parseAssetClass(assetProfile);
@ -220,7 +226,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
public getDataProviderInfo(): DataProviderInfo {
return {
dataSource: DataSource.FINANCIAL_MODELING_PREP,
dataSource: this.getName(),
isPremium: true,
name: 'Financial Modeling Prep',
url: 'https://financialmodelingprep.com/developer/docs'
@ -359,25 +365,57 @@ export class FinancialModelingPrepService implements DataProviderInterface {
[symbol: string]: Pick<SymbolProfile, 'currency'>;
} = {};
const quotes = await fetch(
`${this.getUrl({ version: 'stable' })}/batch-quote-short?symbols=${symbols.join(',')}&apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(requestTimeout)
const [assetProfileResolutions, quotes] = await Promise.all([
this.prismaService.assetProfileResolution.findMany({
where: {
dataSourceTarget: this.getDataProviderInfo().dataSource,
symbolTarget: { in: symbols }
}
}),
fetch(
`${this.getUrl({ version: 'stable' })}/batch-quote-short?symbols=${symbols.join(',')}&apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then(
(res) => res.json() as unknown as { price: number; symbol: string }[]
)
]);
for (const { currency, symbolTarget } of assetProfileResolutions) {
currencyBySymbolMap[symbolTarget] = { currency };
}
const resolvedSymbols = assetProfileResolutions.map(
({ symbolTarget }) => {
return symbolTarget;
}
).then((res) => res.json());
);
const symbolsToFetch = quotes
.map(({ symbol }) => {
return symbol;
})
.filter((symbol) => {
return !resolvedSymbols.includes(symbol);
});
if (symbolsToFetch.length > 0) {
await Promise.all(
symbolsToFetch.map(async (symbol) => {
const assetProfile = await this.getAssetProfile({
requestTimeout,
symbol
});
await Promise.all(
quotes.map(({ symbol }) => {
return this.getAssetProfile({
requestTimeout,
symbol
}).then((assetProfile) => {
if (assetProfile?.currency) {
currencyBySymbolMap[symbol] = { currency: assetProfile.currency };
currencyBySymbolMap[symbol] = {
currency: assetProfile.currency
};
}
});
})
);
})
);
}
for (const { price, symbol } of quotes) {
let marketState: MarketState = 'delayed';
@ -394,7 +432,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
marketState,
currency: currencyBySymbolMap[symbol]?.currency,
dataProviderInfo: this.getDataProviderInfo(),
dataSource: DataSource.FINANCIAL_MODELING_PREP,
dataSource: this.getDataProviderInfo().dataSource,
marketPrice: price
};
}
@ -420,6 +458,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}
public async search({
includeIndices = false,
query,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: GetSearchParams): Promise<LookupResponse> {
@ -466,17 +505,25 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}
).then((res) => res.json());
items = result.map(({ currency, name, symbol }) => {
return {
currency,
symbol,
assetClass: undefined, // TODO
assetSubClass: undefined, // TODO
dataProviderInfo: this.getDataProviderInfo(),
dataSource: this.getName(),
name: this.formatName({ name })
};
});
items = result
.filter(({ symbol }) => {
if (includeIndices === false && symbol.startsWith('^')) {
return false;
}
return true;
})
.map(({ currency, name, symbol }) => {
return {
currency,
symbol,
assetClass: undefined, // TODO
assetSubClass: undefined, // TODO
dataProviderInfo: this.getDataProviderInfo(),
dataSource: this.getName(),
name: this.formatName({ name })
};
});
}
} catch (error) {
let message = error;

5
apps/client/src/app/app-routing.module.ts

@ -155,8 +155,9 @@ const routes: Routes = [
// Preload all lazy loaded modules with the attribute preload === true
{
anchorScrolling: 'enabled',
preloadingStrategy: ModulePreloadService
// enableTracing: true // <-- debugging purposes only
// enableTracing: true, // <-- debugging purposes only
preloadingStrategy: ModulePreloadService,
scrollPositionRestoration: 'top'
}
)
],

188
apps/client/src/app/app.component.html

@ -11,8 +11,8 @@
>
<span i18n>You are using the Live Demo.</span>
<span class="a ml-2 p-1" i18n>Create Account</span>
</div></a
>
</div>
</a>
}
@if (!canCreateAccount && user?.systemMessage) {
<div
@ -47,187 +47,7 @@
</main>
@if (showFooter) {
<footer class="justify-content-center overflow-hidden py-4 w-100">
<div class="container">
<div class="mb-3 row">
<div class="col-sm">
<a [routerLink]="['/']"><gf-logo /></a>
</div>
<div class="col-sm">
<div class="h6 mt-2" i18n>Personal Finance</div>
<ul class="list-unstyled">
@if (hasPermissionToAccessFearAndGreedIndex) {
<li>
<a i18n [routerLink]="routerLinkMarkets">Markets</a>
</li>
}
<li><a i18n [routerLink]="routerLinkResources">Resources</a></li>
</ul>
</div>
<div class="col-sm">
<div class="h6 mt-2">Ghostfolio</div>
<ul class="list-unstyled">
<li><a i18n [routerLink]="routerLinkAbout">About</a></li>
@if (hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
}
<li>
<a i18n [routerLink]="routerLinkAboutChangelog">Changelog</a>
</li>
<li><a i18n [routerLink]="routerLinkFeatures">Features</a></li>
@if (hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="routerLinkFaq"
>Frequently Asked Questions (FAQ)</a
>
</li>
}
@if (!hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="routerLinkAboutLicense">License</a>
</li>
}
@if (hasPermissionForStatistics) {
<li>
<a [routerLink]="routerLinkOpenStartup">Open Startup</a>
</li>
}
@if (hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="routerLinkPricing">Pricing</a>
</li>
}
@if (hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="routerLinkAboutPrivacyPolicy"
>Privacy Policy</a
>
</li>
}
@if (hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="routerLinkAboutTermsOfService"
>Terms of Service</a
>
</li>
}
@if (hasPermissionForSubscription) {
<li>
<a
class="align-items-baseline d-flex"
href="https://status.ghostfol.io"
target="_blank"
title="Ghostfolio Status"
>Status<ion-icon class="ml-1" name="open-outline"
/></a>
</li>
}
</ul>
</div>
<div class="col-sm">
<div class="h6 mt-2" i18n>Community</div>
<ul class="list-unstyled">
<li>
<a
class="align-items-baseline d-flex"
href="https://github.com/ghostfolio/ghostfolio"
target="_blank"
title="Find Ghostfolio on GitHub"
>GitHub<ion-icon class="ml-1" name="open-outline"
/></a>
</li>
<li>
<a
class="align-items-baseline d-flex"
href="https://linkedin.com/company/ghostfolio"
target="_blank"
title="Follow Ghostfolio on LinkedIn"
>LinkedIn<ion-icon class="ml-1" name="open-outline"
/></a>
</li>
<li>
<a
class="align-items-baseline d-flex"
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
target="_blank"
title="Join the Ghostfolio Slack community"
>Slack<ion-icon class="ml-1" name="open-outline"
/></a>
</li>
<li>
<a
class="align-items-baseline d-flex"
href="https://x.com/ghostfolio_"
target="_blank"
title="Follow Ghostfolio on X (formerly Twitter)"
>X (formerly Twitter)<ion-icon class="ml-1" name="open-outline"
/></a>
</li>
<li>&nbsp;</li>
<!--
<li>
<a href="../ca" title="Ghostfolio en català">Català</a>
</li>
-->
<li>
<a href="../zh" title="Ghostfolio in Chinese">Chinese</a>
</li>
<li>
<a href="../de" title="Ghostfolio in Deutsch">Deutsch</a>
</li>
<li>
<a href="../en" title="Ghostfolio in English">English</a>
</li>
<li>
<a href="../es" title="Ghostfolio in Español">Español</a>
</li>
<li>
<a href="../fr" title="Ghostfolio en Français">Français</a>
</li>
<li>
<a href="../it" title="Ghostfolio in Italiano">Italiano</a>
</li>
<li>
<a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a>
</li>
<li>
<a href="../pl" title="Ghostfolio in Polski">Polski</a>
</li>
<li>
<a href="../pt" title="Ghostfolio in Português">Português</a>
</li>
<li>
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
</li>
<!--
<li>
<a href="../uk" title="Ghostfolio in Українська">Українська</a>
</li>
-->
</ul>
</div>
</div>
<div class="mb-2 row text-center">
<div class="col">
© 2021 - {{ currentYear }}
<a href="https://ghostfol.io">Ghostfolio</a>
</div>
</div>
<div class="row text-center text-muted">
<div class="col">
<small class="d-block" i18n
>The risk of loss in trading can be substantial. It is not advisable
to invest money you may need in the short term.</small
>
</div>
</div>
</div>
<div class="container d-none d-md-block mt-5">
<div class="row justify-content-center">
<div class="font-weight-bold line-height-1 logotype">Ghostfolio</div>
</div>
</div>
<footer class="justify-content-center overflow-hidden w-100">
<gf-footer class="py-4" [info]="info" [user]="user" />
</footer>
}

18
apps/client/src/app/app.component.scss

@ -34,18 +34,6 @@
}
}
footer {
background-color: rgba(var(--palette-foreground-text), 0.05);
font-size: 90%;
.logotype {
font-size: 13vw;
letter-spacing: -0.03em;
margin-bottom: -5svw;
opacity: 0.05;
}
}
header {
height: var(--mat-toolbar-standard-height);
}
@ -54,9 +42,3 @@
min-height: calc(100svh - var(--mat-toolbar-standard-height));
}
}
:host-context(.theme-dark) {
footer {
background-color: rgba(var(--palette-foreground-text-dark), 0.05);
}
}

35
apps/client/src/app/app.component.ts

@ -52,36 +52,16 @@ export class AppComponent implements OnDestroy, OnInit {
public canCreateAccount: boolean;
public currentRoute: string;
public currentSubRoute: string;
public currentYear = new Date().getFullYear();
public deviceType: string;
public hasImpersonationId: boolean;
public hasInfoMessage: boolean;
public hasPermissionForStatistics: boolean;
public hasPermissionForSubscription: boolean;
public hasPermissionToAccessFearAndGreedIndex: boolean;
public hasPermissionToChangeDateRange: boolean;
public hasPermissionToChangeFilters: boolean;
public hasPromotion = false;
public hasTabs = false;
public info: InfoItem;
public pageTitle: string;
public routerLinkAbout = publicRoutes.about.routerLink;
public routerLinkAboutChangelog =
publicRoutes.about.subRoutes.changelog.routerLink;
public routerLinkAboutLicense =
publicRoutes.about.subRoutes.license.routerLink;
public routerLinkAboutPrivacyPolicy =
publicRoutes.about.subRoutes.privacyPolicy.routerLink;
public routerLinkAboutTermsOfService =
publicRoutes.about.subRoutes.termsOfService.routerLink;
public routerLinkBlog = publicRoutes.blog.routerLink;
public routerLinkFaq = publicRoutes.faq.routerLink;
public routerLinkFeatures = publicRoutes.features.routerLink;
public routerLinkMarkets = publicRoutes.markets.routerLink;
public routerLinkOpenStartup = publicRoutes.openStartup.routerLink;
public routerLinkPricing = publicRoutes.pricing.routerLink;
public routerLinkRegister = publicRoutes.register.routerLink;
public routerLinkResources = publicRoutes.resources.routerLink;
public showFooter = false;
public user: User;
@ -126,21 +106,6 @@ export class AppComponent implements OnDestroy, OnInit {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.info = this.dataService.fetchInfo();
this.hasPermissionForSubscription = hasPermission(
this.info?.globalPermissions,
permissions.enableSubscription
);
this.hasPermissionForStatistics = hasPermission(
this.info?.globalPermissions,
permissions.enableStatistics
);
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.info?.globalPermissions,
permissions.enableFearAndGreedIndex
);
this.hasPromotion =
!!this.info?.subscriptionOffer?.coupon ||
!!this.info?.subscriptionOffer?.durationExtension;

7
apps/client/src/app/app.module.ts

@ -1,5 +1,3 @@
import { GfLogoComponent } from '@ghostfolio/ui/logo';
import { Platform } from '@angular/cdk/platform';
import {
provideHttpClient,
@ -20,7 +18,6 @@ import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ServiceWorkerModule } from '@angular/service-worker';
import { provideIonicAngular } from '@ionic/angular/standalone';
import { IonIcon } from '@ionic/angular/standalone';
import { provideMarkdown } from 'ngx-markdown';
import { provideNgxSkeletonLoader } from 'ngx-skeleton-loader';
import { NgxStripeModule, STRIPE_PUBLISHABLE_KEY } from 'ngx-stripe';
@ -30,6 +27,7 @@ import { CustomDateAdapter } from './adapter/custom-date-adapter';
import { DateFormats } from './adapter/date-formats';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { GfFooterComponent } from './components/footer/footer.component';
import { GfHeaderComponent } from './components/header/header.component';
import { authInterceptorProviders } from './core/auth.interceptor';
import { httpResponseInterceptorProviders } from './core/http-response.interceptor';
@ -47,10 +45,9 @@ export function NgxStripeFactory(): string {
AppRoutingModule,
BrowserAnimationsModule,
BrowserModule,
GfFooterComponent,
GfHeaderComponent,
GfLogoComponent,
GfNotificationModule,
IonIcon,
MatAutocompleteModule,
MatChipsModule,
MatNativeDateModule,

181
apps/client/src/app/components/footer/footer.component.html

@ -0,0 +1,181 @@
<div class="container">
<div class="mb-3 row">
<div class="col-sm">
<a [routerLink]="['/']"><gf-logo /></a>
</div>
<div class="col-sm">
<div class="h6 mt-2" i18n>Personal Finance</div>
<ul class="list-unstyled">
@if (hasPermissionToAccessFearAndGreedIndex) {
<li>
<a i18n [routerLink]="routerLinkMarkets">Markets</a>
</li>
}
<li><a i18n [routerLink]="routerLinkResources">Resources</a></li>
</ul>
</div>
<div class="col-sm">
<div class="h6 mt-2">Ghostfolio</div>
<ul class="list-unstyled">
<li><a i18n [routerLink]="routerLinkAbout">About</a></li>
@if (hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
}
<li>
<a i18n [routerLink]="routerLinkAboutChangelog">Changelog</a>
</li>
<li><a i18n [routerLink]="routerLinkFeatures">Features</a></li>
@if (hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="routerLinkFaq"
>Frequently Asked Questions (FAQ)</a
>
</li>
}
@if (!hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="routerLinkAboutLicense">License</a>
</li>
}
@if (hasPermissionForStatistics) {
<li>
<a [routerLink]="routerLinkOpenStartup">Open Startup</a>
</li>
}
@if (hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="routerLinkPricing">Pricing</a>
</li>
}
@if (hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="routerLinkAboutPrivacyPolicy"
>Privacy Policy</a
>
</li>
}
@if (hasPermissionForSubscription) {
<li>
<a i18n [routerLink]="routerLinkAboutTermsOfService"
>Terms of Service</a
>
</li>
}
@if (hasPermissionForSubscription) {
<li>
<a
class="align-items-baseline d-flex"
href="https://status.ghostfol.io"
target="_blank"
title="Ghostfolio Status"
>Status<ion-icon class="ml-1" name="open-outline"
/></a>
</li>
}
</ul>
</div>
<div class="col-sm">
<div class="h6 mt-2" i18n>Community</div>
<ul class="list-unstyled">
<li>
<a
class="align-items-baseline d-flex"
href="https://github.com/ghostfolio/ghostfolio"
target="_blank"
title="Find Ghostfolio on GitHub"
>GitHub<ion-icon class="ml-1" name="open-outline"
/></a>
</li>
<li>
<a
class="align-items-baseline d-flex"
href="https://linkedin.com/company/ghostfolio"
target="_blank"
title="Follow Ghostfolio on LinkedIn"
>LinkedIn<ion-icon class="ml-1" name="open-outline"
/></a>
</li>
<li>
<a
class="align-items-baseline d-flex"
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
target="_blank"
title="Join the Ghostfolio Slack community"
>Slack<ion-icon class="ml-1" name="open-outline"
/></a>
</li>
<li>
<a
class="align-items-baseline d-flex"
href="https://x.com/ghostfolio_"
target="_blank"
title="Follow Ghostfolio on X (formerly Twitter)"
>X (formerly Twitter)<ion-icon class="ml-1" name="open-outline"
/></a>
</li>
<li>&nbsp;</li>
<!--
<li>
<a href="../ca" title="Ghostfolio en català">Català</a>
</li>
-->
<li>
<a href="../zh" title="Ghostfolio in Chinese">Chinese</a>
</li>
<li>
<a href="../de" title="Ghostfolio in Deutsch">Deutsch</a>
</li>
<li>
<a href="../en" title="Ghostfolio in English">English</a>
</li>
<li>
<a href="../es" title="Ghostfolio in Español">Español</a>
</li>
<li>
<a href="../fr" title="Ghostfolio en Français">Français</a>
</li>
<li>
<a href="../it" title="Ghostfolio in Italiano">Italiano</a>
</li>
<li>
<a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a>
</li>
<li>
<a href="../pl" title="Ghostfolio in Polski">Polski</a>
</li>
<li>
<a href="../pt" title="Ghostfolio in Português">Português</a>
</li>
<li>
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
</li>
<!--
<li>
<a href="../uk" title="Ghostfolio in Українська">Українська</a>
</li>
-->
</ul>
</div>
</div>
<div class="mb-2 row text-center">
<div class="col">
© 2021 - {{ currentYear }}
<a href="https://ghostfol.io">Ghostfolio</a>
</div>
</div>
<div class="row text-center text-muted">
<div class="col">
<small class="d-block" i18n
>The risk of loss in trading can be substantial. It is not advisable to
invest money you may need in the short term.</small
>
</div>
</div>
</div>
<div class="container d-none d-md-block mt-5">
<div class="row justify-content-center">
<div class="font-weight-bold line-height-1 logotype">Ghostfolio</div>
</div>
</div>

16
apps/client/src/app/components/footer/footer.component.scss

@ -0,0 +1,16 @@
:host {
background-color: rgba(var(--palette-foreground-text), 0.05);
display: block;
font-size: 90%;
.logotype {
font-size: 13vw;
letter-spacing: -0.03em;
margin-bottom: -5svw;
opacity: 0.05;
}
}
:host-context(.theme-dark) {
background-color: rgba(var(--palette-foreground-text-dark), 0.05);
}

74
apps/client/src/app/components/footer/footer.component.ts

@ -0,0 +1,74 @@
import { InfoItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { GfLogoComponent } from '@ghostfolio/ui/logo';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
CUSTOM_ELEMENTS_SCHEMA,
Input,
OnChanges
} from '@angular/core';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { openOutline } from 'ionicons/icons';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, GfLogoComponent, IonIcon, RouterModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-footer',
styleUrls: ['./footer.component.scss'],
templateUrl: './footer.component.html'
})
export class GfFooterComponent implements OnChanges {
@Input() public info: InfoItem;
@Input() public user: User;
public currentYear = new Date().getFullYear();
public hasPermissionForStatistics: boolean;
public hasPermissionForSubscription: boolean;
public hasPermissionToAccessFearAndGreedIndex: boolean;
public routerLinkAbout = publicRoutes.about.routerLink;
public routerLinkAboutChangelog =
publicRoutes.about.subRoutes.changelog.routerLink;
public routerLinkAboutLicense =
publicRoutes.about.subRoutes.license.routerLink;
public routerLinkAboutPrivacyPolicy =
publicRoutes.about.subRoutes.privacyPolicy.routerLink;
public routerLinkAboutTermsOfService =
publicRoutes.about.subRoutes.termsOfService.routerLink;
public routerLinkBlog = publicRoutes.blog.routerLink;
public routerLinkFaq = publicRoutes.faq.routerLink;
public routerLinkFeatures = publicRoutes.features.routerLink;
public routerLinkMarkets = publicRoutes.markets.routerLink;
public routerLinkOpenStartup = publicRoutes.openStartup.routerLink;
public routerLinkPricing = publicRoutes.pricing.routerLink;
public routerLinkResources = publicRoutes.resources.routerLink;
public constructor() {
addIcons({
openOutline
});
}
public ngOnChanges() {
this.hasPermissionForStatistics = hasPermission(
this.info?.globalPermissions,
permissions.enableStatistics
);
this.hasPermissionForSubscription = hasPermission(
this.info?.globalPermissions,
permissions.enableSubscription
);
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.info?.globalPermissions,
permissions.enableFearAndGreedIndex
);
}
}

53
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html

@ -23,32 +23,33 @@
</button>
</mat-form-field>
@if (data.hasPermissionToUseSocialLogin) {
<div class="my-3 text-center text-muted" i18n>or</div>
<div class="d-flex flex-column">
<button
class="mb-2 px-4 rounded-pill"
mat-stroked-button
(click)="onLoginWithInternetIdentity()"
>
<img
class="mr-2"
src="../assets/icons/internet-computer.svg"
style="height: 0.75rem"
/><span i18n>Sign in with Internet Identity</span>
</button>
<a
class="px-4 rounded-pill"
href="../api/v1/auth/google"
mat-stroked-button
><img
class="mr-2"
src="../assets/icons/google.svg"
style="height: 1rem"
/><span i18n>Sign in with Google</span></a
>
</div>
}
@if (data.hasPermissionToUseSocialLogin) {
<div class="my-3 text-center text-muted" i18n>or</div>
<div class="d-flex flex-column">
<button
class="mb-2 px-4 rounded-pill"
mat-stroked-button
(click)="onLoginWithInternetIdentity()"
>
<img
class="mr-2"
src="../assets/icons/internet-computer.svg"
style="height: 0.75rem"
/><span i18n>Sign in with Internet Identity</span>
</button>
<a
class="px-4 rounded-pill"
href="../api/v1/auth/google"
mat-stroked-button
><img
class="mr-2"
src="../assets/icons/google.svg"
style="height: 1rem"
/><span i18n>Sign in with Google</span></a
>
</div>
}
</form>
</div>
</div>

10
libs/common/src/lib/helper.ts

@ -1,7 +1,7 @@
import * as currencies from '@dinero.js/currencies';
import { NumberParser } from '@internationalized/number';
import { Type as ActivityType, DataSource, MarketData } from '@prisma/client';
import { Big } from 'big.js';
import { isISO4217CurrencyCode } from 'class-validator';
import {
getDate,
getMonth,
@ -340,8 +340,12 @@ export function interpolate(template: string, context: any) {
});
}
export function isCurrency(aCurrency = '') {
return currencies[aCurrency] || isDerivedCurrency(aCurrency);
export function isCurrency(aCurrency: string) {
if (!aCurrency) {
return false;
}
return isISO4217CurrencyCode(aCurrency) || isDerivedCurrency(aCurrency);
}
export function isDerivedCurrency(aCurrency: string) {

3
libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts

@ -112,6 +112,7 @@ export const Loading: Story = {
accounts: undefined,
baseCurrency: 'USD',
deviceType: 'desktop',
hasPermissionToOpenDetails: false,
locale: 'en-US',
showActions: false,
showAllocationInPercentage: false,
@ -128,6 +129,7 @@ export const Default: Story = {
accounts,
baseCurrency: 'USD',
deviceType: 'desktop',
hasPermissionToOpenDetails: false,
locale: 'en-US',
showActions: false,
showAllocationInPercentage: false,
@ -147,6 +149,7 @@ export const WithoutFooter: Story = {
accounts,
baseCurrency: 'USD',
deviceType: 'desktop',
hasPermissionToOpenDetails: false,
locale: 'en-US',
showActions: false,
showAllocationInPercentage: false,

62
libs/ui/src/lib/holdings-table/holdings-table.component.stories.ts

@ -0,0 +1,62 @@
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { moduleMetadata } from '@storybook/angular';
import type { Meta, StoryObj } from '@storybook/angular';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfEntityLogoComponent } from '../entity-logo';
import { holdings } from '../mocks/holdings';
import { GfValueComponent } from '../value';
import { GfHoldingsTableComponent } from './holdings-table.component';
export default {
title: 'Holdings Table',
component: GfHoldingsTableComponent,
decorators: [
moduleMetadata({
imports: [
CommonModule,
GfEntityLogoComponent,
GfValueComponent,
MatButtonModule,
MatDialogModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
NgxSkeletonLoaderModule
]
})
]
} as Meta<GfHoldingsTableComponent>;
type Story = StoryObj<GfHoldingsTableComponent>;
export const Loading: Story = {
args: {
holdings: undefined,
baseCurrency: 'USD',
deviceType: 'desktop',
hasPermissionToOpenDetails: false,
hasPermissionToShowQuantities: true,
hasPermissionToShowValues: true,
locale: 'en-US',
pageSize: Number.MAX_SAFE_INTEGER
}
};
export const Default: Story = {
args: {
holdings,
baseCurrency: 'USD',
deviceType: 'desktop',
hasPermissionToOpenDetails: false,
hasPermissionToShowQuantities: true,
hasPermissionToShowValues: true,
locale: 'en-US',
pageSize: Number.MAX_SAFE_INTEGER
}
};

293
libs/ui/src/lib/mocks/holdings.ts

@ -0,0 +1,293 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
export const holdings: PortfolioPosition[] = [
{
allocationInPercentage: 0.042990776363386086,
assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity',
assetSubClass: 'STOCK' as any,
assetSubClassLabel: 'Stock',
countries: [
{
code: 'US',
weight: 1,
continent: 'North America',
name: 'United States'
}
],
currency: 'USD',
dataSource: 'YAHOO' as any,
dateOfFirstActivity: new Date('2021-12-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 3856,
grossPerformancePercent: 0.46047289228564603,
grossPerformancePercentWithCurrencyEffect: 0.46047289228564603,
grossPerformanceWithCurrencyEffect: 3856,
holdings: [],
investment: 8374,
marketPrice: 244.6,
name: 'Apple Inc',
netPerformance: 3855,
netPerformancePercent: 0.460353475041796,
netPerformancePercentWithCurrencyEffect: 0.036440677966101696,
netPerformanceWithCurrencyEffect: 430,
quantity: 50,
sectors: [
{
name: 'Technology',
weight: 1
}
],
symbol: 'AAPL',
tags: [],
transactionCount: 1,
url: 'https://www.apple.com',
valueInBaseCurrency: 12230
},
{
allocationInPercentage: 0.02377401948293552,
assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity',
assetSubClass: 'STOCK' as any,
assetSubClassLabel: 'Stock',
countries: [
{
code: 'DE',
weight: 1,
continent: 'Europe',
name: 'Germany'
}
],
currency: 'EUR',
dataSource: 'YAHOO' as any,
dateOfFirstActivity: new Date('2021-04-23T00:00:00.000Z'),
dividend: 192,
grossPerformance: 2226.700251889169,
grossPerformancePercent: 0.49083842309827874,
grossPerformancePercentWithCurrencyEffect: 0.29306136948826367,
grossPerformanceWithCurrencyEffect: 1532.8272791336772,
holdings: [],
investment: 4536.523929471033,
marketPrice: 322.2,
name: 'Allianz SE',
netPerformance: 2222.2921914357685,
netPerformancePercent: 0.48986674069961134,
netPerformancePercentWithCurrencyEffect: 0.034489367670592026,
netPerformanceWithCurrencyEffect: 225.48257403052068,
quantity: 20,
sectors: [
{
name: 'Financial Services',
weight: 1
}
],
symbol: 'ALV.DE',
tags: [],
transactionCount: 2,
url: 'https://www.allianz.com',
valueInBaseCurrency: 6763.224181360202
},
{
allocationInPercentage: 0.08038536990007467,
assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity',
assetSubClass: 'STOCK' as any,
assetSubClassLabel: 'Stock',
countries: [
{
code: 'US',
weight: 1,
continent: 'North America',
name: 'United States'
}
],
currency: 'USD',
dataSource: 'YAHOO' as any,
dateOfFirstActivity: new Date('2018-10-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 12758.05,
grossPerformancePercent: 1.2619300787837724,
grossPerformancePercentWithCurrencyEffect: 1.2619300787837724,
grossPerformanceWithCurrencyEffect: 12758.05,
holdings: [],
investment: 10109.95,
marketPrice: 228.68,
name: 'Amazon.com, Inc.',
netPerformance: 12677.26,
netPerformancePercent: 1.253938941339967,
netPerformancePercentWithCurrencyEffect: -0.037866008722316276,
netPerformanceWithCurrencyEffect: -899.99926757812,
quantity: 100,
sectors: [
{
name: 'Consumer Discretionary',
weight: 1
}
],
symbol: 'AMZN',
tags: [],
transactionCount: 1,
url: 'https://www.aboutamazon.com',
valueInBaseCurrency: 22868
},
{
allocationInPercentage: 0.19216416482928922,
assetClass: 'LIQUIDITY' as any,
assetClassLabel: 'Liquidity',
assetSubClass: 'CRYPTOCURRENCY' as any,
assetSubClassLabel: 'Cryptocurrency',
countries: [],
currency: 'USD',
dataSource: 'COINGECKO' as any,
dateOfFirstActivity: new Date('2017-08-16T00:00:00.000Z'),
dividend: 0,
grossPerformance: 52666.7898248,
grossPerformancePercent: 26.333394912400003,
grossPerformancePercentWithCurrencyEffect: 26.333394912400003,
grossPerformanceWithCurrencyEffect: 52666.7898248,
holdings: [],
investment: 1999.9999999999998,
marketPrice: 97364,
name: 'Bitcoin',
netPerformance: 52636.8898248,
netPerformancePercent: 26.3184449124,
netPerformancePercentWithCurrencyEffect: -0.04760906442310894,
netPerformanceWithCurrencyEffect: -2732.737808972287,
quantity: 0.5614682,
sectors: [],
symbol: 'bitcoin',
tags: [],
transactionCount: 1,
url: null,
valueInBaseCurrency: 54666.7898248
},
{
allocationInPercentage: 0.04307127421937313,
assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity',
assetSubClass: 'STOCK' as any,
assetSubClassLabel: 'Stock',
countries: [
{
code: 'US',
weight: 1,
continent: 'North America',
name: 'United States'
}
],
currency: 'USD',
dataSource: 'YAHOO' as any,
dateOfFirstActivity: new Date('2023-01-03T00:00:00.000Z'),
dividend: 0,
grossPerformance: 5065.5,
grossPerformancePercent: 0.7047750229568411,
grossPerformancePercentWithCurrencyEffect: 0.7047750229568411,
grossPerformanceWithCurrencyEffect: 5065.5,
holdings: [],
investment: 7187.4,
marketPrice: 408.43,
name: 'Microsoft Corporation',
netPerformance: 5065.5,
netPerformancePercent: 0.7047750229568411,
netPerformancePercentWithCurrencyEffect: -0.015973588391056275,
netPerformanceWithCurrencyEffect: -198.899926757814,
quantity: 30,
sectors: [
{
name: 'Technology',
weight: 1
}
],
symbol: 'MSFT',
tags: [],
transactionCount: 1,
url: 'https://www.microsoft.com',
valueInBaseCurrency: 12252.9
},
{
allocationInPercentage: 0.18762679306394897,
assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity',
assetSubClass: 'STOCK' as any,
assetSubClassLabel: 'Stock',
countries: [
{
code: 'US',
weight: 1,
continent: 'North America',
name: 'United States'
}
],
currency: 'USD',
dataSource: 'YAHOO' as any,
dateOfFirstActivity: new Date('2017-01-03T00:00:00.000Z'),
dividend: 0,
grossPerformance: 51227.500000005,
grossPerformancePercent: 23.843379101756675,
grossPerformancePercentWithCurrencyEffect: 23.843379101756675,
grossPerformanceWithCurrencyEffect: 51227.500000005,
holdings: [],
investment: 2148.499999995,
marketPrice: 355.84,
name: 'Tesla, Inc.',
netPerformance: 51197.500000005,
netPerformancePercent: 23.829415871596066,
netPerformancePercentWithCurrencyEffect: -0.12051410125545206,
netPerformanceWithCurrencyEffect: -7314.00091552734,
quantity: 150,
sectors: [
{
name: 'Consumer Discretionary',
weight: 1
}
],
symbol: 'TSLA',
tags: [],
transactionCount: 1,
url: 'https://www.tesla.com',
valueInBaseCurrency: 53376
},
{
allocationInPercentage: 0.053051250766657634,
assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity',
assetSubClass: 'ETF' as any,
assetSubClassLabel: 'ETF',
countries: [
{
code: 'US',
weight: 1,
continent: 'North America',
name: 'United States'
}
],
currency: 'USD',
dataSource: 'YAHOO' as any,
dateOfFirstActivity: new Date('2019-03-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 6845.8,
grossPerformancePercent: 1.0164758094605268,
grossPerformancePercentWithCurrencyEffect: 1.0164758094605268,
grossPerformanceWithCurrencyEffect: 6845.8,
holdings: [],
investment: 8246.2,
marketPrice: 301.84,
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
netPerformance: 6746.3,
netPerformancePercent: 1.0017018833976383,
netPerformancePercentWithCurrencyEffect: 0.01085061564051406,
netPerformanceWithCurrencyEffect: 161.99969482422,
quantity: 50,
sectors: [
{
name: 'Equity',
weight: 1
}
],
symbol: 'VTI',
tags: [],
transactionCount: 5,
url: 'https://www.vanguard.com',
valueInBaseCurrency: 15092
}
];

351
libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts

@ -4,6 +4,7 @@ import { moduleMetadata } from '@storybook/angular';
import type { Meta, StoryObj } from '@storybook/angular';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { holdings } from '../mocks/holdings';
import { GfTreemapChartComponent } from './treemap-chart.component';
export default {
@ -34,359 +35,11 @@ type Story = StoryObj<GfTreemapChartComponent>;
export const Default: Story = {
args: {
holdings,
baseCurrency: 'USD',
colorScheme: 'LIGHT',
cursor: undefined,
dateRange: 'mtd',
holdings: [
{
allocationInPercentage: 0.042990776363386086,
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
countries: [],
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2021-12-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 3856,
grossPerformancePercent: 0.46047289228564603,
grossPerformancePercentWithCurrencyEffect: 0.46047289228564603,
grossPerformanceWithCurrencyEffect: 3856,
holdings: [],
investment: 8374,
marketPrice: 244.6,
name: 'Apple Inc',
netPerformance: 3855,
netPerformancePercent: 0.460353475041796,
netPerformancePercentWithCurrencyEffect: 0.036440677966101696,
netPerformanceWithCurrencyEffect: 430,
quantity: 50,
sectors: [],
symbol: 'AAPL',
tags: [],
transactionCount: 1,
url: 'https://www.apple.com',
valueInBaseCurrency: 12230
},
{
allocationInPercentage: 0.02377401948293552,
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
countries: [],
currency: 'EUR',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2021-04-23T00:00:00.000Z'),
dividend: 192,
grossPerformance: 2226.700251889169,
grossPerformancePercent: 0.49083842309827874,
grossPerformancePercentWithCurrencyEffect: 0.29306136948826367,
grossPerformanceWithCurrencyEffect: 1532.8272791336772,
holdings: [],
investment: 4536.523929471033,
marketPrice: 322.2,
name: 'Allianz SE',
netPerformance: 2222.2921914357685,
netPerformancePercent: 0.48986674069961134,
netPerformancePercentWithCurrencyEffect: 0.034489367670592026,
netPerformanceWithCurrencyEffect: 225.48257403052068,
quantity: 20,
sectors: [],
symbol: 'ALV.DE',
tags: [],
transactionCount: 2,
url: 'https://www.allianz.com',
valueInBaseCurrency: 6763.224181360202
},
{
allocationInPercentage: 0.08038536990007467,
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
countries: [],
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2018-10-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 12758.05,
grossPerformancePercent: 1.2619300787837724,
grossPerformancePercentWithCurrencyEffect: 1.2619300787837724,
grossPerformanceWithCurrencyEffect: 12758.05,
holdings: [],
investment: 10109.95,
marketPrice: 228.68,
name: 'Amazon.com, Inc.',
netPerformance: 12677.26,
netPerformancePercent: 1.253938941339967,
netPerformancePercentWithCurrencyEffect: -0.037866008722316276,
netPerformanceWithCurrencyEffect: -899.99926757812,
quantity: 100,
sectors: [],
symbol: 'AMZN',
tags: [],
transactionCount: 1,
url: 'https://www.aboutamazon.com',
valueInBaseCurrency: 22868
},
{
allocationInPercentage: 0.19216416482928922,
assetClass: 'LIQUIDITY',
assetSubClass: 'CRYPTOCURRENCY',
countries: [],
currency: 'USD',
dataSource: 'COINGECKO',
dateOfFirstActivity: new Date('2017-08-16T00:00:00.000Z'),
dividend: 0,
grossPerformance: 52666.7898248,
grossPerformancePercent: 26.333394912400003,
grossPerformancePercentWithCurrencyEffect: 26.333394912400003,
grossPerformanceWithCurrencyEffect: 52666.7898248,
holdings: [],
investment: 1999.9999999999998,
marketPrice: 97364,
name: 'Bitcoin',
netPerformance: 52636.8898248,
netPerformancePercent: 26.3184449124,
netPerformancePercentWithCurrencyEffect: -0.04760906442310894,
netPerformanceWithCurrencyEffect: -2732.737808972287,
quantity: 0.5614682,
sectors: [],
symbol: 'bitcoin',
tags: [],
transactionCount: 1,
url: null,
valueInBaseCurrency: 54666.7898248
},
{
allocationInPercentage: 0.007378652850073097,
assetClass: 'FIXED_INCOME',
assetSubClass: 'BOND',
countries: [],
currency: 'EUR',
dataSource: 'MANUAL',
dateOfFirstActivity: new Date('2021-02-01T00:00:00.000Z'),
dividend: 11.45,
grossPerformance: 0,
grossPerformancePercent: 0,
grossPerformancePercentWithCurrencyEffect: -0.1247202380342517,
grossPerformanceWithCurrencyEffect: -258.2576430160448,
holdings: [],
investment: 2099.0764063811926,
marketPrice: 1,
name: 'Bondora Go & Grow',
netPerformance: 0,
netPerformancePercent: 0,
netPerformancePercentWithCurrencyEffect: 0.009445843828715519,
netPerformanceWithCurrencyEffect: 19.6420125363184,
quantity: 2000,
sectors: [],
symbol: 'BONDORA_GO_AND_GROW',
tags: [],
transactionCount: 5,
url: null,
valueInBaseCurrency: 2099.0764063811926
},
{
allocationInPercentage: 0.07787531695543741,
assetClass: 'EQUITY',
assetSubClass: 'ETF',
countries: [],
currency: 'CHF',
dataSource: 'MANUAL',
dateOfFirstActivity: new Date('2021-04-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 4550.843985045582,
grossPerformancePercent: 0.3631417324494093,
grossPerformancePercentWithCurrencyEffect: 0.42037247857285137,
grossPerformanceWithCurrencyEffect: 5107.057936556927,
holdings: [],
investment: 17603.097090932337,
marketPrice: 188.22,
name: 'frankly Extreme 95 Index',
netPerformance: 4550.843985045582,
netPerformancePercent: 0.3631417324494093,
netPerformancePercentWithCurrencyEffect: 0.026190604904358043,
netPerformanceWithCurrencyEffect: 565.4165171873152,
quantity: 105.87328656807,
sectors: [],
symbol: 'FRANKLY95P',
tags: [],
transactionCount: 6,
url: 'https://www.frankly.ch',
valueInBaseCurrency: 22153.941075977917
},
{
allocationInPercentage: 0.04307127421937313,
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
countries: [],
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2023-01-03T00:00:00.000Z'),
dividend: 0,
grossPerformance: 5065.5,
grossPerformancePercent: 0.7047750229568411,
grossPerformancePercentWithCurrencyEffect: 0.7047750229568411,
grossPerformanceWithCurrencyEffect: 5065.5,
holdings: [],
investment: 7187.4,
marketPrice: 408.43,
name: 'Microsoft Corporation',
netPerformance: 5065.5,
netPerformancePercent: 0.7047750229568411,
netPerformancePercentWithCurrencyEffect: -0.015973588391056275,
netPerformanceWithCurrencyEffect: -198.899926757814,
quantity: 30,
sectors: [],
symbol: 'MSFT',
tags: [],
transactionCount: 1,
url: 'https://www.microsoft.com',
valueInBaseCurrency: 12252.9
},
{
allocationInPercentage: 0.18762679306394897,
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
countries: [],
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2017-01-03T00:00:00.000Z'),
dividend: 0,
grossPerformance: 51227.500000005,
grossPerformancePercent: 23.843379101756675,
grossPerformancePercentWithCurrencyEffect: 23.843379101756675,
grossPerformanceWithCurrencyEffect: 51227.500000005,
holdings: [],
investment: 2148.499999995,
marketPrice: 355.84,
name: 'Tesla, Inc.',
netPerformance: 51197.500000005,
netPerformancePercent: 23.829415871596066,
netPerformancePercentWithCurrencyEffect: -0.12051410125545206,
netPerformanceWithCurrencyEffect: -7314.00091552734,
quantity: 150,
sectors: [],
symbol: 'TSLA',
tags: [],
transactionCount: 1,
url: 'https://www.tesla.com',
valueInBaseCurrency: 53376
},
{
allocationInPercentage: 0.053051250766657634,
assetClass: 'EQUITY',
assetSubClass: 'ETF',
countries: [],
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2019-03-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 6845.8,
grossPerformancePercent: 1.0164758094605268,
grossPerformancePercentWithCurrencyEffect: 1.0164758094605268,
grossPerformanceWithCurrencyEffect: 6845.8,
holdings: [],
investment: 8246.2,
marketPrice: 301.84,
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
netPerformance: 6746.3,
netPerformancePercent: 1.0017018833976383,
netPerformancePercentWithCurrencyEffect: 0.01085061564051406,
netPerformanceWithCurrencyEffect: 161.99969482422,
quantity: 50,
sectors: [],
symbol: 'VTI',
tags: [],
transactionCount: 5,
url: 'https://www.vanguard.com',
valueInBaseCurrency: 15092
},
{
allocationInPercentage: 0.0836576192450555,
assetClass: 'EQUITY',
assetSubClass: 'ETF',
countries: [],
currency: 'CHF',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2018-03-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 6462.42356864925,
grossPerformancePercent: 0.5463044783973836,
grossPerformancePercentWithCurrencyEffect: 0.6282343505275325,
grossPerformanceWithCurrencyEffect: 7121.935580698947,
holdings: [],
investment: 17336.464702612564,
marketPrice: 129.74,
name: 'Vanguard FTSE All-World UCITS ETF',
netPerformance: 6373.040578098944,
netPerformancePercent: 0.5387484388540966,
netPerformancePercentWithCurrencyEffect: 0.008409682389650015,
netPerformanceWithCurrencyEffect: 198.47200506226807,
quantity: 165,
sectors: [],
symbol: 'VWRL.SW',
tags: [],
transactionCount: 5,
url: 'https://www.vanguard.com',
valueInBaseCurrency: 23798.888271261814
},
{
allocationInPercentage: 0.03265192235898284,
assetClass: 'EQUITY',
assetSubClass: 'ETF',
countries: [],
currency: 'EUR',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2021-08-19T00:00:00.000Z'),
dividend: 0,
grossPerformance: 3112.7991183879094,
grossPerformancePercent: 0.5040147846036197,
grossPerformancePercentWithCurrencyEffect: 0.3516875105542396,
grossPerformanceWithCurrencyEffect: 2416.799201046856,
holdings: [],
investment: 6176.007556675063,
marketPrice: 118.005,
name: 'Xtrackers MSCI World UCITS ETF 1C',
netPerformance: 3081.4179261125105,
netPerformancePercent: 0.4989336392216841,
netPerformancePercentWithCurrencyEffect: 0.006460676966633529,
netPerformanceWithCurrencyEffect: 59.626750161726044,
quantity: 75,
sectors: [],
symbol: 'XDWD.DE',
tags: [],
transactionCount: 1,
url: null,
valueInBaseCurrency: 9288.806675062973
},
{
allocationInPercentage: 0.17537283996478595,
assetClass: 'LIQUIDITY',
assetSubClass: 'CASH',
countries: [],
currency: 'USD',
dataSource: 'MANUAL',
dateOfFirstActivity: new Date('2021-04-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 0,
grossPerformancePercent: 0,
grossPerformancePercentWithCurrencyEffect: 0,
grossPerformanceWithCurrencyEffect: 0,
holdings: [],
investment: 49890,
marketPrice: 0,
name: 'USD',
netPerformance: 0,
netPerformancePercent: 0,
netPerformancePercentWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0,
quantity: 0,
sectors: [],
symbol: 'USD',
tags: [],
transactionCount: 0,
valueInBaseCurrency: 49890
}
],
locale: 'en-US'
}
};

7
package-lock.json

@ -28,7 +28,6 @@
"@dfinity/candid": "0.15.7",
"@dfinity/identity": "0.15.7",
"@dfinity/principal": "0.15.7",
"@dinero.js/currencies": "2.0.0-alpha.8",
"@internationalized/number": "3.6.3",
"@ionic/angular": "8.7.3",
"@keyv/redis": "4.4.0",
@ -5018,12 +5017,6 @@
"ts-node": "^10.8.2"
}
},
"node_modules/@dinero.js/currencies": {
"version": "2.0.0-alpha.8",
"resolved": "https://registry.npmjs.org/@dinero.js/currencies/-/currencies-2.0.0-alpha.8.tgz",
"integrity": "sha512-zApiqtuuPwjiM9LJA5/kNcT48VSHRiz2/mktkXjIpfxrJKzthXybUAgEenExIH6dYhLDgVmsLQZtZFOsdYl0Ag==",
"license": "MIT"
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz",

1
package.json

@ -74,7 +74,6 @@
"@dfinity/candid": "0.15.7",
"@dfinity/identity": "0.15.7",
"@dfinity/principal": "0.15.7",
"@dinero.js/currencies": "2.0.0-alpha.8",
"@internationalized/number": "3.6.3",
"@ionic/angular": "8.7.3",
"@keyv/redis": "4.4.0",

17489
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save