Compare commits

..

7 Commits

Author SHA1 Message Date
c69686651e Release 2.22.0 (#2638) 2023-11-11 18:59:43 +01:00
93b6011ddc Feature/refactor get range in market data service (#2631)
* Refactor to unique asset in getRange()

* Update changelog
2023-11-11 18:57:41 +01:00
f567e25f27 Feature/introduce action menus in overview of action control panel (#2637)
* Introduce action menus

* Exchange rates management
* Coupons management

* Update changelog
2023-11-11 18:48:23 +01:00
5dc538bafb Feature/optimize testimonial carousel style on mobile (#2634)
* Optimize style on mobile

* Update changelog
2023-11-11 17:29:59 +01:00
b4de06fcf0 Feature/add platform icons to account selectors (#2633)
* Add platform icons to account selectors

* Update changelog
2023-11-11 17:27:29 +01:00
27da0eb26e Feature/harmonize name column of historical market data table (#2632)
* Harmonize name column

* Update changelog
2023-11-11 09:02:12 +01:00
8ff80c10e5 Feature/extend personal finance tools pages 20231110 (#2630)
* Add Magnifi

* Add Basil Finance
2023-11-11 09:01:58 +01:00
26 changed files with 328 additions and 71 deletions

View File

@ -5,6 +5,20 @@ 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.22.0 - 2023-11-11
### Added
- Added the platform icon to the account selectors in the cash balance transfer from one to another account
- Added the platform icon to the account selector of the create or edit activity dialog
### Changed
- Optimized the style of the carousel component on mobile for the testimonial section on the landing page
- Introduced action menus in the overview of the admin control panel
- Harmonized the name column in the historical market data table of the admin control panel
- Refactored the implementation of the data range functionality (`getRange()`) in the market data service
## 2.21.0 - 2023-11-09
### Changed

View File

@ -61,6 +61,7 @@ export const CurrentRateServiceMock = {
for (const dataGatheringItem of dataGatheringItems) {
values.push({
date,
dataSource: dataGatheringItem.dataSource,
marketPriceInBaseCurrency: mockGetValue(
dataGatheringItem.symbol,
date
@ -74,6 +75,7 @@ export const CurrentRateServiceMock = {
for (const dataGatheringItem of dataGatheringItems) {
values.push({
date,
dataSource: dataGatheringItem.dataSource,
marketPriceInBaseCurrency: mockGetValue(
dataGatheringItem.symbol,
date

View File

@ -2,6 +2,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { DataSource, MarketData } from '@prisma/client';
import { CurrentRateService } from './current-rate.service';
@ -25,30 +26,30 @@ jest.mock('@ghostfolio/api/services/market-data/market-data.service', () => {
getRange: ({
dateRangeEnd,
dateRangeStart,
symbols
uniqueAssets
}: {
dateRangeEnd: Date;
dateRangeStart: Date;
symbols: string[];
uniqueAssets: UniqueAsset[];
}) => {
return Promise.resolve<MarketData[]>([
{
createdAt: dateRangeStart,
dataSource: DataSource.YAHOO,
dataSource: uniqueAssets[0].dataSource,
date: dateRangeStart,
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
marketPrice: 1841.823902,
state: 'CLOSE',
symbol: symbols[0]
symbol: uniqueAssets[0].symbol
},
{
createdAt: dateRangeEnd,
dataSource: DataSource.YAHOO,
dataSource: uniqueAssets[0].dataSource,
date: dateRangeEnd,
id: '082d6893-df27-4c91-8a5d-092e84315b56',
marketPrice: 1847.839966,
state: 'CLOSE',
symbol: symbols[0]
symbol: uniqueAssets[0].symbol
}
]);
}
@ -134,6 +135,7 @@ describe('CurrentRateService', () => {
errors: [],
values: [
{
dataSource: 'YAHOO',
date: undefined,
marketPriceInBaseCurrency: 1841.823902,
symbol: 'AMZN'

View File

@ -2,7 +2,11 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { resetHours } from '@ghostfolio/common/helper';
import { DataProviderInfo, ResponseError } from '@ghostfolio/common/interfaces';
import {
DataProviderInfo,
ResponseError,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import { isBefore, isToday } from 'date-fns';
import { flatten, isEmpty, uniqBy } from 'lodash';
@ -52,6 +56,7 @@ export class CurrentRateService {
if (dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice) {
result.push({
dataSource: dataGatheringItem.dataSource,
date: today,
marketPriceInBaseCurrency:
this.exchangeRateDataService.toCurrency(
@ -75,27 +80,30 @@ export class CurrentRateService {
);
}
const symbols = dataGatheringItems.map((dataGatheringItem) => {
return dataGatheringItem.symbol;
});
const uniqueAssets: UniqueAsset[] = dataGatheringItems.map(
({ dataSource, symbol }) => {
return { dataSource, symbol };
}
);
promises.push(
this.marketDataService
.getRange({
dateQuery,
symbols
uniqueAssets
})
.then((data) => {
return data.map((marketDataItem) => {
return data.map(({ dataSource, date, marketPrice, symbol }) => {
return {
date: marketDataItem.date,
dataSource,
date,
symbol,
marketPriceInBaseCurrency:
this.exchangeRateDataService.toCurrency(
marketDataItem.marketPrice,
currencies[marketDataItem.symbol],
marketPrice,
currencies[symbol],
userCurrency
),
symbol: marketDataItem.symbol
)
};
});
})
@ -112,7 +120,7 @@ export class CurrentRateService {
};
if (!isEmpty(quoteErrors)) {
for (const { symbol } of quoteErrors) {
for (const { dataSource, symbol } of quoteErrors) {
try {
// If missing quote, fallback to the latest available historical market price
let value: GetValueObject = response.values.find((currentValue) => {
@ -121,6 +129,7 @@ export class CurrentRateService {
if (!value) {
value = {
dataSource,
symbol,
date: today,
marketPriceInBaseCurrency: 0

View File

@ -1,5 +1,6 @@
export interface GetValueObject {
import { UniqueAsset } from '@ghostfolio/common/interfaces';
export interface GetValueObject extends UniqueAsset {
date: Date;
marketPriceInBaseCurrency: number;
symbol: string;
}

View File

@ -40,7 +40,12 @@ export class SymbolService {
const marketData = await this.marketDataService.getRange({
dateQuery: { gte: subDays(new Date(), days) },
symbols: [dataGatheringItem.symbol]
uniqueAssets: [
{
dataSource: dataGatheringItem.dataSource,
symbol: dataGatheringItem.symbol
}
]
});
historicalData = marketData.map(({ date, marketPrice: value }) => {

View File

@ -127,7 +127,9 @@ export class UserService {
updatedAt
} = await this.prismaService.user.findUnique({
include: {
Account: true,
Account: {
include: { Platform: true }
},
Analytics: true,
Settings: true,
Subscription: true
@ -250,8 +252,8 @@ export class UserService {
currentPermissions.push(permissions.impersonateAllUsers);
}
user.Account = sortBy(user.Account, (account) => {
return account.name;
user.Account = sortBy(user.Account, ({ name }) => {
return name.toLowerCase();
});
user.permissions = currentPermissions.sort();

View File

@ -66,6 +66,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-basil-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-beanvest</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -126,6 +130,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-kubera</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-magnifi</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-markets.sh</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -368,6 +376,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-basil-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-beanvest</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -428,6 +440,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-kubera</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-magnifi</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-markets.sh</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -694,6 +710,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-basil-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-beanvest</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -754,6 +774,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-kubera</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-magnifi</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-markets.sh</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -866,6 +890,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-basil-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-beanvest</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -926,6 +954,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-kubera</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-magnifi</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-markets.sh</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>

View File

@ -59,10 +59,10 @@ export class MarketDataService {
public async getRange({
dateQuery,
symbols
uniqueAssets
}: {
dateQuery: DateQuery;
symbols: string[];
uniqueAssets: UniqueAsset[];
}): Promise<MarketData[]> {
return await this.prismaService.marketData.findMany({
orderBy: [
@ -74,10 +74,17 @@ export class MarketDataService {
}
],
where: {
date: dateQuery,
symbol: {
in: symbols
}
OR: uniqueAssets.map(({ dataSource, symbol }) => {
return {
AND: [
{
dataSource,
symbol,
date: dateQuery
}
]
};
})
}
});
}

View File

@ -20,6 +20,7 @@ import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces';
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
import { translate } from '@ghostfolio/ui/i18n';
import { AssetSubClass, DataSource, Prisma } from '@prisma/client';
import { isUUID } from 'class-validator';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
@ -83,7 +84,7 @@ export class AdminMarketDataComponent
public defaultDateFormat: string;
public deviceType: string;
public displayedColumns = [
'symbol',
'nameWithSymbol',
'dataSource',
'assetClass',
'assetSubClass',
@ -97,6 +98,7 @@ export class AdminMarketDataComponent
];
public filters$ = new Subject<Filter[]>();
public isLoading = false;
public isUUID = isUUID;
public placeholder = '';
public pageSize = DEFAULT_PAGE_SIZE;
public totalItems = 0;

View File

@ -28,6 +28,24 @@
</td>
</ng-container>
<ng-container matColumnDef="nameWithSymbol">
<th
*matHeaderCellDef
class="px-1"
mat-header-cell
mat-sort-header="symbol"
>
<ng-container i18n>Name</ng-container>
</th>
<td *matCellDef="let element" class="line-height-1 px-1" mat-cell>
<div class="text-truncate">{{ element.name }}</div>
<div *ngIf="!isUUID(element.symbol)">
<small class="text-muted">{{ element.symbol | gfSymbol }}</small>
</div>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container>
<ng-container matColumnDef="dataSource">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
<ng-container i18n>Data Source</ng-container>

View File

@ -6,6 +6,7 @@ import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -20,6 +21,7 @@ import { GfCreateAssetProfileDialogModule } from './create-asset-profile-dialog/
GfActivitiesFilterModule,
GfAssetProfileDialogModule,
GfCreateAssetProfileDialogModule,
GfSymbolModule,
MatButtonModule,
MatMenuModule,
MatPaginatorModule,

View File

@ -38,7 +38,7 @@
<div class="w-50">
<table>
<tr *ngFor="let exchangeRate of exchangeRates">
<td class="d-flex">
<td>
<gf-value
[locale]="user?.settings?.locale"
[value]="1"
@ -46,8 +46,9 @@
</td>
<td class="pl-1">{{ exchangeRate.label1 }}</td>
<td class="px-1">=</td>
<td class="d-flex justify-content-end">
<td align="right">
<gf-value
class="d-inline-block"
[locale]="user?.settings?.locale"
[precision]="4"
[value]="exchangeRate.value"
@ -55,26 +56,50 @@
</td>
<td class="pl-1">{{ exchangeRate.label2 }}</td>
<td>
<a
class="h-100 mx-1 no-min-width px-2"
<button
class="mx-1 no-min-width px-2"
mat-button
[queryParams]="{
[matMenuTriggerFor]="exchangeRateActionsMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-horizontal"></ion-icon>
</button>
<mat-menu
#exchangeRateActionsMenu="matMenu"
class="h-100 mx-1 no-min-width px-2"
xPosition="before"
>
<a
mat-menu-item
[queryParams]="{
assetProfileDialog: true,
dataSource: exchangeRate.dataSource,
symbol: exchangeRate.symbol
}"
[routerLink]="['/admin', 'market-data']"
>
<ion-icon name="create-outline"></ion-icon>
</a>
<button
*ngIf="customCurrencies.includes(exchangeRate.label2)"
class="h-100 mx-1 no-min-width px-2"
mat-button
(click)="onDeleteCurrency(exchangeRate.label2)"
>
<ion-icon name="trash-outline"></ion-icon>
</button>
[routerLink]="['/admin', 'market-data']"
>
<span class="align-items-center d-flex">
<ion-icon
class="mr-2"
name="create-outline"
></ion-icon>
<span i18n>Edit</span>
</span>
</a>
<button
*ngIf="customCurrencies.includes(exchangeRate.label2)"
mat-menu-item
(click)="onDeleteCurrency(exchangeRate.label2)"
>
<span class="align-items-center d-flex">
<ion-icon
class="mr-2"
name="trash-outline"
></ion-icon>
<span i18n>Delete</span>
</span>
</button>
</mat-menu>
</td>
</tr>
</table>
@ -149,17 +174,34 @@
<table>
<tr *ngFor="let coupon of coupons">
<td class="text-monospace">{{ coupon.code }}</td>
<td class="d-flex justify-content-end pl-2">
{{ coupon.duration }}
</td>
<td class="pl-2 text-right">{{ coupon.duration }}</td>
<td>
<button
class="h-100 mx-1 no-min-width px-2"
class="mx-1 no-min-width px-2"
mat-button
(click)="onDeleteCoupon(coupon.code)"
[matMenuTriggerFor]="couponActionsMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="trash-outline"></ion-icon>
<ion-icon name="ellipsis-horizontal"></ion-icon>
</button>
<mat-menu
#couponActionsMenu="matMenu"
class="h-100 mx-1 no-min-width px-2"
xPosition="before"
>
<button
mat-menu-item
(click)="onDeleteCoupon(coupon.code)"
>
<span class="align-items-center d-flex">
<ion-icon
class="mr-2"
name="trash-outline"
></ion-icon>
<span i18n>Delete</span>
</span>
</button>
</mat-menu>
</td>
</tr>
</table>

View File

@ -3,6 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatMenuModule } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { RouterModule } from '@angular/router';
@ -20,6 +21,7 @@ import { AdminOverviewComponent } from './admin-overview.component';
GfValueModule,
MatButtonModule,
MatCardModule,
MatMenuModule,
MatSelectModule,
MatSlideToggleModule,
ReactiveFormsModule,

View File

@ -1,5 +1,6 @@
:host {
display: block;
align-items: center;
display: flex;
img {
border-radius: 0.2rem;

View File

@ -10,9 +10,17 @@
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>From</mat-label>
<mat-select formControlName="fromAccount">
<mat-option *ngFor="let account of accounts" [value]="account.id"
>{{ account.name }}</mat-option
>
<mat-option *ngFor="let account of accounts" [value]="account.id">
<div class="d-flex">
<gf-symbol-icon
*ngIf="account.Platform?.url"
class="mr-1"
[tooltip]="account.Platform?.name"
[url]="account.Platform?.url"
></gf-symbol-icon
><span>{{ account.name }}</span>
</div>
</mat-option>
</mat-select>
</mat-form-field>
</div>
@ -20,9 +28,17 @@
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>To</mat-label>
<mat-select formControlName="toAccount">
<mat-option *ngFor="let account of accounts" [value]="account.id"
>{{ account.name }}</mat-option
>
<mat-option *ngFor="let account of accounts" [value]="account.id">
<div class="d-flex">
<gf-symbol-icon
*ngIf="account.Platform?.url"
class="mr-1"
[tooltip]="account.Platform?.name"
[url]="account.Platform?.url"
></gf-symbol-icon
><span>{{ account.name }}</span>
</div>
</mat-option>
</mat-select>
</mat-form-field>
</div>

View File

@ -6,6 +6,7 @@ import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
import { TransferBalanceDialog } from './transfer-balance-dialog.component';
@ -13,6 +14,7 @@ import { TransferBalanceDialog } from './transfer-balance-dialog.component';
declarations: [TransferBalanceDialog],
imports: [
CommonModule,
GfSymbolIconModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,

View File

@ -327,7 +327,7 @@
<div class="col-md-8 offset-md-2">
<gf-carousel [aria-label]="'Testimonials'">
<div *ngFor="let testimonial of testimonials" gf-carousel-item>
<div class="d-flex px-3">
<div class="d-flex px-4">
<gf-logo
class="mr-3 mt-2 pt-1"
size="medium"

View File

@ -72,9 +72,20 @@
*ngIf="!activityForm.controls['accountId'].hasValidator(Validators.required)"
[value]="null"
></mat-option>
<mat-option *ngFor="let account of data.accounts" [value]="account.id"
>{{ account.name }}</mat-option
<mat-option
*ngFor="let account of data.accounts"
[value]="account.id"
>
<div class="d-flex">
<gf-symbol-icon
*ngIf="account.Platform?.url"
class="mr-1"
[tooltip]="account.Platform?.name"
[url]="account.Platform?.url"
></gf-symbol-icon
><span>{{ account.name }}</span>
</div>
</mat-option>
</mat-select>
</mat-form-field>
</div>

View File

@ -10,6 +10,7 @@ import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
import { GfSymbolAutocompleteModule } from '@ghostfolio/ui/symbol-autocomplete/symbol-autocomplete.module';
import { GfValueModule } from '@ghostfolio/ui/value';
@ -21,6 +22,7 @@ import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog
CommonModule,
FormsModule,
GfSymbolAutocompleteModule,
GfSymbolIconModule,
GfValueModule,
MatAutocompleteModule,
MatButtonModule,

View File

@ -2,6 +2,7 @@ import { Product } from '@ghostfolio/common/interfaces';
import { AllvueSystemsPageComponent } from './products/allvue-systems-page.component';
import { AltooPageComponent } from './products/altoo-page.component';
import { BasilFinancePageComponent } from './products/basil-finance-page.component';
import { BeanvestPageComponent } from './products/beanvest-page.component';
import { CapitallyPageComponent } from './products/capitally-page.component';
import { CapMonPageComponent } from './products/capmon-page.component';
@ -18,6 +19,7 @@ import { GoSpatzPageComponent } from './products/gospatz-page.component';
import { IntuitMintPageComponent } from './products/intuit-mint-page.component';
import { JustEtfPageComponent } from './products/justetf-page.component';
import { KuberaPageComponent } from './products/kubera-page.component';
import { MagnifiPageComponent } from './products/magnifi-page.component';
import { MarketsShPageComponent } from './products/markets.sh-page.component';
import { MaybeFinancePageComponent } from './products/maybe-finance-page.component';
import { MonarchMoneyPageComponent } from './products/monarch-money-page.component';
@ -84,6 +86,15 @@ export const products: Product[] = [
origin: $localize`Switzerland`,
slogan: 'Simplicity for Complex Wealth'
},
{
component: BasilFinancePageComponent,
founded: 2022,
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'basil-finance',
name: 'Basil Finance',
slogan: 'The ultimate solution for tracking and managing your investments'
},
{
component: BeanvestPageComponent,
founded: 2020,
@ -252,6 +263,17 @@ export const products: Product[] = [
pricingPerYear: '$150',
slogan: 'The Time Machine for your Net Worth'
},
{
component: MagnifiPageComponent,
founded: 2018,
hasFreePlan: false,
hasSelfHostingAbility: false,
key: 'magnifi',
name: 'Magnifi',
origin: $localize`United States`,
pricingPerYear: '$132',
slogan: 'AI Investing Assistant'
},
{
component: MarketsShPageComponent,
founded: 2022,

View File

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

View File

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

View File

@ -2,8 +2,8 @@
*ngIf="this.showPrevArrow"
aria-hidden="true"
aria-label="previous"
class="carousel-nav carousel-nav-prev no-min-width position-absolute"
mat-stroked-button
class="carousel-nav carousel-nav-prev no-min-width position-absolute px-1"
mat-button
tabindex="-1"
(click)="previous()"
>
@ -25,8 +25,8 @@
*ngIf="this.showNextArrow"
aria-hidden="true"
aria-label="next"
class="carousel-nav carousel-nav-next no-min-width position-absolute"
mat-stroked-button
class="carousel-nav carousel-nav-next no-min-width position-absolute px-1"
mat-button
tabindex="-1"
(click)="next()"
>

View File

@ -12,13 +12,14 @@
button {
top: 50%;
transform: translateY(-50%);
z-index: 1;
&.carousel-nav-prev {
left: -50px;
left: -0.5rem;
}
&.carousel-nav-next {
right: -50px;
right: -0.5rem;
}
}

View File

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