Browse Source

Merge branch 'main' into ref/investment-chart-component

pull/5490/head
Thomas Kaul 2 months ago
committed by GitHub
parent
commit
02626cb235
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 16
      apps/api/src/app/user/user.service.ts
  3. 8
      apps/client/src/app/app-routing.module.ts
  4. 2
      apps/client/src/app/app.module.ts
  5. 2
      apps/client/src/app/components/admin-jobs/admin-jobs.html
  6. 19
      apps/client/src/app/components/rule/rule.component.ts
  7. 22
      apps/client/src/app/components/rule/rule.module.ts
  8. 11
      apps/client/src/app/components/rules/rules.component.ts
  9. 16
      apps/client/src/app/components/rules/rules.module.ts
  10. 40
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts
  11. 83
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html
  12. 26
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.module.ts
  13. 22
      apps/client/src/app/pages/markets/markets-page-routing.module.ts
  14. 9
      apps/client/src/app/pages/markets/markets-page.component.ts
  15. 14
      apps/client/src/app/pages/markets/markets-page.module.ts
  16. 24
      apps/client/src/app/pages/markets/markets-page.routes.ts
  17. 4
      apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts
  18. 23
      apps/client/src/app/services/user/user.service.ts

6
CHANGELOG.md

@ -9,8 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Extended the variations of the interstitials for the subscription
- Renamed the job identifier column in the jobs queue view of the admin control panel
- Refactored the markets page to standalone
- Refactored the header component to standalone
- Refactored the investment chart component to standalone
- Refactored the rule component to standalone
- Refactored the rules component to standalone
- Refactored the subscription interstitial dialog component to standalone
## 2.197.0 - 2025-09-07

16
apps/api/src/app/user/user.service.ts

