Compare commits

..

8 Commits

Author SHA1 Message Date
572dcf075a Release 2.52.0 (#3011) 2024-02-16 20:05:01 +01:00
29cb83d469 Bugfix/improve x axis scale of dividend and investment timeline (#3010)
* Improve X-axis scale

* Update changelog
2024-02-16 20:03:20 +01:00
cac73ac111 Feature/divide faq page in three sections (#3003)
* Divide FAQ page in three sections

* General
* Cloud (SaaS)
* Self-Hosting

* Update changelog
2024-02-16 18:57:34 +01:00
02cf4295a9 Feature/add loading indicator to dividend and investment timelines (#3007)
* Add loading indicators

* Dividend timeline
* Investment timeline

* Update changelog
2024-02-16 09:43:51 +01:00
78b3328bf7 Add activities count (#3005) 2024-02-15 10:25:47 +01:00
e0d6d9e8ca Migrate if / else to control flow (#3001) 2024-02-13 20:46:21 +01:00
54310f2214 Feature/add support for jupiter cryptocurrency (#2999)
* Add JUP29210

* Update changelog
2024-02-13 19:46:15 +01:00
1fec49fbc2 Improve states (#3000) 2024-02-13 19:44:17 +01:00
39 changed files with 1083 additions and 631 deletions

View File

@ -5,6 +5,23 @@ 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).
## 2.52.0 - 2024-02-16
### Added
- Added a loading indicator to the dividend timeline on the analysis page
- Added a loading indicator to the investment timeline on the analysis page
- Added support for the cryptocurrency _Jupiter_ (`JUP29210-USD`)
### Changed
- Divided the content of the Frequently Asked Questions (FAQ) page into three sections: _General_, _Cloud (SaaS)_ and _Self-Hosting_
### Fixed
- Fixed an issue with the X-axis scale of the dividend timeline on the analysis page
- Fixed an issue with the X-axis scale of the investment timeline on the analysis page
## 2.51.0 - 2024-02-12
### Changed

View File

@ -226,7 +226,7 @@ export class AdminService {
this.prismaService.symbolProfile.count({ where })
]);
let marketData = assetProfiles.map(
let marketData: AdminMarketDataItem[] = assetProfiles.map(
({
_count,
assetClass,

View File

@ -1,5 +1,6 @@
{
"CYBER24781": "CyberConnect",
"JUP29210": "Jupiter",
"LUNA1": "Terra",
"LUNA2": "Terra",
"SGB1": "Songbird",

View File

@ -370,6 +370,14 @@
<loc>https://ghostfol.io/en/faq</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/saas</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/self-hosting</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/features</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>

View File

@ -111,6 +111,7 @@ export class AppComponent implements OnDestroy, OnInit {
this.hasTabs =
(this.currentRoute === this.routerLinkAbout[0].slice(1) ||
this.currentRoute === this.routerLinkFaq[0].slice(1) ||
this.currentRoute === 'account' ||
this.currentRoute === 'admin' ||
this.currentRoute === 'home' ||
@ -120,7 +121,6 @@ export class AppComponent implements OnDestroy, OnInit {
this.showFooter =
(this.currentRoute === 'blog' ||
this.currentRoute === this.routerLinkFaq[0].slice(1) ||
this.currentRoute === this.routerLinkFeatures[0].slice(1) ||
this.currentRoute === this.routerLinkMarkets[0].slice(1) ||
this.currentRoute === 'open' ||

View File

@ -131,50 +131,48 @@
<ng-container
*ngIf="assetProfile?.countries?.length > 0 || assetProfile?.sectors?.length > 0"
>
<ng-container
*ngIf="assetProfile?.countries?.length === 1 && assetProfile?.sectors?.length === 1; else charts"
>
<div *ngIf="assetProfile?.sectors?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="assetProfile?.sectors[0].name"
>Sector</gf-value
>
</div>
<div *ngIf="assetProfile?.countries?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="assetProfile?.countries[0].name"
>Country</gf-value
>
</div>
</ng-container>
<ng-template #charts>
<div class="col-md-6 mb-3">
<div class="h5" i18n>Sectors</div>
<gf-portfolio-proportion-chart
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[maxItems]="10"
[positions]="sectors"
/>
</div>
<div class="col-md-6 mb-3">
<div class="h5" i18n>Countries</div>
<gf-portfolio-proportion-chart
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[maxItems]="10"
[positions]="countries"
/>
</div>
</ng-template>
@if (assetProfile?.countries?.length === 1 &&
assetProfile?.sectors?.length === 1 ) {
<div *ngIf="assetProfile?.sectors?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="assetProfile?.sectors[0].name"
>Sector</gf-value
>
</div>
<div *ngIf="assetProfile?.countries?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="assetProfile?.countries[0].name"
>Country</gf-value
>
</div>
} @else {
<div class="col-md-6 mb-3">
<div class="h5" i18n>Sectors</div>
<gf-portfolio-proportion-chart
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[maxItems]="10"
[positions]="sectors"
/>
</div>
<div class="col-md-6 mb-3">
<div class="h5" i18n>Countries</div>
<gf-portfolio-proportion-chart
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[maxItems]="10"
[positions]="countries"
/>
</div>
}
</ng-container>
</div>
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">

View File

@ -1,10 +1,8 @@
<div
class="align-items-center container d-flex flex-column h-100 justify-content-center overview p-0 position-relative"
>
<div
*ngIf="hasPermissionToCreateOrder && historicalDataItems?.length === 0; else isUserActive"
class="justify-content-center row w-100"
>
@if(hasPermissionToCreateOrder && historicalDataItems?.length === 0) {
<div class="justify-content-center row w-100">
<div class="col introduction">
<h4 i18n>Welcome to Ghostfolio</h4>
<p i18n>Ready to take control of your personal finances?</p>
@ -60,43 +58,43 @@
</div>
</div>
</div>
<ng-template #isUserActive>
<div class="row w-100">
<div class="col p-0">
<div class="chart-container mx-auto position-relative">
<gf-line-chart
class="position-absolute"
symbol="Performance"
unit="%"
[colorScheme]="user?.settings?.colorScheme"
[hidden]="historicalDataItems?.length === 0"
[historicalDataItems]="historicalDataItems"
[isAnimated]="user?.settings?.dateRange === '1d' ? false : true"
[locale]="user?.settings?.locale"
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
[showGradient]="true"
[showLoader]="false"
[showXAxis]="false"
[showYAxis]="false"
/>
</div>
</div>
</div>
<div class="overview-container row mt-1">
<div class="col">
<gf-portfolio-performance
class="pb-4"
[deviceType]="deviceType"
[errors]="errors"
[isAllTimeHigh]="isAllTimeHigh"
[isAllTimeLow]="isAllTimeLow"
[isLoading]="isLoadingPerformance"
} @else {
<div class="row w-100">
<div class="col p-0">
<div class="chart-container mx-auto position-relative">
<gf-line-chart
class="position-absolute"
symbol="Performance"
unit="%"
[colorScheme]="user?.settings?.colorScheme"
[hidden]="historicalDataItems?.length === 0"
[historicalDataItems]="historicalDataItems"
[isAnimated]="user?.settings?.dateRange === '1d' ? false : true"
[locale]="user?.settings?.locale"
[performance]="performance"
[showDetails]="showDetails"
[unit]="unit"
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
[showGradient]="true"
[showLoader]="false"
[showXAxis]="false"
[showYAxis]="false"
/>
</div>
</div>
</ng-template>
</div>
<div class="overview-container row mt-1">
<div class="col">
<gf-portfolio-performance
class="pb-4"
[deviceType]="deviceType"
[errors]="errors"
[isAllTimeHigh]="isAllTimeHigh"
[isAllTimeLow]="isAllTimeLow"
[isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale"
[performance]="performance"
[showDetails]="showDetails"
[unit]="unit"
/>
</div>
</div>
}
</div>

View File

@ -38,8 +38,16 @@ import {
} from 'chart.js';
import 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation';
import { addDays, format, isAfter, parseISO, subDays } from 'date-fns';
import { last } from 'lodash';
import {
addDays,
format,
isAfter,
isValid,
min,
parseISO,
subDays
} from 'date-fns';
import { first, last } from 'lodash';
@Component({
selector: 'gf-investment-chart',
@ -143,7 +151,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
});
}
const chartData: ChartData<'line'> = {
const chartData: ChartData<'bar' | 'line'> = {
labels: this.historicalDataItems.map(({ date }) => {
return parseDate(date);
}),
@ -194,17 +202,23 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
};
if (this.chartCanvas) {
let scaleXMin: string;
if (this.daysInMarket) {
const minDate = min([
parseDate(first(this.investments)?.date),
subDays(new Date().setHours(0, 0, 0, 0), this.daysInMarket)
]);
scaleXMin = isValid(minDate) ? minDate.toISOString() : undefined;
}
if (this.chart) {
this.chart.data = chartData;
this.chart.options.plugins.tooltip = <unknown>(
this.getTooltipPluginConfiguration()
);
this.chart.options.scales.x.min = this.daysInMarket
? subDays(
new Date().setHours(0, 0, 0, 0),
this.daysInMarket
).toISOString()
: undefined;
this.chart.options.scales.x.min = scaleXMin;
if (
this.savingsRate &&
@ -287,9 +301,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
grid: {
display: false
},
min: this.daysInMarket
? subDays(new Date(), this.daysInMarket).toISOString()
: undefined,
min: scaleXMin,
suggestedMax: new Date().toISOString(),
type: 'time',
time: {

View File

@ -186,57 +186,52 @@
<ng-container
*ngIf="SymbolProfile?.countries?.length > 0 || SymbolProfile?.sectors?.length > 0"
>
<ng-container
*ngIf="SymbolProfile?.countries?.length === 1 && SymbolProfile?.sectors?.length === 1; else charts"
>
<div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="SymbolProfile.sectors[0].name"
>Sector</gf-value
>
</div>
<div
*ngIf="SymbolProfile?.countries?.length === 1"
class="col-6 mb-3"
@if(SymbolProfile?.countries?.length === 1 &&
SymbolProfile?.sectors?.length === 1) {
<div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="SymbolProfile.sectors[0].name"
>Sector</gf-value
>
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="SymbolProfile.countries[0].name"
>Country</gf-value
>
</div>
</ng-container>
<ng-template #charts>
<div class="col-md-6 mb-3">
<div class="h5" i18n>Sectors</div>
<gf-portfolio-proportion-chart
[baseCurrency]="data.baseCurrency"
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[locale]="data.locale"
[maxItems]="10"
[positions]="sectors"
/>
</div>
<div class="col-md-6 mb-3">
<div class="h5" i18n>Countries</div>
<gf-portfolio-proportion-chart
[baseCurrency]="data.baseCurrency"
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[locale]="data.locale"
[maxItems]="10"
[positions]="countries"
/>
</div>
</ng-template>
</div>
<div *ngIf="SymbolProfile?.countries?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="SymbolProfile.countries[0].name"
>Country</gf-value
>
</div>
} @else {
<div class="col-md-6 mb-3">
<div class="h5" i18n>Sectors</div>
<gf-portfolio-proportion-chart
[baseCurrency]="data.baseCurrency"
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[locale]="data.locale"
[maxItems]="10"
[positions]="sectors"
/>
</div>
<div class="col-md-6 mb-3">
<div class="h5" i18n>Countries</div>
<gf-portfolio-proportion-chart
[baseCurrency]="data.baseCurrency"
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[locale]="data.locale"
[maxItems]="10"
[positions]="countries"
/>
</div>
}
</ng-container>
<div *ngIf="dataProviderInfo" class="col-md-12 mb-3 text-center">
<hr />

View File

@ -3,7 +3,6 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import * as path from 'path';
import { AboutPageComponent } from './about-page.component';

View File

@ -8,7 +8,7 @@ import { AboutPageComponent } from './about-page.component';
@NgModule({
declarations: [AboutPageComponent],
imports: [CommonModule, MatTabsModule, AboutPageRoutingModule, RouterModule],
imports: [AboutPageRoutingModule, CommonModule, MatTabsModule, RouterModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AboutPageModule {}

View File

@ -8,6 +8,27 @@ import { FaqPageComponent } from './faq-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
children: [
{
path: '',
loadChildren: () =>
import('./overview/faq-overview-page.module').then(
(m) => m.FaqOverviewPageModule
)
},
{
path: 'saas',
loadChildren: () =>
import('./saas/saas-page.module').then((m) => m.SaasPageModule)
},
{
path: 'self-hosting',
loadChildren: () =>
import('./self-hosting/self-hosting-page.module').then(
(m) => m.SelfHostingPageModule
)
}
],
component: FaqPageComponent,
path: '',
title: $localize`Frequently Asked Questions (FAQ)`

View File

@ -1,39 +1,57 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { User } from '@ghostfolio/common/interfaces';
import { DataService } from '@ghostfolio/client/services/data.service';
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
@Component({
host: { class: 'page' },
host: { class: 'page has-tabs' },
selector: 'gf-faq-page',
styleUrls: ['./faq-page.scss'],
templateUrl: './faq-page.html'
})
export class FaqPageComponent implements OnDestroy {
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkMarkets = ['/' + $localize`markets`];
public routerLinkPricing = ['/' + $localize`pricing`];
public routerLinkRegister = ['/' + $localize`register`];
public user: User;
export class FaqPageComponent implements OnDestroy, OnInit {
public deviceType: string;
public hasPermissionForSubscription: boolean;
public tabs: TabConfiguration[] = [];
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private userService: UserService
) {}
private dataService: DataService,
private deviceService: DeviceDetectorService
) {
const { globalPermissions } = this.dataService.fetchInfo();
this.hasPermissionForSubscription = hasPermission(
globalPermissions,
permissions.enableSubscription
);
this.tabs = [
{
iconName: 'reader-outline',
label: $localize`General`,
path: ['/' + $localize`faq`]
},
{
iconName: 'cloudy-outline',
label: $localize`Cloud` + ' (SaaS)',
path: ['/' + $localize`faq`, 'saas'],
showCondition: this.hasPermissionForSubscription
},
{
iconName: 'server-outline',
label: $localize`Self-Hosting`,
path: ['/' + $localize`faq`, $localize`self-hosting`]
}
];
}
public ngOnInit() {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.changeDetectorRef.markForCheck();
}
});
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
}
public ngOnDestroy() {

View File

@ -1,291 +1,29 @@
<div class="container">
<div class="mb-5 row">
<div class="col">
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>
Frequently Asked Questions (FAQ)
</h1>
<p>
Find quick answers to commonly asked questions about Ghostfolio in our
Frequently Asked Questions (FAQ) section. Discover what Ghostfolio is,
explore its features, and learn about our privacy practices. Get all the
information you need in one place.
</p>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>What is Ghostfolio?</mat-card-title>
</mat-card-header>
<mat-card-content>
Ghostfolio is a lightweight, open source wealth management application
for individuals to keep track of their net worth. The software
empowers you to make solid, data-driven investment decisions.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>What assets can I track with Ghostfolio?</mat-card-title
>
</mat-card-header>
<mat-card-content>
With Ghostfolio, you can keep track of various assets like stocks,
ETFs, bonds, cryptocurrencies and commodities.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>What else is included in Ghostfolio?</mat-card-title
></mat-card-header
>
<mat-card-content>
Please find a feature overview to manage your wealth
<a [routerLink]="routerLinkFeatures">here</a>.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>How do I start?</mat-card-title>
</mat-card-header>
<mat-card-content>
You can sign up via the “<a [routerLink]="routerLinkRegister"
>Get Started</a
>” button at the top of the page. You have multiple options to join
Ghostfolio: Create an account with a security token or
<i>Google Sign</i>. We will guide you to set up your portfolio.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>Will you spam me with emails once I sign up?</mat-card-title
></mat-card-header
>
<mat-card-content>
No, we do not even collect your email address, so you will not receive
any spam emails from us.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>Can I use Ghostfolio anonymously?</mat-card-title
></mat-card-header
>
<mat-card-content>
Yes, the authentication system via security token enables you to sign
in securely and anonymously to Ghostfolio. There is no need for an
e-mail address, phone number, or a username.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>How can Ghostfolio be free?</mat-card-title
></mat-card-header
>
<mat-card-content
>This project is driven by the efforts of contributors from around the
world. The
<a href="https://github.com/ghostfolio/ghostfolio">source code</a> is
fully available as open source software (OSS). Thanks to our generous
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> users and
<a href="https://www.buymeacoffee.com/ghostfolio">sponsors</a> we have
the ability to run a free, limited plan for novice
investors.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Is it really free?</mat-card-title></mat-card-header
>
<mat-card-content
>Yes, it is! Our
<a [routerLink]="routerLinkPricing">pricing page</a> details
everything you get for free.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>Do you monetize or sell my financial data?</mat-card-title
></mat-card-header
>
<mat-card-content
>No, we value your privacy. We do not sell or share your financial
data with any third parties.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>What is your business model?</mat-card-title
></mat-card-header
>
<mat-card-content
>By offering
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a>, a
subscription plan with a managed hosting service and enhanced
features, we fund our business while providing added value to our
users.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>What is Ghostfolio Premium?</mat-card-title
></mat-card-header
>
<mat-card-content
><a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> is a fully
managed Ghostfolio cloud offering for ambitious investors. Revenue is
used to cover the costs of the hosting infrastructure and to fund
ongoing development. It is the Open Source code base with some extras
like the <a [routerLink]="routerLinkMarkets">markets overview</a> and
a professional data provider.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>Can I start with a trial version?</mat-card-title
></mat-card-header
>
<mat-card-content
>Yes, you can try
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> by signing
up for Ghostfolio and applying for a trial (see “My Ghostfolio”). It
is easy, free and there is no commitment. You can stop using it at any
time.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>How can I get a student discount for Ghostfolio
Premium?</mat-card-title
>
</mat-card-header>
<mat-card-content
>Request your student discount
<a href="mailto:hi@ghostfol.io?Subject=Student Discount">here</a> with
your university e-mail address.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>Does the Ghostfolio Premium subscription renew
automatically?</mat-card-title
>
</mat-card-header>
<mat-card-content
>No, <a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> does
not include auto-renewal. Upon expiration, you can choose whether to
start a new subscription.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Which devices are supported?</mat-card-title>
</mat-card-header>
<mat-card-content
>Ghostfolio works in every modern web browser on smartphones, tablets
and desktop computers. For <i>Android</i> users, there is a dedicated
Ghostfolio app available in the
<a
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
>Google Play Store</a
>.</mat-card-content
>
</mat-card>
<mat-card
*ngIf="user?.subscription?.type === 'Premium'"
appearance="outlined"
class="mb-3"
>
<mat-card-header>
<mat-card-title
>I cannot find my broker in the list of platforms. What can I
do?</mat-card-title
>
</mat-card-header>
<mat-card-content>
Please send an e-mail with the web address of your broker to
<a href="mailto:hi@ghostfol.io">hi&#64;ghostfol.io</a> and we are
happy to add it.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>Ghostfolio sounds cool, how can I get involved?</mat-card-title
>
</mat-card-header>
<mat-card-content
>Any support for Ghostfolio is welcome. Be it with a
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a>
subscription to finance the hosting infrastructure, a positive rating
in the
<a
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
>Google Play Store</a
>, a star on
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>,
feedback, bug reports, feature requests and of course contributions!
You can reach us via Ghostfolio
<a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
title="Join the Ghostfolio Slack community"
>Slack</a
>
community,
<a
href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)"
>&#64;ghostfolio_</a
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
>,
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
>hi&#64;ghostfol.io</a
></ng-container
>
or
<a
href="https://github.com/ghostfolio/ghostfolio"
title="Find Ghostfolio on GitHub"
>GitHub</a
>.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Got any other questions?</mat-card-title>
</mat-card-header>
<mat-card-content
>Please join the Ghostfolio
<a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
title="Join the Ghostfolio Slack community"
>Slack </a
>community, post to
<a
href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)"
>&#64;ghostfolio_</a
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
>, send an e-mail to
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
>hi&#64;ghostfol.io</a
></ng-container
>
or start a discussion at
<a
href="https://github.com/ghostfolio/ghostfolio"
title="Find Ghostfolio on GitHub"
>GitHub</a
>.</mat-card-content
>
</mat-card>
</div>
</div>
</div>
<mat-tab-nav-panel #tabPanel class="flex-grow-1 overflow-auto">
<router-outlet></router-outlet>
</mat-tab-nav-panel>
<nav
mat-align-tabs="center"
mat-tab-nav-bar
[disablePagination]="true"
[tabPanel]="tabPanel"
>
<ng-container *ngFor="let tab of tabs">
<a
#rla="routerLinkActive"
*ngIf="tab.showCondition !== false"
class="no-min-width px-3"
mat-tab-link
routerLinkActive
[active]="rla.isActive"
[routerLink]="tab.path"
[routerLinkActiveOptions]="{ exact: true }"
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large': 'small'"
/>
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a>
</ng-container>
</nav>

View File

@ -1,13 +1,14 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { FaqPageRoutingModule } from './faq-page-routing.module';
import { FaqPageComponent } from './faq-page.component';
@NgModule({
declarations: [FaqPageComponent],
imports: [CommonModule, FaqPageRoutingModule, MatCardModule],
imports: [CommonModule, FaqPageRoutingModule, MatTabsModule, RouterModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class FaqPageModule {}

View File

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

View File

@ -0,0 +1,21 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { FaqOverviewPageComponent } from './faq-overview-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: FaqOverviewPageComponent,
path: '',
title: $localize`Frequently Asked Questions (FAQ)`
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class FaqOverviewPageRoutingModule {}

View File

@ -0,0 +1,41 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { User } from '@ghostfolio/common/interfaces';
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
@Component({
host: { class: 'page' },
selector: 'gf-faq-overview-page',
styleUrls: ['./faq-overview-page.scss'],
templateUrl: './faq-overview-page.html'
})
export class FaqOverviewPageComponent implements OnDestroy {
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkPricing = ['/' + $localize`pricing`];
public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private userService: UserService
) {}
public ngOnInit() {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

@ -0,0 +1,172 @@
<div class="container">
<div class="mb-5 row">
<div class="col">
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>
Frequently Asked Questions (FAQ)
</h1>
<p>
Find quick answers to commonly asked questions about Ghostfolio in our
Frequently Asked Questions (FAQ) section. Discover what Ghostfolio is,
explore its features, and learn about our privacy practices. Get all the
information you need in one place.
</p>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>What is Ghostfolio?</mat-card-title>
</mat-card-header>
<mat-card-content>
Ghostfolio is a lightweight, open source wealth management application
for individuals to keep track of their net worth. The software
empowers you to make solid, data-driven investment decisions.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>What assets can I track with Ghostfolio?</mat-card-title
>
</mat-card-header>
<mat-card-content>
With Ghostfolio, you can keep track of various assets like stocks,
ETFs, bonds, cryptocurrencies and commodities.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>What else is included in Ghostfolio?</mat-card-title
></mat-card-header
>
<mat-card-content>
Please find a feature overview to manage your wealth
<a [routerLink]="routerLinkFeatures">here</a>.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>Can I use Ghostfolio anonymously?</mat-card-title
></mat-card-header
>
<mat-card-content>
Yes, the authentication system via security token enables you to sign
in securely and anonymously to Ghostfolio. There is no need for an
e-mail address, phone number, or a username.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>How can Ghostfolio be free?</mat-card-title
></mat-card-header
>
<mat-card-content
>This project is driven by the efforts of contributors from around the
world. The
<a href="https://github.com/ghostfolio/ghostfolio">source code</a> is
fully available as open source software (OSS). Thanks to our generous
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> users and
<a href="https://www.buymeacoffee.com/ghostfolio">sponsors</a> we have
the ability to run a free, limited plan for novice
investors.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>Do you monetize or sell my financial data?</mat-card-title
></mat-card-header
>
<mat-card-content
>No, we value your privacy. We do not sell or share your financial
data with any third parties.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>What is your business model?</mat-card-title
></mat-card-header
>
<mat-card-content
>By offering
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a>, a
subscription plan with a managed hosting service and enhanced
features, we fund our business while providing added value to our
users.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>Ghostfolio sounds cool, how can I get involved?</mat-card-title
>
</mat-card-header>
<mat-card-content
>Any support for Ghostfolio is welcome. Be it with a
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a>
subscription to finance the hosting infrastructure, a positive rating
in the
<a
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
>Google Play Store</a
>, a star on
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>,
feedback, bug reports, feature requests and of course contributions!
You can reach us via Ghostfolio
<a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
title="Join the Ghostfolio Slack community"
>Slack</a
>
community,
<a
href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)"
>&#64;ghostfolio_</a
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
>,
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
>hi&#64;ghostfol.io</a
></ng-container
>
or
<a
href="https://github.com/ghostfolio/ghostfolio"
title="Find Ghostfolio on GitHub"
>GitHub</a
>.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Got any other questions?</mat-card-title>
</mat-card-header>
<mat-card-content
>Please join the Ghostfolio
<a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
title="Join the Ghostfolio Slack community"
>Slack </a
>community, post to
<a
href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)"
>&#64;ghostfolio_</a
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
>, send an e-mail to
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
>hi&#64;ghostfol.io</a
></ng-container
>
or start a discussion at
<a
href="https://github.com/ghostfolio/ghostfolio"
title="Find Ghostfolio on GitHub"
>GitHub</a
>.</mat-card-content
>
</mat-card>
</div>
</div>
</div>

View File

@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { FaqOverviewPageRoutingModule } from './faq-overview-page-routing.module';
import { FaqOverviewPageComponent } from './faq-overview-page.component';
@NgModule({
declarations: [FaqOverviewPageComponent],
imports: [CommonModule, FaqOverviewPageRoutingModule, MatCardModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class FaqOverviewPageModule {}

View File

@ -0,0 +1,12 @@
:host {
display: block;
a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
&:hover {
color: rgba(var(--palette-primary-300), 1);
}
}
}

View File

@ -0,0 +1,21 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SaasPageComponent } from './saas-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: SaasPageComponent,
path: '',
title: $localize`Cloud` + ' (SaaS) ' + $localize`FAQ`
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SaasPageRoutingModule {}

View File

@ -0,0 +1,42 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { User } from '@ghostfolio/common/interfaces';
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
@Component({
host: { class: 'page' },
selector: 'gf-saas-page',
styleUrls: ['./saas-page.scss'],
templateUrl: './saas-page.html'
})
export class SaasPageComponent implements OnDestroy {
public routerLinkMarkets = ['/' + $localize`markets`];
public routerLinkPricing = ['/' + $localize`pricing`];
public routerLinkRegister = ['/' + $localize`register`];
public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private userService: UserService
) {}
public ngOnInit() {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

@ -0,0 +1,162 @@
<div class="container">
<div class="mb-5 row">
<div class="col">
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>
Frequently Asked Questions (FAQ)
</h1>
<p>
Find quick answers to commonly asked questions about the fully managed
Ghostfolio cloud offering in our Frequently Asked Questions (FAQ)
section.
</p>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>How do I start?</mat-card-title>
</mat-card-header>
<mat-card-content>
You can sign up via the “<a [routerLink]="routerLinkRegister"
>Get Started</a
>” button at the top of the page. You have multiple options to join
Ghostfolio: Create an account with a security token or
<i>Google Sign</i>. We will guide you to set up your portfolio.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>Will you spam me with emails once I sign up?</mat-card-title
></mat-card-header
>
<mat-card-content>
No, we do not even collect your email address, so you will not receive
any spam emails from us.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Is it really free?</mat-card-title></mat-card-header
>
<mat-card-content
>Yes, it is! Our
<a [routerLink]="routerLinkPricing">pricing page</a> details
everything you get for free.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>What is Ghostfolio Premium?</mat-card-title
></mat-card-header
>
<mat-card-content
><a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> is a fully
managed Ghostfolio cloud offering for ambitious investors. Revenue is
used to cover the costs of the hosting infrastructure and to fund
ongoing development. It is the Open Source code base with some extras
like the <a [routerLink]="routerLinkMarkets">markets overview</a> and
a professional data provider.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>Can I start with a trial version?</mat-card-title
></mat-card-header
>
<mat-card-content
>Yes, you can try
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> by signing
up for Ghostfolio and applying for a trial (see “My Ghostfolio”). It
is easy, free and there is no commitment. You can stop using it at any
time.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>How can I get a student discount for Ghostfolio
Premium?</mat-card-title
>
</mat-card-header>
<mat-card-content
>Request your student discount
<a href="mailto:hi@ghostfol.io?Subject=Student Discount">here</a> with
your university e-mail address.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>Does the Ghostfolio Premium subscription renew
automatically?</mat-card-title
>
</mat-card-header>
<mat-card-content
>No, <a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> does
not include auto-renewal. Upon expiration, you can choose whether to
start a new subscription.</mat-card-content
>
</mat-card>
<mat-card
*ngIf="user?.subscription?.type === 'Premium'"
appearance="outlined"
class="mb-3"
>
<mat-card-header>
<mat-card-title
>I cannot find my broker in the list of platforms. What can I
do?</mat-card-title
>
</mat-card-header>
<mat-card-content>
Please send an e-mail with the web address of your broker to
<a href="mailto:hi@ghostfol.io">hi&#64;ghostfol.io</a> and we are
happy to add it.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Which devices are supported?</mat-card-title>
</mat-card-header>
<mat-card-content
>Ghostfolio works in every modern web browser on smartphones, tablets
and desktop computers. For <i>Android</i> users of the managed cloud
offering, there is a dedicated Ghostfolio app available in the
<a
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
>Google Play Store</a
>.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Got any other questions?</mat-card-title>
</mat-card-header>
<mat-card-content
>Please join the Ghostfolio
<a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
title="Join the Ghostfolio Slack community"
>Slack </a
>community, post to
<a
href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)"
>&#64;ghostfolio_</a
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
>, send an e-mail to
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
>hi&#64;ghostfol.io</a
></ng-container
>
or start a discussion at
<a
href="https://github.com/ghostfolio/ghostfolio"
title="Find Ghostfolio on GitHub"
>GitHub</a
>.</mat-card-content
>
</mat-card>
</div>
</div>
</div>

View File

@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { SaasPageRoutingModule } from './saas-page-routing.module';
import { SaasPageComponent } from './saas-page.component';
@NgModule({
declarations: [SaasPageComponent],
imports: [CommonModule, MatCardModule, SaasPageRoutingModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class SaasPageModule {}

View File

@ -0,0 +1,12 @@
:host {
display: block;
a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
&:hover {
color: rgba(var(--palette-primary-300), 1);
}
}
}

View File

@ -0,0 +1,21 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SelfHostingPageComponent } from './self-hosting-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: SelfHostingPageComponent,
path: '',
title: $localize`Self-Hosting` + ' ' + $localize`FAQ`
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SelfHostingPageRoutingModule {}

View File

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

View File

@ -0,0 +1,56 @@
<div class="container">
<div class="mb-5 row">
<div class="col">
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>
Frequently Asked Questions (FAQ)
</h1>
<p>
Find quick answers to commonly asked questions about self-hosting
Ghostfolio in our Frequently Asked Questions (FAQ) section.
</p>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>How do I start?</mat-card-title>
</mat-card-header>
<mat-card-content>
If you prefer to run Ghostfolio on your own infrastructure, please
find the source code and further instructions on
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Which devices are supported?</mat-card-title>
</mat-card-header>
<mat-card-content
>Ghostfolio works in every modern web browser on smartphones, tablets
and desktop computers.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Got any other questions?</mat-card-title>
</mat-card-header>
<mat-card-content
>Please join the Ghostfolio
<a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
title="Join the Ghostfolio Slack community"
>Slack </a
>community, post to
<a
href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)"
>&#64;ghostfolio_</a
>
or start a discussion at
<a
href="https://github.com/ghostfolio/ghostfolio"
title="Find Ghostfolio on GitHub"
>GitHub</a
>.</mat-card-content
>
</mat-card>
</div>
</div>
</div>

View File

@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { SelfHostingPageRoutingModule } from './self-hosting-page-routing.module';
import { SelfHostingPageComponent } from './self-hosting-page.component';
@NgModule({
declarations: [SelfHostingPageComponent],
imports: [CommonModule, MatCardModule, SelfHostingPageRoutingModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class SelfHostingPageModule {}

View File

@ -0,0 +1,12 @@
:host {
display: block;
a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
&:hover {
color: rgba(var(--palette-primary-300), 1);
}
}
}

View File

@ -24,84 +24,83 @@
>
</ng-template>
<div class="pt-3">
<ng-container *ngIf="mode === 'DIVIDEND'; else selectFile">
<form
[formGroup]="uniqueAssetForm"
(ngSubmit)="onLoadDividends(stepper)"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Holding</mat-label>
<mat-select formControlName="uniqueAsset">
<mat-select-trigger
>{{ uniqueAssetForm.controls['uniqueAsset']?.value?.name
}}</mat-select-trigger
>
<mat-option
*ngFor="let holding of holdings"
class="line-height-1"
[value]="{ dataSource: holding.dataSource, name: holding.name, symbol: holding.symbol }"
>
<span><b>{{ holding.name }}</b></span>
<br />
<small class="text-muted"
>{{ holding.symbol | gfSymbol }} · {{ holding.currency
}}</small
>
</mat-option>
</mat-select>
<mat-spinner
*ngIf="isLoading"
class="position-absolute"
[diameter]="20"
/>
</mat-form-field>
<div class="d-flex flex-column justify-content-center">
<button
color="primary"
mat-flat-button
type="submit"
[disabled]="!uniqueAssetForm.valid"
@if (mode === 'DIVIDEND') {
<form
[formGroup]="uniqueAssetForm"
(ngSubmit)="onLoadDividends(stepper)"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Holding</mat-label>
<mat-select formControlName="uniqueAsset">
<mat-select-trigger
>{{ uniqueAssetForm.controls['uniqueAsset']?.value?.name
}}</mat-select-trigger
>
<span i18n>Load Dividends</span>
</button>
</div>
</form>
</ng-container>
<ng-template #selectFile>
<mat-option
*ngFor="let holding of holdings"
class="line-height-1"
[value]="{ dataSource: holding.dataSource, name: holding.name, symbol: holding.symbol }"
>
<span><b>{{ holding.name }}</b></span>
<br />
<small class="text-muted"
>{{ holding.symbol | gfSymbol }} · {{ holding.currency
}}</small
>
</mat-option>
</mat-select>
<mat-spinner
*ngIf="isLoading"
class="position-absolute"
[diameter]="20"
/>
</mat-form-field>
<div class="d-flex flex-column justify-content-center">
<button
class="drop-area p-4 text-center text-muted"
gfFileDrop
(click)="onSelectFile(stepper)"
(filesDropped)="onFilesDropped({stepper, files: $event})"
color="primary"
mat-flat-button
type="submit"
[disabled]="!uniqueAssetForm.valid"
>
<div
class="align-items-center d-flex flex-column justify-content-center"
>
<ion-icon class="cloud-icon" name="cloud-upload-outline" />
<span i18n>Choose or drop a file here</span>
</div>
<span i18n>Load Dividends</span>
</button>
<p class="mb-0 mt-3 text-center">
<small>
<span class="mr-1" i18n
>The following file formats are supported:</span
>
<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
target="_blank"
>CSV</a
>
<span class="mx-1" i18n>or</span>
<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
target="_blank"
>JSON</a
>
</small>
</p>
</div>
</ng-template>
</form>
} @else {
<div class="d-flex flex-column justify-content-center">
<button
class="drop-area p-4 text-center text-muted"
gfFileDrop
(click)="onSelectFile(stepper)"
(filesDropped)="onFilesDropped({stepper, files: $event})"
>
<div
class="align-items-center d-flex flex-column justify-content-center"
>
<ion-icon class="cloud-icon" name="cloud-upload-outline" />
<span i18n>Choose or drop a file here</span>
</div>
</button>
<p class="mb-0 mt-3 text-center">
<small>
<span class="mr-1" i18n
>The following file formats are supported:</span
>
<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
target="_blank"
>CSV</a
>
<span class="mx-1" i18n>or</span>
<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
target="_blank"
>JSON</a
>
</small>
</p>
</div>
}
</div>
</mat-step>
@ -115,79 +114,78 @@
>
</ng-template>
<div class="pt-3">
<ng-container *ngIf="errorMessages?.length === 0; else errorMessage">
<gf-activities-table
*ngIf="importStep === 1"
[baseCurrency]="data?.user?.settings?.baseCurrency"
[dataSource]="dataSource"
[deviceType]="data?.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="false"
[hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false"
[locale]="data?.user?.settings?.locale"
[pageSize]="maxSafeInteger"
[showActions]="false"
[showCheckbox]="true"
[showFooter]="false"
[showSymbolColumn]="false"
[sortColumn]="sortColumn"
[sortDirection]="sortDirection"
[sortDisabled]="true"
[totalItems]="totalItems"
(selectedActivities)="updateSelection($event)"
/>
<div class="d-flex justify-content-end mt-3">
<button mat-button (click)="onReset(stepper)">
<ng-container i18n>Back</ng-container>
</button>
<button
class="ml-1"
color="primary"
mat-flat-button
[disabled]="!selectedActivities?.length"
(click)="onImportActivities()"
>
<ng-container i18n>Import</ng-container>
</button>
</div>
</ng-container>
<ng-template #errorMessage>
<mat-accordion displayMode="flat">
<mat-expansion-panel
*ngFor="let message of errorMessages; let i = index"
[disabled]="!details[i]"
>
<mat-expansion-panel-header class="pl-1">
<mat-panel-title>
<div class="d-flex">
<div class="align-items-center d-flex mr-2">
<ion-icon name="warning-outline" />
</div>
<div>{{ message }}</div>
@if(errorMessages?.length === 0) {
<gf-activities-table
*ngIf="importStep === 1"
[baseCurrency]="data?.user?.settings?.baseCurrency"
[dataSource]="dataSource"
[deviceType]="data?.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="false"
[hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false"
[locale]="data?.user?.settings?.locale"
[pageSize]="maxSafeInteger"
[showActions]="false"
[showCheckbox]="true"
[showFooter]="false"
[showSymbolColumn]="false"
[sortColumn]="sortColumn"
[sortDirection]="sortDirection"
[sortDisabled]="true"
[totalItems]="totalItems"
(selectedActivities)="updateSelection($event)"
/>
<div class="d-flex justify-content-end mt-3">
<button mat-button (click)="onReset(stepper)">
<ng-container i18n>Back</ng-container>
</button>
<button
class="ml-1"
color="primary"
mat-flat-button
[disabled]="!selectedActivities?.length"
(click)="onImportActivities()"
>
<ng-container i18n>Import</ng-container>
</button>
</div>
} @else {
<mat-accordion displayMode="flat">
<mat-expansion-panel
*ngFor="let message of errorMessages; let i = index"
[disabled]="!details[i]"
>
<mat-expansion-panel-header class="pl-1">
<mat-panel-title>
<div class="d-flex">
<div class="align-items-center d-flex mr-2">
<ion-icon name="warning-outline" />
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<pre
*ngIf="details[i]"
class="m-0"
><code>{{ details[i] | json }}</code></pre>
</mat-expansion-panel>
</mat-accordion>
<div class="d-flex justify-content-end mt-3">
<button mat-button (click)="onReset(stepper)">
<ng-container i18n>Back</ng-container>
</button>
<button
class="ml-1"
color="primary"
mat-flat-button
[disabled]="true"
>
<ng-container i18n>Import</ng-container>
</button>
</div>
</ng-template>
<div>{{ message }}</div>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<pre
*ngIf="details[i]"
class="m-0"
><code>{{ details[i] | json }}</code></pre>
</mat-expansion-panel>
</mat-accordion>
<div class="d-flex justify-content-end mt-3">
<button mat-button (click)="onReset(stepper)">
<ng-container i18n>Back</ng-container>
</button>
<button
class="ml-1"
color="primary"
mat-flat-button
[disabled]="true"
>
<ng-container i18n>Import</ng-container>
</button>
</div>
}
</div>
</mat-step>
</mat-stepper>

View File

@ -46,7 +46,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public investmentTimelineDataLabel = $localize`Investment`;
public investmentsByGroup: InvestmentItem[];
public isLoadingBenchmarkComparator: boolean;
public isLoadingDividendTimelineChart: boolean;
public isLoadingInvestmentChart: boolean;
public isLoadingInvestmentTimelineChart: boolean;
public mode: GroupBy = 'month';
public modeOptions: ToggleOption[] = [
{ label: $localize`Monthly`, value: 'month' },
@ -154,6 +156,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
}
private fetchDividendsAndInvestments() {
this.isLoadingDividendTimelineChart = true;
this.isLoadingInvestmentTimelineChart = true;
this.dataService
.fetchDividends({
filters: this.userService.getFilters(),
@ -164,6 +169,8 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
.subscribe(({ dividends }) => {
this.dividendsByGroup = dividends;
this.isLoadingDividendTimelineChart = false;
this.changeDetectorRef.markForCheck();
});
@ -194,6 +201,8 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
? translate('MONTH')
: translate('MONTHS');
this.isLoadingInvestmentTimelineChart = false;
this.changeDetectorRef.markForCheck();
});
}

View File

@ -294,6 +294,7 @@
[daysInMarket]="daysInMarket"
[groupBy]="mode"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[isLoading]="isLoadingInvestmentTimelineChart"
[locale]="user?.settings?.locale"
[range]="user?.settings?.dateRange"
[savingsRate]="savingsRate"
@ -331,6 +332,7 @@
[daysInMarket]="daysInMarket"
[groupBy]="mode"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[isLoading]="isLoadingDividendTimelineChart"
[locale]="user?.settings?.locale"
[range]="user?.settings?.dateRange"
/>

View File

@ -48,11 +48,9 @@ export class FirePageComponent implements OnDestroy, OnInit {
.fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ summary }) => {
if (summary.cash === null || summary.currentValue === null) {
return;
}
this.fireWealth = new Big(summary.fireWealth);
this.fireWealth = summary.fireWealth
? new Big(summary.fireWealth)
: new Big(10000);
this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100);
this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12);
@ -94,10 +92,13 @@ export class FirePageComponent implements OnDestroy, OnInit {
permissions.createOrder
);
this.hasPermissionToUpdateUserSettings = hasPermission(
this.user.permissions,
permissions.updateUserSettings
);
this.hasPermissionToUpdateUserSettings =
this.user.subscription?.type === 'Basic'
? false
: hasPermission(
this.user.permissions,
permissions.updateUserSettings
);
this.changeDetectorRef.markForCheck();
}

View File

@ -18,6 +18,10 @@
[fireWealth]="fireWealth?.toNumber()"
[hasPermissionToUpdateUserSettings]="!hasImpersonationId && hasPermissionToUpdateUserSettings"
[locale]="user?.settings?.locale"
[ngStyle]="{
opacity: user?.subscription?.type === 'Basic' ? '0.67' : 'initial',
'pointer-events': user?.subscription?.type === 'Basic' ? 'none' : 'initial'
}"
[projectedTotalAmount]="user?.settings?.projectedTotalAmount"
[retirementDate]="user?.settings?.retirementDate"
[savingsRate]="user?.settings?.savingsRate"
@ -54,7 +58,11 @@
}"
/>
</div>
<div *ngIf="!isLoading" i18n>
<div
*ngIf="!isLoading"
i18n
[ngClass]="{ 'text-muted': user?.subscription?.type === 'Basic' }"
>
If you retire today, you would be able to withdraw
<span class="font-weight-bold"
><gf-value

View File

@ -6,6 +6,7 @@ export interface AdminMarketData {
}
export interface AdminMarketDataItem {
activitiesCount?: number;
assetClass?: AssetClass;
assetSubClass?: AssetSubClass;
countriesCount: number;

View File

@ -13,7 +13,7 @@
.mdc-text-field--disabled {
.mdc-floating-label,
.mdc-text-field__input {
color: inherit;
color: inherit !important;
}
.mdc-notched-outline__leading,

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.51.0",
"version": "2.52.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",