From b168a9e3c1b26f34a254b841c1aa1184dd0be61d Mon Sep 17 00:00:00 2001
From: Ayush Jain <136119112+URAYUSHJAIN@users.noreply.github.com>
Date: Sat, 11 Oct 2025 14:19:05 +0530
Subject: [PATCH 1/7] Feature/create infinite logo carousel component (#5671)
* Create infinite logo carousel component
* Update changelog
---
CHANGELOG.md | 1 +
.../pages/landing/landing-page.component.ts | 2 +
.../src/app/pages/landing/landing-page.html | 105 +--------
.../src/app/pages/landing/landing-page.scss | 76 +------
libs/ui/src/lib/logo-carousel/index.ts | 1 +
.../logo-carousel/interfaces/interfaces.ts | 7 +
.../logo-carousel.component.html | 16 ++
.../logo-carousel.component.scss | 213 ++++++++++++++++++
.../logo-carousel.component.stories.ts | 13 ++
.../logo-carousel/logo-carousel.component.ts | 110 +++++++++
10 files changed, 366 insertions(+), 178 deletions(-)
create mode 100644 libs/ui/src/lib/logo-carousel/index.ts
create mode 100644 libs/ui/src/lib/logo-carousel/interfaces/interfaces.ts
create mode 100644 libs/ui/src/lib/logo-carousel/logo-carousel.component.html
create mode 100644 libs/ui/src/lib/logo-carousel/logo-carousel.component.scss
create mode 100644 libs/ui/src/lib/logo-carousel/logo-carousel.component.stories.ts
create mode 100644 libs/ui/src/lib/logo-carousel/logo-carousel.component.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 197bcdac5..544a7bdc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
+- Changed the _As seen in_ section on the landing page to an animated carousel
- Refactored various components to use self-closing tags
### Fixed
diff --git a/apps/client/src/app/pages/landing/landing-page.component.ts b/apps/client/src/app/pages/landing/landing-page.component.ts
index f16a9c14a..f755494e0 100644
--- a/apps/client/src/app/pages/landing/landing-page.component.ts
+++ b/apps/client/src/app/pages/landing/landing-page.component.ts
@@ -4,6 +4,7 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { GfCarouselComponent } from '@ghostfolio/ui/carousel';
import { GfLogoComponent } from '@ghostfolio/ui/logo';
+import { GfLogoCarouselComponent } from '@ghostfolio/ui/logo-carousel';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { GfWorldMapChartComponent } from '@ghostfolio/ui/world-map-chart';
@@ -27,6 +28,7 @@ import { Subject } from 'rxjs';
imports: [
CommonModule,
GfCarouselComponent,
+ GfLogoCarouselComponent,
GfLogoComponent,
GfValueComponent,
GfWorldMapChartComponent,
diff --git a/apps/client/src/app/pages/landing/landing-page.html b/apps/client/src/app/pages/landing/landing-page.html
index 7202e2910..7f77c31a7 100644
--- a/apps/client/src/app/pages/landing/landing-page.html
+++ b/apps/client/src/app/pages/landing/landing-page.html
@@ -114,109 +114,8 @@
As seen in
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/client/src/app/pages/landing/landing-page.scss b/apps/client/src/app/pages/landing/landing-page.scss
index 4c0c14efd..35f362b0f 100644
--- a/apps/client/src/app/pages/landing/landing-page.scss
+++ b/apps/client/src/app/pages/landing/landing-page.scss
@@ -34,69 +34,6 @@
&.logo-agplv3 {
mask-image: url('/assets/images/logo-AGPLv3.svg');
}
-
- &.logo-alternative-to {
- mask-image: url('/assets/images/logo-alternative-to.svg');
- }
-
- &.logo-awesome {
- background-image: url('/assets/images/logo-awesome.png');
- background-position: center;
- background-repeat: no-repeat;
- background-size: contain;
- filter: grayscale(1);
- }
-
- &.logo-dev-community {
- mask-image: url('/assets/images/logo-dev-community.svg');
- }
-
- &.logo-hacker-news {
- mask-image: url('/assets/images/logo-hacker-news.svg');
- }
-
- &.logo-openalternative {
- mask-image: url('/assets/images/logo-openalternative.svg');
- }
-
- &.logo-privacy-tools {
- mask-image: url('/assets/images/logo-privacy-tools.svg');
- }
-
- &.logo-product-hunt {
- background-image: url('/assets/images/logo-product-hunt.png');
- background-position: center;
- background-repeat: no-repeat;
- background-size: contain;
- filter: grayscale(1);
- }
-
- &.logo-reddit {
- mask-image: url('/assets/images/logo-reddit.svg');
- max-height: 1rem;
- }
-
- &.logo-sackgeld {
- mask-image: url('/assets/images/logo-sackgeld.png');
- }
-
- &.logo-selfh-st {
- mask-image: url('/assets/images/logo-selfh-st.svg');
- max-height: 1.25rem;
- }
-
- &.logo-sourceforge {
- mask-image: url('/assets/images/logo-sourceforge.svg');
- }
-
- &.logo-umbrel {
- mask-image: url('/assets/images/logo-umbrel.svg');
- max-height: 1.5rem;
- }
-
- &.logo-unraid {
- mask-image: url('/assets/images/logo-unraid.svg');
- }
}
.outro-inner-container {
@@ -128,18 +65,7 @@
}
.logo {
- &.logo-agplv3,
- &.logo-alternative-to,
- &.logo-dev-community,
- &.logo-hacker-news,
- &.logo-openalternative,
- &.logo-privacy-tools,
- &.logo-reddit,
- &.logo-sackgeld,
- &.logo-selfh-st,
- &.logo-sourceforge,
- &.logo-umbrel,
- &.logo-unraid {
+ &.logo-agplv3 {
background-color: rgba(var(--light-primary-text));
}
}
diff --git a/libs/ui/src/lib/logo-carousel/index.ts b/libs/ui/src/lib/logo-carousel/index.ts
new file mode 100644
index 000000000..e69f6039c
--- /dev/null
+++ b/libs/ui/src/lib/logo-carousel/index.ts
@@ -0,0 +1 @@
+export * from './logo-carousel.component';
diff --git a/libs/ui/src/lib/logo-carousel/interfaces/interfaces.ts b/libs/ui/src/lib/logo-carousel/interfaces/interfaces.ts
new file mode 100644
index 000000000..43dcfeefd
--- /dev/null
+++ b/libs/ui/src/lib/logo-carousel/interfaces/interfaces.ts
@@ -0,0 +1,7 @@
+export interface LogoItem {
+ className: string;
+ isMask?: boolean;
+ name: string;
+ title: string;
+ url: string;
+}
diff --git a/libs/ui/src/lib/logo-carousel/logo-carousel.component.html b/libs/ui/src/lib/logo-carousel/logo-carousel.component.html
new file mode 100644
index 000000000..79b4ca11b
--- /dev/null
+++ b/libs/ui/src/lib/logo-carousel/logo-carousel.component.html
@@ -0,0 +1,16 @@
+
+
+ @for (logo of logosRepeated; track $index) {
+
+ }
+
+
diff --git a/libs/ui/src/lib/logo-carousel/logo-carousel.component.scss b/libs/ui/src/lib/logo-carousel/logo-carousel.component.scss
new file mode 100644
index 000000000..d8a8865f7
--- /dev/null
+++ b/libs/ui/src/lib/logo-carousel/logo-carousel.component.scss
@@ -0,0 +1,213 @@
+:host {
+ display: block;
+ overflow: hidden;
+ position: relative;
+ width: 100%;
+
+ .logo-carousel-container {
+ &::before,
+ &::after {
+ content: '';
+ height: 100%;
+ pointer-events: none;
+ position: absolute;
+ top: 0;
+ width: 100px;
+ z-index: 2;
+ }
+
+ &::before {
+ background: linear-gradient(
+ to right,
+ var(--light-background) 0%,
+ rgba(var(--palette-background-background), 0) 100%
+ );
+ left: 0;
+ }
+
+ &::after {
+ background: linear-gradient(
+ to left,
+ var(--light-background) 0%,
+ rgba(var(--palette-background-background), 0) 100%
+ );
+ right: 0;
+ }
+
+ @media (max-width: 768px) {
+ &::before,
+ &::after {
+ width: 50px;
+ }
+ }
+
+ @media (max-width: 576px) {
+ &::before,
+ &::after {
+ width: 30px;
+ }
+ }
+
+ .logo-carousel-track {
+ animation: scroll 60s linear infinite;
+ width: fit-content;
+
+ &:hover {
+ animation-play-state: paused;
+ }
+
+ .logo-carousel-item {
+ flex-shrink: 0;
+ min-width: 200px;
+ padding: 0 2rem;
+
+ @media (max-width: 768px) {
+ min-width: 150px;
+ padding: 0 1.5rem;
+ }
+
+ @media (max-width: 576px) {
+ min-width: 120px;
+ padding: 0 1rem;
+ }
+
+ .logo {
+ height: 3rem;
+ transition:
+ opacity 0.3s ease,
+ transform 0.3s ease;
+ width: 7.5rem;
+
+ &:hover {
+ opacity: 0.8;
+ }
+
+ &.mask {
+ background-color: rgba(var(--dark-secondary-text));
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+
+ &.logo-alternative-to {
+ mask-image: url('/assets/images/logo-alternative-to.svg');
+ }
+
+ &.logo-awesome {
+ background-image: url('/assets/images/logo-awesome.png');
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: contain;
+ filter: grayscale(1);
+ }
+
+ &.logo-dev-community {
+ mask-image: url('/assets/images/logo-dev-community.svg');
+ }
+
+ &.logo-hacker-news {
+ mask-image: url('/assets/images/logo-hacker-news.svg');
+ }
+
+ &.logo-openalternative {
+ mask-image: url('/assets/images/logo-openalternative.svg');
+ }
+
+ &.logo-privacy-tools {
+ mask-image: url('/assets/images/logo-privacy-tools.svg');
+ }
+
+ &.logo-product-hunt {
+ background-image: url('/assets/images/logo-product-hunt.png');
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: contain;
+ filter: grayscale(1);
+ }
+
+ &.logo-reddit {
+ mask-image: url('/assets/images/logo-reddit.svg');
+ max-height: 1rem;
+ }
+
+ &.logo-sackgeld {
+ mask-image: url('/assets/images/logo-sackgeld.png');
+ }
+
+ &.logo-selfh-st {
+ mask-image: url('/assets/images/logo-selfh-st.svg');
+ max-height: 1.25rem;
+ }
+
+ &.logo-sourceforge {
+ mask-image: url('/assets/images/logo-sourceforge.svg');
+ }
+
+ &.logo-umbrel {
+ mask-image: url('/assets/images/logo-umbrel.svg');
+ max-height: 1.5rem;
+ }
+
+ &.logo-unraid {
+ mask-image: url('/assets/images/logo-unraid.svg');
+ }
+
+ @media (max-width: 768px) {
+ height: 2.5rem;
+ width: 6rem;
+ }
+
+ @media (max-width: 576px) {
+ height: 2rem;
+ width: 5rem;
+ }
+ }
+ }
+
+ @keyframes scroll {
+ 0% {
+ transform: translateX(0);
+ }
+ 100% {
+ transform: translateX(-50%);
+ }
+ }
+ }
+ }
+}
+
+:host-context(.theme-dark) {
+ .logo-carousel-container {
+ &::before {
+ background: linear-gradient(
+ to right,
+ var(--dark-background) 0%,
+ rgba(var(--palette-background-background-dark), 0) 100%
+ );
+ }
+
+ &::after {
+ background: linear-gradient(
+ to left,
+ var(--dark-background) 0%,
+ rgba(var(--palette-background-background-dark), 0) 100%
+ );
+ }
+
+ .logo {
+ &.logo-alternative-to,
+ &.logo-dev-community,
+ &.logo-hacker-news,
+ &.logo-openalternative,
+ &.logo-privacy-tools,
+ &.logo-reddit,
+ &.logo-sackgeld,
+ &.logo-selfh-st,
+ &.logo-sourceforge,
+ &.logo-umbrel,
+ &.logo-unraid {
+ background-color: rgba(var(--light-primary-text));
+ }
+ }
+ }
+}
diff --git a/libs/ui/src/lib/logo-carousel/logo-carousel.component.stories.ts b/libs/ui/src/lib/logo-carousel/logo-carousel.component.stories.ts
new file mode 100644
index 000000000..a4e88df98
--- /dev/null
+++ b/libs/ui/src/lib/logo-carousel/logo-carousel.component.stories.ts
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/angular';
+
+import { GfLogoCarouselComponent } from './logo-carousel.component';
+
+const meta: Meta = {
+ title: 'Logo Carousel',
+ component: GfLogoCarouselComponent
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/libs/ui/src/lib/logo-carousel/logo-carousel.component.ts b/libs/ui/src/lib/logo-carousel/logo-carousel.component.ts
new file mode 100644
index 000000000..d7d3fa6af
--- /dev/null
+++ b/libs/ui/src/lib/logo-carousel/logo-carousel.component.ts
@@ -0,0 +1,110 @@
+import { CommonModule } from '@angular/common';
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+
+import { LogoItem } from './interfaces/interfaces';
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule],
+ selector: 'gf-logo-carousel',
+ styleUrls: ['./logo-carousel.component.scss'],
+ templateUrl: './logo-carousel.component.html'
+})
+export class GfLogoCarouselComponent {
+ public readonly logos: LogoItem[] = [
+ {
+ className: 'logo-alternative-to',
+ isMask: true,
+ name: 'AlternativeTo',
+ title: 'AlternativeTo - Crowdsourced software recommendations',
+ url: 'https://alternativeto.net'
+ },
+ {
+ className: 'logo-awesome',
+ name: 'Awesome Selfhosted',
+ title:
+ 'Awesome-Selfhosted: A list of Free Software network services and web applications which can be hosted on your own servers',
+ url: 'https://github.com/awesome-selfhosted/awesome-selfhosted'
+ },
+ {
+ className: 'logo-dev-community',
+ isMask: true,
+ name: 'DEV Community',
+ title:
+ 'DEV Community - A constructive and inclusive social network for software developers',
+ url: 'https://dev.to'
+ },
+ {
+ className: 'logo-hacker-news',
+ isMask: true,
+ name: 'Hacker News',
+ title: 'Hacker News',
+ url: 'https://news.ycombinator.com'
+ },
+ {
+ className: 'logo-openalternative',
+ isMask: true,
+ name: 'OpenAlternative',
+ title: 'OpenAlternative: Open Source Alternatives to Popular Software',
+ url: 'https://openalternative.co'
+ },
+ {
+ className: 'logo-privacy-tools',
+ isMask: true,
+ name: 'Privacy Tools',
+ title: 'Privacy Tools: Software Alternatives and Encryption',
+ url: 'https://www.privacytools.io'
+ },
+ {
+ className: 'logo-product-hunt',
+ name: 'Product Hunt',
+ title: 'Product Hunt – The best new products in tech.',
+ url: 'https://www.producthunt.com'
+ },
+ {
+ className: 'logo-reddit',
+ isMask: true,
+ name: 'Reddit',
+ title: 'Reddit - Dive into anything',
+ url: 'https://www.reddit.com'
+ },
+ {
+ className: 'logo-sackgeld',
+ isMask: true,
+ name: 'Sackgeld',
+ title: 'Sackgeld.com – Apps für ein höheres Sackgeld',
+ url: 'https://www.sackgeld.com'
+ },
+ {
+ className: 'logo-selfh-st',
+ isMask: true,
+ name: 'selfh.st',
+ title: 'selfh.st — Self-hosted content and software',
+ url: 'https://selfh.st'
+ },
+ {
+ className: 'logo-sourceforge',
+ isMask: true,
+ name: 'SourceForge',
+ title:
+ 'SourceForge: The Complete Open-Source and Business Software Platform',
+ url: 'https://sourceforge.net'
+ },
+ {
+ className: 'logo-umbrel',
+ isMask: true,
+ name: 'Umbrel',
+ title: 'Umbrel — A personal server OS for self-hosting',
+ url: 'https://umbrel.com'
+ },
+ {
+ className: 'logo-unraid',
+ isMask: true,
+ name: 'Unraid',
+ title: 'Unraid | Unleash Your Hardware',
+ url: 'https://unraid.net'
+ }
+ ];
+
+ public readonly logosRepeated = [...this.logos, ...this.logos];
+}
From 94e8a7c6bc0b7050a1a6f65af6b14d1197ae01f2 Mon Sep 17 00:00:00 2001
From: Shivansh Pandey <131197200+Shivansh-22866@users.noreply.github.com>
Date: Sat, 11 Oct 2025 14:25:37 +0530
Subject: [PATCH 2/7] Feature/add support for configuring safe withdrawal rate
(#5679)
* Add support for configuring safe withdrawal rate
* Update changelog
---
CHANGELOG.md | 4 ++
.../portfolio/fire/fire-page.component.ts | 59 ++++++++++++++++---
.../app/pages/portfolio/fire/fire-page.html | 35 ++++++++---
.../app/pages/portfolio/fire/fire-page.scss | 16 +++++
4 files changed, 98 insertions(+), 16 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 544a7bdc6..58a7b3b48 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
+### 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
diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
index c80b55c45..63187c05c 100644
--- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
@@ -7,8 +7,10 @@ import { GfFireCalculatorComponent } from '@ghostfolio/ui/fire-calculator';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { GfValueComponent } from '@ghostfolio/ui/value';
-import { NgStyle } from '@angular/common';
+import { CommonModule, NgStyle } from '@angular/common';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { FormControl } from '@angular/forms';
import { Big } from 'big.js';
import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@@ -17,11 +19,14 @@ import { takeUntil } from 'rxjs/operators';
@Component({
imports: [
+ CommonModule,
+ FormsModule,
GfFireCalculatorComponent,
GfPremiumIndicatorComponent,
GfValueComponent,
NgStyle,
- NgxSkeletonLoaderModule
+ NgxSkeletonLoaderModule,
+ ReactiveFormsModule
],
selector: 'gf-fire-page',
styleUrls: ['./fire-page.scss'],
@@ -33,6 +38,8 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
public hasImpersonationId: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public isLoading = false;
+ public safeWithdrawalRateControl = new FormControl(undefined);
+ public safeWithdrawalRateOptions = [0.025, 0.03, 0.035, 0.04, 0.045];
public user: User;
public withdrawalRatePerMonth: Big;
public withdrawalRatePerYear: Big;
@@ -70,11 +77,7 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
};
}
- this.withdrawalRatePerYear = Big(
- this.fireWealth.today.valueInBaseCurrency
- ).mul(this.user.settings.safeWithdrawalRate);
-
- this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12);
+ this.calculateWithdrawalRates();
this.isLoading = false;
@@ -88,6 +91,12 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
this.hasImpersonationId = !!impersonationId;
});
+ this.safeWithdrawalRateControl.valueChanges
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe((value) => {
+ this.onSafeWithdrawalRateChange(Number(value));
+ });
+
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
@@ -102,6 +111,13 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
permissions.updateUserSettings
);
+ this.safeWithdrawalRateControl.setValue(
+ this.user.settings.safeWithdrawalRate,
+ { emitEvent: false }
+ );
+
+ this.calculateWithdrawalRates();
+
this.changeDetectorRef.markForCheck();
}
});
@@ -141,6 +157,25 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
});
});
}
+
+ public onSafeWithdrawalRateChange(safeWithdrawalRate: number) {
+ this.dataService
+ .putUserSetting({ safeWithdrawalRate })
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe(() => {
+ this.userService
+ .get(true)
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe((user) => {
+ this.user = user;
+
+ this.calculateWithdrawalRates();
+
+ this.changeDetectorRef.markForCheck();
+ });
+ });
+ }
+
public onSavingsRateChange(savingsRate: number) {
this.dataService
.putUserSetting({ savingsRate })
@@ -180,4 +215,14 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
+
+ private calculateWithdrawalRates() {
+ if (this.fireWealth && this.user?.settings?.safeWithdrawalRate) {
+ this.withdrawalRatePerYear = new Big(
+ this.fireWealth.today.valueInBaseCurrency
+ ).mul(this.user.settings.safeWithdrawalRate);
+
+ this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12);
+ }
+ }
}
diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html
index d6548f761..ce51717fa 100644
--- a/apps/client/src/app/pages/portfolio/fire/fire-page.html
+++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html
@@ -105,15 +105,32 @@
and a safe withdrawal rate (SWR) of
-
- .
+ @if (
+ !hasImpersonationId &&
+ hasPermissionToUpdateUserSettings &&
+ user?.settings?.isExperimentalFeatures
+ ) {
+ .
+ } @else {
+
+ .
+ }
}
diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.scss b/apps/client/src/app/pages/portfolio/fire/fire-page.scss
index 5d4e87f30..2892885c9 100644
--- a/apps/client/src/app/pages/portfolio/fire/fire-page.scss
+++ b/apps/client/src/app/pages/portfolio/fire/fire-page.scss
@@ -1,3 +1,19 @@
:host {
display: block;
+
+ .safe-withdrawal-rate-select {
+ background-color: transparent;
+ color: rgb(var(--dark-primary-text));
+
+ &:focus {
+ box-shadow: none;
+ outline: 0;
+ }
+ }
+}
+
+:host-context(.theme-dark) {
+ .safe-withdrawal-rate-select {
+ color: rgb(var(--light-primary-text));
+ }
}
From ab7f1fd88141ecaa868dc14899fadba4701b5616 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 11 Oct 2025 11:25:28 +0200
Subject: [PATCH 3/7] Task/set up GitHub Sponsors (#5723)
* Add github
---
.github/FUNDING.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 9df3e0d6d..a881e8fc8 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1,2 @@
buy_me_a_coffee: ghostfolio
+github: ghostfolio
From 948233c6510e4d53c304e9761d2c438669a4a22c Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 11 Oct 2025 11:26:10 +0200
Subject: [PATCH 4/7] Task/remove @IsOptional() from dataSource in
CreateOrderDto (#5703)
* Remove is @IsOptional() from dataSource
---
apps/api/src/app/order/create-order.dto.ts | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/apps/api/src/app/order/create-order.dto.ts b/apps/api/src/app/order/create-order.dto.ts
index af87fd93e..ba7a1d868 100644
--- a/apps/api/src/app/order/create-order.dto.ts
+++ b/apps/api/src/app/order/create-order.dto.ts
@@ -44,8 +44,7 @@ export class CreateOrderDto {
customCurrency?: string;
@IsEnum(DataSource)
- @IsOptional()
- dataSource?: DataSource;
+ dataSource: DataSource;
@IsISO8601()
@Validate(IsAfter1970Constraint)
From c5c11929008ac5bf6c4e56bcf9fe337f3a99d4f3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sven=20G=C3=BCnther?=
Date: Sat, 11 Oct 2025 17:07:42 +0200
Subject: [PATCH 5/7] Bugfix/import of custom asset profiles (#5670)
* Import of custom asset profiles
* Update changelog
---
CHANGELOG.md | 1 +
apps/api/src/app/import/import.service.ts | 38 ++++++++++++++++
test/import/ok/penthouse-apartment.json | 53 +++++++++++++++++++++++
3 files changed, 92 insertions(+)
create mode 100644 test/import/ok/penthouse-apartment.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58a7b3b48..c7472e421 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed the server startup message to properly display IPv6 addresses
+- Fixed an issue where importing custom asset profiles failed due to validation errors
## 2.207.0 - 2025-10-08
diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts
index 82231d237..69ec781c3 100644
--- a/apps/api/src/app/import/import.service.ts
+++ b/apps/api/src/app/import/import.service.ts
@@ -373,6 +373,7 @@ export class ImportService {
const assetProfiles = await this.validateActivities({
activitiesDto,
+ assetProfilesWithMarketDataDto,
maxActivitiesToImport,
user
});
@@ -698,10 +699,12 @@ export class ImportService {
private async validateActivities({
activitiesDto,
+ assetProfilesWithMarketDataDto,
maxActivitiesToImport,
user
}: {
activitiesDto: Partial[];
+ assetProfilesWithMarketDataDto: ImportDataDto['assetProfiles'];
maxActivitiesToImport: number;
user: UserWithSettings;
}) {
@@ -749,6 +752,41 @@ export class ImportService {
)?.[symbol]
};
+ if (!assetProfile?.name) {
+ const assetProfileInImport = assetProfilesWithMarketDataDto?.find(
+ (profile) => {
+ return (
+ profile.dataSource === dataSource && profile.symbol === symbol
+ );
+ }
+ );
+
+ if (assetProfileInImport) {
+ // Merge all fields of custom asset profiles into the validation object
+ Object.assign(assetProfile, {
+ assetClass: assetProfileInImport.assetClass,
+ assetSubClass: assetProfileInImport.assetSubClass,
+ comment: assetProfileInImport.comment,
+ countries: assetProfileInImport.countries,
+ currency: assetProfileInImport.currency,
+ cusip: assetProfileInImport.cusip,
+ dataSource: assetProfileInImport.dataSource,
+ figi: assetProfileInImport.figi,
+ figiComposite: assetProfileInImport.figiComposite,
+ figiShareClass: assetProfileInImport.figiShareClass,
+ holdings: assetProfileInImport.holdings,
+ isActive: assetProfileInImport.isActive,
+ isin: assetProfileInImport.isin,
+ name: assetProfileInImport.name,
+ scraperConfiguration: assetProfileInImport.scraperConfiguration,
+ sectors: assetProfileInImport.sectors,
+ symbol: assetProfileInImport.symbol,
+ symbolMapping: assetProfileInImport.symbolMapping,
+ url: assetProfileInImport.url
+ });
+ }
+ }
+
if (
(dataSource !== 'MANUAL' && type === 'BUY') ||
type === 'DIVIDEND' ||
diff --git a/test/import/ok/penthouse-apartment.json b/test/import/ok/penthouse-apartment.json
new file mode 100644
index 000000000..3b230cf76
--- /dev/null
+++ b/test/import/ok/penthouse-apartment.json
@@ -0,0 +1,53 @@
+{
+ "meta": {
+ "date": "2023-02-05T00:00:00.000Z",
+ "version": "dev"
+ },
+ "accounts": [],
+ "assetProfiles": [
+ {
+ "assetClass": null,
+ "assetSubClass": null,
+ "comment": null,
+ "countries": [],
+ "currency": "USD",
+ "cusip": null,
+ "dataSource": "MANUAL",
+ "figi": null,
+ "figiComposite": null,
+ "figiShareClass": null,
+ "holdings": [],
+ "isActive": true,
+ "isin": null,
+ "marketData": [],
+ "name": "Penthouse Apartment",
+ "scraperConfiguration": null,
+ "sectors": [],
+ "symbol": "7e91b7d4-1430-4212-8380-289a06c9bbc1",
+ "symbolMapping": {},
+ "url": null
+ }
+ ],
+ "platforms": [],
+ "tags": [],
+ "activities": [
+ {
+ "accountId": null,
+ "comment": null,
+ "currency": "USD",
+ "dataSource": "MANUAL",
+ "date": "2022-01-01T00:00:00.000Z",
+ "fee": 0,
+ "quantity": 1,
+ "symbol": "7e91b7d4-1430-4212-8380-289a06c9bbc1",
+ "tags": [],
+ "type": "BUY",
+ "unitPrice": 500000,
+ }
+ ],
+ "user": {
+ "settings": {
+ "currency": "USD"
+ }
+ }
+}
From 13838bed6d20307eccf3ea02cb632ea0e5642b5c Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 11 Oct 2025 20:12:37 +0200
Subject: [PATCH 6/7] Feature/improve language localization for de 20250811
(#5730)
* Update translation
* Update changelog
---
CHANGELOG.md | 1 +
apps/client/src/locales/messages.de.xlf | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7472e421..1128a6205 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Changed the _As seen in_ section on the landing page to an animated carousel
- Refactored various components to use self-closing tags
+- Improved the language localization for German (`de`)
### Fixed
diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf
index 3e12ef852..9211617f0 100644
--- a/apps/client/src/locales/messages.de.xlf
+++ b/apps/client/src/locales/messages.de.xlf
@@ -5409,7 +5409,7 @@
,
- entnehmen,
+ entnehmen,
apps/client/src/app/pages/portfolio/fire/fire-page.html
93
From d2fe16c794edc91334b8c58ecb492705b59ef694 Mon Sep 17 00:00:00 2001
From: Tanbir Ali <124070023+tanbirali@users.noreply.github.com>
Date: Sat, 11 Oct 2025 23:44:25 +0530
Subject: [PATCH 7/7] Task/refactor transactionCount to activitiesCount in
portfolio holding response (#5709)
* Refactor transactionCount to activitiesCount in portfolio holding response
* Update changelog
---
CHANGELOG.md | 1 +
apps/api/src/app/portfolio/portfolio.service.ts | 6 +++---
.../holding-detail-dialog.component.ts | 8 +++-----
.../holding-detail-dialog/holding-detail-dialog.html | 6 +++---
.../responses/portfolio-holding-response.interface.ts | 2 +-
5 files changed, 11 insertions(+), 12 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1128a6205..d178ae97a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### 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
- Improved the language localization for German (`de`)
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index e73f79784..f774378ef 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -778,6 +778,7 @@ export class PortfolioService {
if (activities.length === 0) {
return {
activities: [],
+ activitiesCount: 0,
averagePrice: undefined,
dataProviderInfo: undefined,
dividendInBaseCurrency: undefined,
@@ -802,7 +803,6 @@ export class PortfolioService {
quantity: undefined,
SymbolProfile: undefined,
tags: [],
- transactionCount: undefined,
value: undefined
};
}
@@ -966,8 +966,8 @@ export class PortfolioService {
marketPriceMin,
SymbolProfile,
tags,
- transactionCount,
activities: activitiesOfHolding,
+ activitiesCount: transactionCount,
averagePrice: averagePrice.toNumber(),
dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0],
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
@@ -1070,6 +1070,7 @@ export class PortfolioService {
marketPriceMin,
SymbolProfile,
activities: [],
+ activitiesCount: 0,
averagePrice: 0,
dataProviderInfo: undefined,
dividendInBaseCurrency: 0,
@@ -1095,7 +1096,6 @@ export class PortfolioService {
},
quantity: 0,
tags: [],
- transactionCount: undefined,
value: 0
};
}
diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
index 8f6616174..d4c1c59c1 100644
--- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
+++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
@@ -100,6 +100,7 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces';
templateUrl: 'holding-detail-dialog.html'
})
export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
+ public activitiesCount: number;
public activityForm: FormGroup;
public accounts: Account[];
public assetClass: string;
@@ -151,8 +152,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
public SymbolProfile: EnhancedSymbolProfile;
public tags: Tag[];
public tagsAvailable: Tag[];
- public totalItems: number;
- public transactionCount: number;
public user: User;
public value: number;
@@ -261,6 +260,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(
({
+ activitiesCount,
averagePrice,
dataProviderInfo,
dividendInBaseCurrency,
@@ -279,9 +279,9 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
quantity,
SymbolProfile,
tags,
- transactionCount,
value
}) => {
+ this.activitiesCount = activitiesCount;
this.averagePrice = averagePrice;
if (
@@ -429,8 +429,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
this.activityForm.setValue({ tags: this.tags }, { emitEvent: false });
- this.transactionCount = transactionCount;
- this.totalItems = transactionCount;
this.value = value;
if (SymbolProfile?.assetClass) {
diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
index 651f01a65..298692303 100644
--- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
+++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
@@ -223,9 +223,9 @@
- @if (transactionCount === 1) {
+ @if (activitiesCount === 1) {
Activity
} @else {
Activities
@@ -363,7 +363,7 @@
[sortColumn]="sortColumn"
[sortDirection]="sortDirection"
[sortDisabled]="true"
- [totalItems]="totalItems"
+ [totalItems]="activitiesCount"
(activityToClone)="onCloneActivity($event)"
(activityToUpdate)="onUpdateActivity($event)"
(export)="onExport()"
diff --git a/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts b/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts
index 000460228..b82a8f85d 100644
--- a/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts
+++ b/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts
@@ -10,6 +10,7 @@ import { Tag } from '@prisma/client';
export interface PortfolioHoldingResponse {
activities: Activity[];
+ activitiesCount: number;
averagePrice: number;
dataProviderInfo: DataProviderInfo;
dividendInBaseCurrency: number;
@@ -34,6 +35,5 @@ export interface PortfolioHoldingResponse {
quantity: number;
SymbolProfile: EnhancedSymbolProfile;
tags: Tag[];
- transactionCount: number;
value: number;
}