@ -184,6 +184,7 @@ export class UserService {
userWhereUniqueInput: Prisma.UserWhereUniqueInput
): Promise<UserWithSettings | null> {
const {
_count,
accessesGet,
accessToken,
accounts,
@ -199,6 +200,11 @@ export class UserService {
updatedAt
} = await this.prismaService.user.findUnique({
include: {
_count: {
select: {
activities: true
}
},
accessesGet: true,
accounts: {
include: { platform: true }
@ -210,6 +216,8 @@ export class UserService {
where: userWhereUniqueInput
});
const activitiesCount = _count?.activities ?? 0;
const user: UserWithSettings = {
accessesGet,
accessToken,
@ -404,13 +412,13 @@ export class UserService {
);
let frequency = 7;
if (daysSinceRegistration > 720) {
if (activitiesCount > 1000 || daysSinceRegistration > 720) {
frequency = 1;
} else if (daysSinceRegistration > 360) {
} else if (activitiesCount > 750 || daysSinceRegistration > 360) {
frequency = 2;
} else if (daysSinceRegistration > 180) {
} else if (activitiesCount > 500 || daysSinceRegistration > 180) {
frequency = 3;
} else if (daysSinceRegistration > 60) {
} else if (activitiesCount > 250 || daysSinceRegistration > 60) {
frequency = 4;
} else if (daysSinceRegistration > 30) {
frequency = 5;

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

@ -1,11 +1,11 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strategy';
import { publicRoutes, internalRoutes } from '@ghostfolio/common/routes/routes';
import { NgModule } from '@angular/core';
import { RouterModule, Routes, TitleStrategy } from '@angular/router';
import { AuthGuard } from './core/auth.guard';
import { ModulePreloadService } from './core/module-preload.service';
import { PageTitleStrategy } from './services/page-title.strategy';
const routes: Routes = [
{
@ -89,9 +89,7 @@ const routes: Routes = [
{
path: publicRoutes.markets.path,
loadChildren: () =>
import('./pages/markets/markets-page.module').then(
(m) => m.MarketsPageModule
)
import('./pages/markets/markets-page.routes').then((m) => m.routes)
},
{
path: publicRoutes.openStartup.path,

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

@ -31,7 +31,6 @@ import { DateFormats } from './adapter/date-formats';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { GfHeaderComponent } from './components/header/header.component';
import { GfSubscriptionInterstitialDialogModule } from './components/subscription-interstitial-dialog/subscription-interstitial-dialog.module';
import { authInterceptorProviders } from './core/auth.interceptor';
import { httpResponseInterceptorProviders } from './core/http-response.interceptor';
import { LanguageService } from './core/language.service';
@ -51,7 +50,6 @@ export function NgxStripeFactory(): string {
GfHeaderComponent,
GfLogoComponent,
GfNotificationModule,
GfSubscriptionInterstitialDialogModule,
IonIcon,
MatAutocompleteModule,
MatChipsModule,

2
apps/client/src/app/components/admin-jobs/admin-jobs.html

@ -19,7 +19,7 @@
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="index">
<th *matHeaderCellDef class="px-1 py-2 text-right" mat-header-cell>
#
<ng-container i18n>Job ID</ng-container>
</th>
<td *matCellDef="let element" class="px-1 py-2 text-right" mat-cell>
{{ element.id }}

19
apps/client/src/app/components/rule/rule.component.ts

@ -5,6 +5,7 @@ import {
XRayRulesSettings
} from '@ghostfolio/common/interfaces';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@ -13,7 +14,10 @@ import {
OnInit,
Output
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import {
addCircleOutline,
@ -24,19 +28,26 @@ import {
warningOutline
} from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, takeUntil } from 'rxjs';
import { IRuleSettingsDialogParams } from './rule-settings-dialog/interfaces/interfaces';
import { GfRuleSettingsDialogComponent } from './rule-settings-dialog/rule-settings-dialog.component';
@Component({
selector: 'gf-rule',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './rule.component.html',
imports: [
CommonModule,
IonIcon,
MatButtonModule,
MatMenuModule,
NgxSkeletonLoaderModule
],
selector: 'gf-rule',
styleUrls: ['./rule.component.scss'],
standalone: false
templateUrl: './rule.component.html'
})
export class RuleComponent implements OnInit {
export class GfRuleComponent implements OnInit {
@Input() categoryName: string;
@Input() hasPermissionToUpdateUserSettings: boolean;
@Input() isLoading: boolean;

22
apps/client/src/app/components/rule/rule.module.ts

@ -1,22 +0,0 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu';
import { IonIcon } from '@ionic/angular/standalone';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { RuleComponent } from './rule.component';
@NgModule({
declarations: [RuleComponent],
exports: [RuleComponent],
imports: [
CommonModule,
IonIcon,
MatButtonModule,
MatMenuModule,
NgxSkeletonLoaderModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfRuleModule {}

11
apps/client/src/app/components/rules/rules.component.ts

@ -1,4 +1,5 @@
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
import { GfRuleComponent } from '@ghostfolio/client/components/rule/rule.component';
import {
PortfolioReportRule,
XRayRulesSettings
@ -11,15 +12,17 @@ import {
Input,
Output
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'gf-rules',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './rules.component.html',
imports: [GfRuleComponent, MatButtonModule, MatCardModule],
selector: 'gf-rules',
styleUrls: ['./rules.component.scss'],
standalone: false
templateUrl: './rules.component.html'
})
export class RulesComponent {
export class GfRulesComponent {
@Input() categoryName: string;
@Input() hasPermissionToUpdateUserSettings: boolean;
@Input() isLoading: boolean;

16
apps/client/src/app/components/rules/rules.module.ts

@ -1,16 +0,0 @@
import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { RulesComponent } from './rules.component';
@NgModule({
declarations: [RulesComponent],
exports: [RulesComponent],
imports: [CommonModule, GfRuleModule, MatButtonModule, MatCardModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfRulesModule {}

40
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts

@ -1,13 +1,24 @@
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { GfMembershipCardComponent } from '@ghostfolio/ui/membership-card';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
CUSTOM_ELEMENTS_SCHEMA,
Inject,
OnInit
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef
} from '@angular/material/dialog';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { arrowForwardOutline, checkmarkCircleOutline } from 'ionicons/icons';
import ms from 'ms';
@ -19,17 +30,26 @@ import { SubscriptionInterstitialDialogParams } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column flex-grow-1 h-100' },
imports: [
CommonModule,
GfMembershipCardComponent,
GfPremiumIndicatorComponent,
IonIcon,
MatButtonModule,
MatDialogModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-subscription-interstitial-dialog',
styleUrls: ['./subscription-interstitial-dialog.scss'],
templateUrl: 'subscription-interstitial-dialog.html',
standalone: false
templateUrl: 'subscription-interstitial-dialog.html'
})
export class SubscriptionInterstitialDialog implements OnInit {
export class GfSubscriptionInterstitialDialogComponent implements OnInit {
private static readonly SKIP_BUTTON_DELAY_IN_SECONDS = 5;
private static readonly VARIANTS_COUNT = 2;
private static readonly VARIANTS_COUNT = 4;
public remainingSkipButtonDelay =
SubscriptionInterstitialDialog.SKIP_BUTTON_DELAY_IN_SECONDS;
GfSubscriptionInterstitialDialogComponent.SKIP_BUTTON_DELAY_IN_SECONDS;
public routerLinkPricing = publicRoutes.pricing.routerLink;
public variantIndex: number;
@ -38,10 +58,10 @@ export class SubscriptionInterstitialDialog implements OnInit {
public constructor(
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: SubscriptionInterstitialDialogParams,
public dialogRef: MatDialogRef<SubscriptionInterstitialDialog>
public dialogRef: MatDialogRef<GfSubscriptionInterstitialDialogComponent>
) {
this.variantIndex = Math.floor(
Math.random() * SubscriptionInterstitialDialog.VARIANTS_COUNT
Math.random() * GfSubscriptionInterstitialDialogComponent.VARIANTS_COUNT
);
addIcons({ arrowForwardOutline, checkmarkCircleOutline });
@ -50,7 +70,9 @@ export class SubscriptionInterstitialDialog implements OnInit {
public ngOnInit() {
interval(ms('1 second'))
.pipe(
take(SubscriptionInterstitialDialog.SKIP_BUTTON_DELAY_IN_SECONDS),
take(
GfSubscriptionInterstitialDialogComponent.SKIP_BUTTON_DELAY_IN_SECONDS
),
tap(() => {
this.remainingSkipButtonDelay--;

83
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html

@ -73,6 +73,89 @@
</div>
}
@case (1) {
<div class="flex-grow-1 pt-0" mat-dialog-content>
<div class="intro-container">
<div class="intro-inner-container mx-auto">
<div class="h-100 intro w-100"></div>
</div>
</div>
<div>
<div class="font-weight-normal h5 text-center" i18n>
Get access to 80’000+ tickers from over 50 exchanges
</div>
<p class="my-2 text-center" i18n>with</p>
<h5 class="align-items-center d-flex justify-content-center">
<span>Ghostfolio Premium</span>
<gf-premium-indicator class="ml-1" [enableLink]="false" />
</h5>
</div>
</div>
<div class="justify-content-end" mat-dialog-actions>
<button
mat-button
[disabled]="remainingSkipButtonDelay > 0"
(click)="closeDialog()"
>
<ng-container i18n>Skip</ng-container>
@if (remainingSkipButtonDelay > 0) {
({{ remainingSkipButtonDelay }})
}
</button>
<a
color="primary"
mat-flat-button
[routerLink]="routerLinkPricing"
(click)="closeDialog()"
>
<span i18n>Upgrade Plan</span>
<ion-icon class="ml-1" name="arrow-forward-outline" />
</a>
</div>
}
@case (2) {
<div class="flex-grow-1 pt-0" mat-dialog-content>
<div class="intro-container">
<div class="intro-inner-container mx-auto">
<div class="h-100 intro w-100"></div>
</div>
</div>
<div>
<h5 class="align-items-center d-flex justify-content-center">
<span>Ghostfolio Premium</span>
<gf-premium-indicator class="ml-1" [enableLink]="false" />
</h5>
<p class="my-2 text-center" i18n>for</p>
<div class="font-weight-normal h5 text-center">
<ng-container i18n>less than</ng-container> $1
<ng-container i18n>per week</ng-container>
</div>
</div>
</div>
<div class="flex-column" mat-dialog-actions>
<button
class="mb-2 py-4 w-100"
mat-button
[disabled]="remainingSkipButtonDelay > 0"
(click)="closeDialog()"
>
<ng-container i18n>Skip</ng-container>
@if (remainingSkipButtonDelay > 0) {
({{ remainingSkipButtonDelay }})
}
</button>
<a
class="m-0 py-4 w-100"
color="primary"
mat-flat-button
[routerLink]="routerLinkPricing"
(click)="closeDialog()"
>
<span i18n>Upgrade Plan</span>
<ion-icon class="ml-1" name="arrow-forward-outline" />
</a>
</div>
}
@case (3) {
<h1
class="align-items-center d-flex justify-content-center"
mat-dialog-title

26
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.module.ts

@ -1,26 +0,0 @@
import { GfMembershipCardComponent } from '@ghostfolio/ui/membership-card';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { SubscriptionInterstitialDialog } from './subscription-interstitial-dialog.component';
@NgModule({
declarations: [SubscriptionInterstitialDialog],
imports: [
CommonModule,
GfMembershipCardComponent,
GfPremiumIndicatorComponent,
IonIcon,
MatButtonModule,
MatDialogModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfSubscriptionInterstitialDialogModule {}

22
apps/client/src/app/pages/markets/markets-page-routing.module.ts

@ -1,22 +0,0 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { MarketsPageComponent } from './markets-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: MarketsPageComponent,
path: '',
title: publicRoutes.markets.title
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class MarketsPageRoutingModule {}

9
apps/client/src/app/pages/markets/markets-page.component.ts

@ -1,14 +1,17 @@
import { HomeMarketComponent } from '@ghostfolio/client/components/home-market/home-market.component';
import { CommonModule } from '@angular/common';
import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
@Component({
host: { class: 'page' },
imports: [CommonModule, HomeMarketComponent],
selector: 'gf-markets-page',
styleUrls: ['./markets-page.scss'],
templateUrl: './markets-page.html',
standalone: false
templateUrl: './markets-page.html'
})
export class MarketsPageComponent implements OnDestroy {
export class GfMarketsPageComponent implements OnDestroy {
private unsubscribeSubject = new Subject<void>();
public ngOnDestroy() {

14
apps/client/src/app/pages/markets/markets-page.module.ts

@ -1,14 +0,0 @@
import { HomeMarketComponent } from '@ghostfolio/client/components/home-market/home-market.component';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MarketsPageRoutingModule } from './markets-page-routing.module';
import { MarketsPageComponent } from './markets-page.component';
@NgModule({
declarations: [MarketsPageComponent],
imports: [CommonModule, HomeMarketComponent, MarketsPageRoutingModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class MarketsPageModule {}

24
apps/client/src/app/pages/markets/markets-page.routes.ts

@ -0,0 +1,24 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { Routes } from '@angular/router';
import { GfMarketsPageComponent } from './markets-page.component';
export const routes: Routes = [
{
canActivate: [AuthGuard],
children: [
{
path: '',
loadComponent: () =>
import('./markets-page.component').then(
(m) => m.GfMarketsPageComponent
)
}
],
component: GfMarketsPageComponent,
path: '',
title: publicRoutes.markets.title
}
];

4
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts

@ -1,5 +1,5 @@
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
import { GfRulesModule } from '@ghostfolio/client/components/rules/rules.module';
import { GfRulesComponent } from '@ghostfolio/client/components/rules/rules.component';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
@ -26,7 +26,7 @@ import { Subject, takeUntil } from 'rxjs';
@Component({
imports: [
GfPremiumIndicatorComponent,
GfRulesModule,
GfRulesComponent,
IonIcon,
NgClass,
NgxSkeletonLoaderModule

23
apps/client/src/app/services/user/user.service.ts

@ -12,7 +12,7 @@ import { throwError } from 'rxjs';
import { catchError, map, takeUntil } from 'rxjs/operators';
import { SubscriptionInterstitialDialogParams } from '../../components/subscription-interstitial-dialog/interfaces/interfaces';
import { SubscriptionInterstitialDialog } from '../../components/subscription-interstitial-dialog/subscription-interstitial-dialog.component';
import { GfSubscriptionInterstitialDialogComponent } from '../../components/subscription-interstitial-dialog/subscription-interstitial-dialog.component';
import { UserStoreActions } from './user-store.actions';
import { UserStoreState } from './user-store.state';
@ -116,15 +116,18 @@ export class UserService extends ObservableStore<UserStoreState> {
permissions.enableSubscriptionInterstitial
)
) {
const dialogRef = this.dialog.open(SubscriptionInterstitialDialog, {
autoFocus: false,
data: {
user
} as SubscriptionInterstitialDialogParams,
disableClose: true,
height: this.deviceType === 'mobile' ? '98vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
const dialogRef = this.dialog.open(
GfSubscriptionInterstitialDialogComponent,
{
autoFocus: false,
data: {
user
} as SubscriptionInterstitialDialogParams,
disableClose: true,
height: this.deviceType === 'mobile' ? '98vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
}
);
dialogRef
.afterClosed()

Loading…
Cancel
Save