Compare commits

..

22 Commits

Author SHA1 Message Date
20195b2b1a Release 1.180.0 (#1169) 2022-08-18 21:15:17 +02:00
7fa4e6ebd2 Feature/resolve feature graphic of blog post (#1168)
* Resolve feature graphic of blog post

* Update changelog
2022-08-18 21:13:39 +02:00
d8531ddfcb Bugfix/fix links to blog posts (#1167)
* Fix links

* Update changelog
2022-08-18 21:11:10 +02:00
70d670b711 Bugfix/fix license (#1160)
* Fix license

* Update changelog
2022-08-17 23:23:39 +02:00
27b0663a80 Add translations (#1159) 2022-08-16 22:16:15 +02:00
874dfb0235 Improve links (#1158) 2022-08-16 21:52:37 +02:00
072db0d558 Add translations (#1157) 2022-08-16 21:40:51 +02:00
12e692429a Regenerate xlf (#1156) 2022-08-16 21:30:12 +02:00
e22b8b78b8 Feature/tag route titles with template literal strings (#1155)
* Tagged template literal strings

* Update changelog
2022-08-16 21:03:05 +02:00
dc5052f7dc Feature/set up language localization for german (#1153)
* Set up language localization for German

* Update changelog
2022-08-16 20:58:08 +02:00
335553e891 Feature/tag template literal strings (#1152)
* Tagged template literal strings

* Update changelog
2022-08-16 20:53:14 +02:00
d480ad1023 Extract locales (#1151) 2022-08-15 19:56:42 +02:00
7320751056 Feature/set up ng extract i18n merge (#1149)
* Set up ng-extract-i18n-merge

* Update changelog
2022-08-15 19:52:43 +02:00
108c0c13c4 Release 1.179.5 (#1150) 2022-08-15 18:17:57 +02:00
053a5cc5b5 Release 1.179.4 (#1148) 2022-08-14 10:10:54 +02:00
c456a8bcfe Release/1.179.3 (#1147)
* Clean up

* Release 1.179.3
2022-08-13 20:33:43 +02:00
6fcecb5bc6 Release 1.179.2 (#1145) 2022-08-13 13:39:37 +02:00
e4e0a7d9f0 Release 1.179.1 (#1144) 2022-08-13 12:16:39 +02:00
c7173761a3 Release 1.179.0 (#1143) 2022-08-13 10:44:38 +02:00
185e130d9f Feature/add blog post 500 stars on GitHub (#1138)
* Add blog post

* Update changelog
2022-08-13 10:42:56 +02:00
81245635af Feature/setup i18n (#1139)
* Setup i18n

* Update changelog
2022-08-13 10:29:36 +02:00
55182ac1af Feature/reduce maximum width of performance chart (#1137)
* Reduce maximum width

* Update changelog
2022-08-10 17:26:34 +02:00
75 changed files with 6273 additions and 180 deletions

View File

@ -5,6 +5,34 @@ 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).
## 1.180.0 - 18.08.2022
### Added
- Set up `ng-extract-i18n-merge` to improve the i18n extraction and merge workflow
- Set up language localization for German (`de`)
- Resolved the feature graphic of the blog post
### Changed
- Tagged template literal strings in components for localization with `$localize`
### Fixed
- Fixed the license component in the about page
- Fixed the links to the blog posts
## 1.179.5 - 15.08.2022
### Added
- Set up i18n support
- Added a blog post: _500 Stars on GitHub_
### Changed
- Reduced the maximum width of the performance chart on the home page
## 1.178.0 - 09.08.2022
### Added

View File

@ -77,41 +77,45 @@
"polyfills": "apps/client/src/polyfills.ts",
"tsConfig": "apps/client/tsconfig.app.json",
"assets": [
"apps/client/src/assets",
{
"glob": "assetlinks.json",
"input": "apps/client/src/assets",
"output": "./.well-known"
"output": "./../.well-known"
},
{
"glob": "CHANGELOG.md",
"input": "",
"output": "./assets"
"output": "./../assets"
},
{
"glob": "LICENSE",
"input": "",
"output": "./assets"
"output": "./../assets"
},
{
"glob": "robots.txt",
"input": "apps/client/src/assets",
"output": "./"
"output": "./../"
},
{
"glob": "sitemap.xml",
"input": "apps/client/src/assets",
"output": "./"
"output": "./../"
},
{
"glob": "**/*",
"input": "node_modules/ionicons/dist/ionicons",
"output": "./ionicons"
"output": "./../ionicons"
},
{
"glob": "**/*.js",
"input": "node_modules/ionicons/dist/",
"output": "./"
"output": "./../"
},
{
"glob": "**/*",
"input": "apps/client/src/assets",
"output": "./../assets/"
}
],
"styles": ["apps/client/src/styles.scss"],
@ -124,6 +128,10 @@
"namedChunks": true
},
"configurations": {
"development-en": {
"baseHref": "/en/",
"localize": ["en"]
},
"production": {
"fileReplacements": [
{
@ -162,15 +170,21 @@
"proxyConfig": "apps/client/proxy.conf.json"
},
"configurations": {
"development-en": {
"browserTarget": "client:build:development-en"
},
"production": {
"browserTarget": "client:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"builder": "ng-extract-i18n-merge:ng-extract-i18n-merge",
"options": {
"browserTarget": "client:build"
"browserTarget": "client:build",
"includeContext": true,
"outputPath": "src/locales",
"targetFiles": ["messages.de.xlf"]
}
},
"lint": {
@ -188,6 +202,15 @@
"outputs": ["coverage/apps/client"]
}
},
"i18n": {
"locales": {
"de": {
"baseHref": "/de/",
"translation": "apps/client/src/locales/messages.de.xlf"
}
},
"sourceLocale": "en"
},
"tags": []
},
"client-e2e": {

View File

@ -10,7 +10,7 @@ import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-d
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module';
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { ServeStaticModule } from '@nestjs/serve-static';
@ -23,6 +23,7 @@ import { AuthModule } from './auth/auth.module';
import { BenchmarkModule } from './benchmark/benchmark.module';
import { CacheModule } from './cache/cache.module';
import { ExportModule } from './export/export.module';
import { FrontendMiddleware } from './frontend.middleware';
import { ImportModule } from './import/import.module';
import { InfoModule } from './info/info.module';
import { OrderModule } from './order/order.module';
@ -82,4 +83,10 @@ import { UserModule } from './user/user.module';
controllers: [AppController],
providers: [CronService]
})
export class AppModule {}
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(FrontendMiddleware)
.forRoutes({ path: '*', method: RequestMethod.ALL });
}
}

View File

@ -1,5 +1,6 @@
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { OAuthResponse } from '@ghostfolio/common/interfaces';
import {
Body,
@ -62,9 +63,17 @@ export class AuthController {
const jwt: string = req.user.jwt;
if (jwt) {
res.redirect(`${this.configurationService.get('ROOT_URL')}/auth/${jwt}`);
res.redirect(
`${this.configurationService.get(
'ROOT_URL'
)}/${DEFAULT_LANGUAGE_CODE}/auth/${jwt}`
);
} else {
res.redirect(`${this.configurationService.get('ROOT_URL')}/auth`);
res.redirect(
`${this.configurationService.get(
'ROOT_URL'
)}/${DEFAULT_LANGUAGE_CODE}/auth`
);
}
}

View File

@ -0,0 +1,53 @@
import * as path from 'path';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class FrontendMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
if (req.url.includes('cover.png')) {
Logger.log(`Referer: ${req.headers.referer}`, 'FrontendMiddleware');
// Resolve feature graphic for blog post
if (req.headers.referer?.includes('500-stars-on-github')) {
res.sendFile(
path.join(
__dirname,
'..',
'client',
'assets',
'images',
'blog',
'500-stars-on-github.jpg'
)
);
} else {
// Skip
next();
}
} else if (req.path.startsWith('/api/') || this.isFileRequest(req.url)) {
// Skip
next();
} else if (req.path.startsWith('/de/')) {
res.sendFile(this.getPathOfIndexHtmlFile('de'));
} else {
res.sendFile(this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE));
}
}
private getPathOfIndexHtmlFile(aLocale: string) {
return path.join(__dirname, '..', 'client', aLocale, 'index.html');
}
private isFileRequest(filename: string) {
if (filename === '/assets/LICENSE') {
return true;
} else if (filename.includes('auth/ey')) {
return false;
}
return filename.split('.').pop() !== filename;
}
}

View File

@ -1,6 +1,9 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { PROPERTY_COUPONS } from '@ghostfolio/common/config';
import {
DEFAULT_LANGUAGE_CODE,
PROPERTY_COUPONS
} from '@ghostfolio/common/config';
import { Coupon } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
@ -93,7 +96,11 @@ export class SubscriptionController {
'SubscriptionController'
);
res.redirect(`${this.configurationService.get('ROOT_URL')}/account`);
res.redirect(
`${this.configurationService.get(
'ROOT_URL'
)}/${DEFAULT_LANGUAGE_CODE}/account`
);
}
@Post('stripe/checkout-session')

View File

@ -1,5 +1,6 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
import { Injectable, Logger } from '@nestjs/common';
import { Subscription } from '@prisma/client';
@ -33,7 +34,9 @@ export class SubscriptionService {
userId: string;
}) {
const checkoutSessionCreateParams: Stripe.Checkout.SessionCreateParams = {
cancel_url: `${this.configurationService.get('ROOT_URL')}/account`,
cancel_url: `${this.configurationService.get(
'ROOT_URL'
)}/${DEFAULT_LANGUAGE_CODE}/account`,
client_reference_id: userId,
line_items: [
{

View File

@ -54,45 +54,52 @@ const routes: Routes = [
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
},
{
path: 'de/blog/2021/07/hallo-ghostfolio',
path: 'blog/2021/07/hallo-ghostfolio',
loadChildren: () =>
import(
'./pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.module'
).then((m) => m.HalloGhostfolioPageModule)
},
{
path: 'demo',
loadChildren: () =>
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
},
{
path: 'en/blog/2021/07/hello-ghostfolio',
path: 'blog/2021/07/hello-ghostfolio',
loadChildren: () =>
import(
'./pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.module'
).then((m) => m.HelloGhostfolioPageModule)
},
{
path: 'en/blog/2022/01/ghostfolio-first-months-in-open-source',
path: 'blog/2022/01/ghostfolio-first-months-in-open-source',
loadChildren: () =>
import(
'./pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module'
).then((m) => m.FirstMonthsInOpenSourcePageModule)
},
{
path: 'en/blog/2022/07/ghostfolio-meets-internet-identity',
path: 'blog/2022/07/ghostfolio-meets-internet-identity',
loadChildren: () =>
import(
'./pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.module'
).then((m) => m.GhostfolioMeetsInternetIdentityPageModule)
},
{
path: 'en/blog/2022/07/how-do-i-get-my-finances-in-order',
path: 'blog/2022/07/how-do-i-get-my-finances-in-order',
loadChildren: () =>
import(
'./pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.module'
).then((m) => m.HowDoIGetMyFinancesInOrderPageModule)
},
{
path: 'blog/2022/08/500-stars-on-github',
loadChildren: () =>
import(
'./pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.module'
).then((m) => m.FiveHundredStarsOnGitHubPageModule)
},
{
path: 'demo',
loadChildren: () =>
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
},
{
path: 'faq',
loadChildren: () =>

View File

@ -21,8 +21,10 @@
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
<ng-container *ngIf="element.type === 'PUBLIC'">
<ion-icon class="mr-1" name="link-outline"></ion-icon>
<a href="{{ baseUrl }}/p/{{ element.id }}" target="_blank"
>{{ baseUrl }}/p/{{ element.id }}</a
<a
href="{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}"
target="_blank"
>{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}</a
>
</ng-container>
</td>

View File

@ -8,6 +8,7 @@ import {
Output
} from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { Access } from '@ghostfolio/common/interfaces';
@Component({
@ -24,6 +25,7 @@ export class AccessTableComponent implements OnChanges, OnInit {
public baseUrl = window.location.origin;
public dataSource: MatTableDataSource<Access>;
public defaultLanguageCode = DEFAULT_LANGUAGE_CODE;
public displayedColumns = [];
public constructor() {}
@ -44,7 +46,7 @@ export class AccessTableComponent implements OnChanges, OnInit {
public onDeleteAccess(aId: string) {
const confirmation = confirm(
'Do you really want to revoke this granted access?'
$localize`Do you really want to revoke this granted access?`
);
if (confirmation) {

View File

@ -69,7 +69,9 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
}
public onDeleteAccount(aId: string) {
const confirmation = confirm('Do you really want to delete this account?');
const confirmation = confirm(
$localize`Do you really want to delete this account?`
);
if (confirmation) {
this.accountDeleted.emit(aId);

View File

@ -103,7 +103,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
}
public onAddCurrency() {
const currency = prompt('Please add a currency:');
const currency = prompt($localize`Please add a currency:`);
if (currency) {
const currencies = uniq([...this.customCurrencies, currency]);
@ -116,7 +116,9 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
}
public onDeleteCoupon(aCouponCode: string) {
const confirmation = confirm('Do you really want to delete this coupon?');
const confirmation = confirm(
$localize`Do you really want to delete this coupon?`
);
if (confirmation === true) {
const coupons = this.coupons.filter((coupon) => {
@ -127,7 +129,9 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
}
public onDeleteCurrency(aCurrency: string) {
const confirmation = confirm('Do you really want to delete this currency?');
const confirmation = confirm(
$localize`Do you really want to delete this currency?`
);
if (confirmation === true) {
const currencies = this.customCurrencies.filter((currency) => {
@ -142,7 +146,9 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
}
public onFlushCache() {
const confirmation = confirm('Do you really want to flush the cache?');
const confirmation = confirm(
$localize`Do you really want to flush the cache?`
);
if (confirmation === true) {
this.cacheService
@ -190,7 +196,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
}
public onSetSystemMessage() {
const systemMessage = prompt('Please set your system message:');
const systemMessage = prompt($localize`Please set your system message:`);
if (systemMessage) {
this.putSystemMessage(systemMessage);

View File

@ -55,7 +55,9 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
}
public onDeleteUser(aId: string) {
const confirmation = confirm('Do you really want to delete this user?');
const confirmation = confirm(
$localize`Do you really want to delete this user?`
);
if (confirmation) {
this.dataService

View File

@ -109,7 +109,7 @@ export class HeaderComponent implements OnChanges {
data: {
accessToken: '',
hasPermissionToUseSocialLogin: this.hasPermissionForSocialLogin,
title: 'Sign in'
title: $localize`Sign in`
},
width: '30rem'
});
@ -123,7 +123,7 @@ export class HeaderComponent implements OnChanges {
.loginAnonymous(data?.accessToken)
.pipe(
catchError(() => {
alert('Oops! Incorrect Security Token.');
alert($localize`Oops! Incorrect Security Token.`);
return EMPTY;
}),

View File

@ -2,6 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import {
@ -9,7 +10,6 @@ import {
SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { defaultDateRangeOptions } from '@ghostfolio/common/config';
import { Position, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
@ -27,7 +27,7 @@ import { PositionDetailDialogParams } from '../position/position-detail-dialog/i
})
export class HomeHoldingsComponent implements OnDestroy, OnInit {
public dateRange: DateRange;
public dateRangeOptions = defaultDateRangeOptions;
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
public deviceType: string;
public hasImpersonationId: boolean;
public hasPermissionToCreateOrder: boolean;

View File

@ -1,4 +1,5 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import {
@ -6,7 +7,6 @@ import {
SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { defaultDateRangeOptions } from '@ghostfolio/common/config';
import {
PortfolioPerformance,
UniqueAsset,
@ -26,7 +26,7 @@ import { takeUntil } from 'rxjs/operators';
})
export class HomeOverviewComponent implements OnDestroy, OnInit {
public dateRange: DateRange;
public dateRangeOptions = defaultDateRangeOptions;
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
public deviceType: string;
public errors: UniqueAsset[];
public hasError: boolean;

View File

@ -6,7 +6,7 @@
.chart-container {
aspect-ratio: 16 / 9;
height: auto;
max-width: 67rem;
max-width: 50rem;
// Fallback for aspect-ratio (using padding hack)
@supports not (aspect-ratio: 16 / 9) {

View File

@ -25,14 +25,14 @@
>
<img
class="mr-2"
src="./assets/icons/internet-computer.svg"
src="../assets/icons/internet-computer.svg"
style="height: 0.75rem"
/><span i18n>Sign in with Internet Identity</span>
</button>
<a href="/api/v1/auth/google" mat-stroked-button
<a href="../api/v1/auth/google" mat-stroked-button
><img
class="mr-2"
src="./assets/icons/google.svg"
src="../assets/icons/google.svg"
style="height: 1rem"
/><span i18n>Sign in with Google</span></a
>

View File

@ -45,7 +45,7 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
public onEditEmergencyFund() {
const emergencyFundInput = prompt(
'Please enter the amount of your emergency fund:',
$localize`Please enter the amount of your emergency fund:`,
this.summary.emergencyFund.toString()
);
const emergencyFund = parseFloat(emergencyFundInput?.trim());

View File

@ -17,6 +17,14 @@ import { ToggleOption } from '@ghostfolio/common/types';
styleUrls: ['./toggle.component.scss']
})
export class ToggleComponent implements OnChanges, OnInit {
public static DEFAULT_DATE_RANGE_OPTIONS: ToggleOption[] = [
{ label: $localize`Today`, value: '1d' },
{ label: $localize`YTD`, value: 'ytd' },
{ label: $localize`1Y`, value: '1y' },
{ label: $localize`5Y`, value: '5y' },
{ label: $localize`Max`, value: 'max' }
];
@Input() defaultValue: string;
@Input() isLoading: boolean;
@Input() options: ToggleOption[];

View File

@ -56,14 +56,16 @@ export class HttpResponseInterceptor implements HttpInterceptor {
if (!this.snackBarRef) {
if (this.info.isReadOnlyMode) {
this.snackBarRef = this.snackBar.open(
'This feature is currently unavailable. Please try again later.',
$localize`This feature is currently unavailable. Please try again later.`,
undefined,
{ duration: 6000 }
);
} else {
this.snackBarRef = this.snackBar.open(
'This feature requires a subscription.',
this.hasPermissionForSubscription ? 'Upgrade Plan' : undefined,
$localize`This feature requires a subscription.`,
this.hasPermissionForSubscription
? $localize`Upgrade Plan`
: undefined,
{ duration: 6000 }
);
}
@ -79,8 +81,8 @@ export class HttpResponseInterceptor implements HttpInterceptor {
} else if (error.status === StatusCodes.INTERNAL_SERVER_ERROR) {
if (!this.snackBarRef) {
this.snackBarRef = this.snackBar.open(
'Oops! Something went wrong. Please try again later.',
'Okay',
$localize`Oops! Something went wrong. Please try again later.`,
$localize`Okay`,
{ duration: 6000 }
);

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: AboutPageComponent,
path: '',
title: 'About'
title: $localize`About`
}
];

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: ChangelogPageComponent,
path: '',
title: 'Changelog & License'
title: $localize`Changelog & License`
}
];

View File

@ -4,7 +4,7 @@
<h3 class="mb-3 text-center" i18n>Changelog</h3>
<mat-card class="changelog">
<mat-card-content>
<markdown [src]="'assets/CHANGELOG.md'"></markdown>
<markdown [src]="'../assets/CHANGELOG.md'"></markdown>
</mat-card-content>
</mat-card>
</div>
@ -15,7 +15,7 @@
<h3 class="mb-3 text-center" i18n>License</h3>
<mat-card>
<mat-card-content>
<markdown [src]="'assets/LICENSE'"></markdown>
<markdown [src]="'../assets/LICENSE'"></markdown>
</mat-card-content>
</mat-card>
</div>

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: PrivacyPolicyPageComponent,
path: '',
title: 'Privacy Policy'
title: $localize`Privacy Policy`
}
];

View File

@ -2,7 +2,7 @@
<div class="mb-5 row">
<div class="col">
<h3 class="mb-3 text-center" i18n>Privacy Policy</h3>
<markdown [src]="'assets/privacy-policy.md'"></markdown>
<markdown [src]="'../assets/privacy-policy.md'"></markdown>
</div>
</div>
</div>

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: AccountPageComponent,
path: '',
title: 'My Ghostfolio'
title: $localize`My Ghostfolio`
}
];

View File

@ -218,7 +218,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
}
public onRedeemCoupon() {
let couponCode = prompt('Please enter your coupon code:');
let couponCode = prompt($localize`Please enter your coupon code:`);
couponCode = couponCode?.trim();
if (couponCode) {
@ -227,17 +227,21 @@ export class AccountPageComponent implements OnDestroy, OnInit {
.pipe(
takeUntil(this.unsubscribeSubject),
catchError(() => {
this.snackBar.open('😞 Could not redeem coupon code', undefined, {
duration: 3000
});
this.snackBar.open(
'😞 ' + $localize`Could not redeem coupon code`,
undefined,
{
duration: 3000
}
);
return EMPTY;
})
)
.subscribe(() => {
this.snackBarRef = this.snackBar.open(
'✅ Coupon code has been redeemed',
'Reload',
'✅' + $localize`Coupon code has been redeemed`,
$localize`Reload`,
{
duration: 3000
}
@ -283,7 +287,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
this.registerDevice();
} else {
const confirmation = confirm(
'Do you really want to remove this sign in method?'
$localize`Do you really want to remove this sign in method?`
);
if (confirmation) {

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: AccountsPageComponent,
path: '',
title: 'Accounts'
title: $localize`Accounts`
}
];

View File

@ -20,7 +20,7 @@ const routes: Routes = [
],
component: AdminPageComponent,
path: '',
title: 'Admin Control'
title: $localize`Admin Control`
}
];

View File

@ -28,6 +28,7 @@ export class AuthPageComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
const jwt = params['jwt'];
this.tokenStorageService.saveToken(
jwt,
this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true'

View File

@ -68,7 +68,7 @@
<p class="my-5 text-center">
<img
alt="Ghostfol.io Screenshot"
src="./assets/images/screenshot.png"
src="../assets/images/screenshot.png"
style="max-width: 100%; width: 20rem"
title="Ghostfol.io Screenshot"
/>

View File

@ -66,7 +66,7 @@
<p class="my-5 text-center">
<img
alt="Ghostfol.io Screenshot"
src="./assets/images/screenshot.png"
src="../assets/images/screenshot.png"
style="max-width: 100%; width: 20rem"
title="Ghostfol.io Screenshot"
/>

View File

@ -20,9 +20,7 @@
<h2 class="h4">From 1* to 100 stars on GitHub</h2>
<p>
When I decided to
<a [routerLink]="['/en', 'blog', '2021', '07', 'hello-ghostfolio']"
>publish</a
>
<a href="../en/blog/2021/07/hello-ghostfolio">publish</a>
the project as
<a href="https://github.com/ghostfolio/ghostfolio"
>open source software</a

View File

@ -7,8 +7,8 @@
<div class="mb-3 text-muted"><small>2022-07-23</small></div>
<img
alt="Ghostfolio meets Internet Identity Teaser"
class="w-100"
src="./assets/images/blog/ghostfolio-meets-internet-identity.png"
class="rounded w-100"
src="../assets/images/blog/ghostfolio-meets-internet-identity.png"
title="Ghostfolio meets Internet Identity"
/>
</div>

View File

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { FiveHundredStarsOnGitHubPageComponent } from './500-stars-on-github-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: FiveHundredStarsOnGitHubPageComponent,
path: '',
title: '500 Stars on GitHub'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class FiveHundredStarsOnGitHubRoutingModule {}

View File

@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
host: { class: 'page' },
selector: 'gf-500-stars-on-github-page',
styleUrls: ['./500-stars-on-github-page.scss'],
templateUrl: './500-stars-on-github-page.html'
})
export class FiveHundredStarsOnGitHubPageComponent {}

View File

@ -0,0 +1,195 @@
<div class="blog container">
<div class="row">
<div class="col-md-8 offset-md-2">
<article>
<div class="mb-4 text-center">
<h1 class="mb-1">500 Stars</h1>
<div class="mb-3 text-muted"><small>2022-08-18</small></div>
<img
alt="500 Stars on GitHub Teaser"
class="rounded w-100"
src="../assets/images/blog/500-stars-on-github.jpg"
title="500 Stars on GitHub"
/>
</div>
<section class="mb-4">
<p>
<a href="https://ghostfol.io">Ghostfolio</a>, the web-based personal
finance management software, is celebrating 500 stars on
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>. This
is a major milestone for this open source project and a good time
for another
<a href="../en/blog/2022/01/ghostfolio-first-months-in-open-source"
>recap</a
>.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Growing Community</h2>
<p>
The Ghostfolio community is growing on various platforms and has
recently passed 100 members on
<a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
>Slack</a
>
as well as 100 followers on
<a href="https://twitter.com/ghostfolio_">Twitter</a>. If you have
not joined yet, this is a good time to make sure you do not miss out
on any future updates.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Message Queue: Asynchronous Processing</h2>
<p>
Overall
<a href="https://status.ghostfol.io">stability and robustness</a>
has increased significantly since the introduction of a
<a href="https://github.com/OptimalBits/bull">message queue</a>. The
workers of this robust queue system process jobs, namely gathering
historical market data, asynchronously in the background to not
bother the main service.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Ready for Web 3.0</h2>
<p>
The
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
>recent integration of Internet Identity</a
>, a blockchain authentication system, makes Ghostfolio ready for
Web3. This third iteration of the World Wide Web is the vision of a
new and better Internet based on decentralized blockchains to give
power back to the users. <i>Internet Identity</i> created by the
<a href="https://dfinity.org">Dfinity Foundation</a> enables you to
sign in securely and anonymously to Ghostfolio without an email
address, username, or a password. All you need is your device with
built-in biometric authentication.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Break-even Point</h2>
<p>
Despite the complicated
<a [routerLink]="['/markets']">economic situation</a> at this time,
the goal set at the beginning of the year to build a sustainable
business and reach break-even with the SaaS offering (<a
[routerLink]="['/markets']"
>Ghostfolio Premium</a
>) has been achieved. We will continue to leverage the revenue to
further improve the fully managed cloud offering for our paying
customers. A new goal we have set for ourselves is to become
profitable.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Outlook</h2>
<p>
Besides all the positive accomplishments during the last months,
there is still a lot of room for improvement. It would be great to
onboard more contributors who are actively involved in software
engineering to realize the full potential of open source software.
If you are a web developer and interested in personal finance,
please get in touch by email via
<a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a> or on Twitter
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a>. We are
happy to discuss ideas.
</p>
<p>
We would like to say thank you for all your feedback and support
since the beginning of this project.
</p>
<p>
Off to the next 500 stars!<br />
Thomas from Ghostfolio
</p>
</section>
<section class="mb-4">
<ul class="list-inline">
<li class="list-inline-item">
<span class="badge badge-light">Blockchain</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">BuildInPublic</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Cloud</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Community</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Finance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Fintech</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Future</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Goal</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Internet Identity</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Investment</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Message Queue</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">OpenSaaS</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Open Source</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">OSS</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Personal Finance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Planning</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Portfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Portfolio Tracker</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Progress</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">SaaS</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Software</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">User Feedback</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Wealth</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Wealth Management</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Web3</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Web 3.0</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Worker</span>
</li>
</ul>
</section>
</article>
</div>
</div>
</div>

View File

@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { FiveHundredStarsOnGitHubRoutingModule } from './500-stars-on-github-page-routing.module';
import { FiveHundredStarsOnGitHubPageComponent } from './500-stars-on-github-page.component';
@NgModule({
declarations: [FiveHundredStarsOnGitHubPageComponent],
imports: [CommonModule, FiveHundredStarsOnGitHubRoutingModule, RouterModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class FiveHundredStarsOnGitHubPageModule {}

View File

@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: BlogPageComponent,
path: '',
title: 'Blog'
title: $localize`Blog`
}
];

View File

@ -8,7 +8,31 @@
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
[routerLink]="['/en', 'blog', '2022', '07', 'ghostfolio-meets-internet-identity']"
href="../en/blog/2022/08/500-stars-on-github"
>
<div class="flex-grow-1">
<div class="h6 m-0 text-truncate">500 Stars on GitHub</div>
<div class="d-flex text-muted">2022-08-18</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
></ion-icon>
</div>
</a>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
>
<div class="flex-grow-1">
<div class="h6 m-0 text-truncate">
@ -34,7 +58,7 @@
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
[routerLink]="['/en', 'blog', '2022', '07', 'how-do-i-get-my-finances-in-order']"
href="../en/blog/2022/07/how-do-i-get-my-finances-in-order"
>
<div class="flex-grow-1">
<div class="h6 m-0 text-truncate">
@ -60,7 +84,7 @@
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
[routerLink]="['/en', 'blog', '2022', '01', 'ghostfolio-first-months-in-open-source']"
href="'../en/blog/2022/01/ghostfolio-first-months-in-open-source"
>
<div class="flex-grow-1">
<div class="h6 m-0 text-truncate">
@ -86,7 +110,7 @@
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
[routerLink]="['/en', 'blog', '2021', '07', 'hello-ghostfolio']"
href="../en/blog/2021/07/hello-ghostfolio"
>
<div class="flex-grow-1">
<div class="h6 m-0 text-truncate">Hello Ghostfolio</div>
@ -110,7 +134,7 @@
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
[routerLink]="['/de', 'blog', '2021', '07', 'hallo-ghostfolio']"
href="../de/blog/2021/07/hallo-ghostfolio"
>
<div class="flex-grow-1">
<div class="h6 m-0 text-truncate">Hallo Ghostfolio</div>

View File

@ -28,7 +28,7 @@ export class DemoPageComponent implements OnDestroy {
if (hasToken) {
alert(
'As you are already logged in, you cannot access the demo account.'
$localize`As you are already logged in, you cannot access the demo account.`
);
} else {
this.tokenStorageService.saveToken(this.info.demoAuthToken, true);

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: FaqPageComponent,
path: '',
title: 'FAQ'
title: $localize`FAQ`
}
];

View File

@ -35,8 +35,7 @@
>Get Started</a
>” button at the top of the page. You have multiple options to join
Ghostfolio: Create an account with a security token, using
<a
[routerLink]="['/en', 'blog', '2022', '07', 'ghostfolio-meets-internet-identity']"
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
>Internet Identity</a
>
or <i>Google Sign</i>. We will guide you to set up your portfolio.
@ -46,8 +45,7 @@
<mat-card-title i18n>Can I use Ghostfolio anonymously?</mat-card-title>
<mat-card-content i18n>
Yes, the authentication systems (via security token or
<a
[routerLink]="['/en', 'blog', '2022', '07', 'ghostfolio-meets-internet-identity']"
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
>Internet Identity</a
>) enable you to sign in securely and anonymously to Ghostfolio. There
is no need for an email address, phone number, or a username.

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: FeaturesPageComponent,
path: '',
title: 'Features'
title: $localize`Features`
}
];

View File

@ -20,7 +20,7 @@ const routes: Routes = [
],
component: HomePageComponent,
path: '',
title: 'Overview'
title: $localize`Overview`
}
];

View File

@ -13,7 +13,7 @@
<img
alt="Ghostfol.io Trailer"
class="rounded video"
src="./assets/images/video-preview.jpg"
src="../assets/images/video-preview.jpg"
style="max-width: 100%; width: 40rem"
/>
</a>
@ -183,7 +183,7 @@
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
title="Get Ghostfolio on Google Play"
>
<img alt="Google Play Badge" src="assets/badge-en-google-play.png" />
<img alt="Google Play Badge" src="../assets/badge-en-google-play.png" />
</a>
</div>
</div>

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: MarketsPageComponent,
path: '',
title: 'Markets'
title: $localize`Markets`
}
];

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: AllocationsPageComponent,
path: '',
title: 'Allocations'
title: $localize`Allocations`
}
];

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: AnalysisPageComponent,
path: '',
title: 'Analysis'
title: $localize`Analysis`
}
];

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: FirePageComponent,
path: '',
title: 'FIRE'
title: $localize`FIRE`
}
];

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: HoldingsPageComponent,
path: '',
title: 'Holdings'
title: $localize`Holdings`
}
];

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: TransactionsPageComponent,
path: '',
title: 'Activities'
title: $localize`Activities`
}
];

View File

@ -188,7 +188,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
input.type = 'file';
input.onchange = (event) => {
this.snackBar.open('⏳ Importing data...');
this.snackBar.open('⏳' + $localize`Importing data...`);
// Getting the file reference
const file = (event.target as HTMLInputElement).files[0];
@ -334,7 +334,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
private handleImportSuccess() {
this.fetchActivities();
this.snackBar.open('✅ Import has been completed', undefined, {
this.snackBar.open('✅' + $localize`Import has been completed`, undefined, {
duration: 3000
});
}

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: PricingPageComponent,
path: '',
title: 'Pricing'
title: $localize`Pricing`
}
];

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: PublicPageComponent,
path: ':id',
title: 'Portfolio'
title: $localize`Portfolio`
}
];

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: RegisterPageComponent,
path: '',
title: 'Registration'
title: $localize`Registration`
}
];

View File

@ -36,15 +36,15 @@
>
<img
class="mr-2"
src="./assets/icons/internet-computer.svg"
src="../assets/icons/internet-computer.svg"
style="height: 0.75rem"
/>
<span i18n>Continue with Internet Identity</span>
</button>
<a class="d-block" href="/api/v1/auth/google" mat-stroked-button
<a class="d-block" href="../api/v1/auth/google" mat-stroked-button
><img
class="mr-2"
src="./assets/icons/google.svg"
src="../assets/icons/google.svg"
style="height: 1rem"
/><span i18n>Continue with Google</span></a
>

View File

@ -9,7 +9,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: ResourcesPageComponent,
path: '',
title: 'Resources'
title: $localize`Resources`
}
];

View File

@ -29,8 +29,7 @@
easier and faster in this guide.
</div>
<div>
<a
[routerLink]="['/en', 'blog', '2022', '07', 'how-do-i-get-my-finances-in-order']"
<a href="../en/blog/2022/07/how-do-i-get-my-finances-in-order"
>How do I get my finances in order? →</a
>
</div>

View File

@ -3,7 +3,7 @@ import { RouterModule, Routes } from '@angular/router';
import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component';
const routes: Routes = [
{ component: WebauthnPageComponent, path: '', title: 'Login' }
{ component: WebauthnPageComponent, path: '', title: $localize`Login` }
];
@NgModule({

View File

@ -16,7 +16,7 @@ const routes: Routes = [
],
component: ZenPageComponent,
path: '',
title: 'Overview'
title: $localize`Overview`
}
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

View File

@ -1,6 +1,6 @@
User-agent: *
Allow: /
Disallow: /about/privacy-policy
Disallow: /p/*
Disallow: /en/about/privacy-policy
Disallow: /en/p/*
Sitemap: https://ghostfol.io/sitemap.xml

View File

@ -6,12 +6,12 @@
"icons": [
{
"sizes": "192x192",
"src": "/assets/android-chrome-192x192.png",
"src": "/en/assets/android-chrome-192x192.png",
"type": "image/png"
},
{
"sizes": "512x512",
"src": "/assets/android-chrome-512x512.png",
"src": "/en/assets/android-chrome-512x512.png",
"type": "image/png"
}
],

View File

@ -6,66 +6,70 @@
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://ghostfol.io</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/about</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/about/changelog</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/blog</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/demo</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
<loc>https://ghostfol.io/en/about</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/about/changelog</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/faq</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
<loc>https://ghostfol.io/en/blog/2022/08/500-stars-on-github</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/features</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
<loc>https://ghostfol.io/en/demo</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/markets</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
<loc>https://ghostfol.io/en/faq</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/pricing</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
<loc>https://ghostfol.io/en/features</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/register</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
<loc>https://ghostfol.io/en/markets</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/resources</loc>
<lastmod>2022-07-29T00:00:00+00:00</lastmod>
<loc>https://ghostfol.io/en/pricing</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/register</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/resources</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
</url>
</urlset>

View File

@ -19,10 +19,7 @@
name="twitter:description"
content="Ghostfolio is a lightweight wealth management application for individuals to keep track of stocks, ETFs or cryptocurrencies"
/>
<meta
name="twitter:image"
content="https://www.ghostfol.io/assets/cover.png"
/>
<meta name="twitter:image" content="https://ghostfol.io/assets/cover.png" />
<meta
name="twitter:title"
content="Ghostfolio Open Source Wealth Management Software"
@ -37,12 +34,9 @@
content="Ghostfolio Open Source Wealth Management Software"
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.ghostfol.io" />
<meta
property="og:image"
content="https://www.ghostfol.io/assets/cover.png"
/>
<meta property="og:updated_time" content="2022-05-28T00:00:00+00:00" />
<meta property="og:url" content="https://ghostfol.io" />
<meta property="og:image" content="https://ghostfol.io/assets/cover.png" />
<meta property="og:updated_time" content="2022-08-18T00:00:00+00:00" />
<meta
property="og:site_name"
content="Ghostfolio Open Source Wealth Management Software"
@ -51,26 +45,26 @@
<link
rel="apple-touch-icon"
sizes="180x180"
href="/assets/apple-touch-icon.png"
href="../assets/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/assets/favicon-32x32.png"
href="../assets/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/assets/favicon-16x16.png"
href="../assets/favicon-16x16.png"
/>
<link rel="manifest" href="/assets/site.webmanifest" />
<link rel="manifest" href="../assets/site.webmanifest" />
</head>
<body>
<gf-root></gf-root>
<script type="module" src="ionicons/ionicons.esm.js"></script>
<script type="module" src="../ionicons/ionicons.esm.js"></script>
<script nomodule="" src="ionicons.js"></script>
<noscript

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@
@import '~bootstrap/scss/root';
@import '~bootstrap/scss/reboot';
@import '~bootstrap/scss/type';
// @import '~bootstrap/scss/images';
@import '~bootstrap/scss/images';
// @import '~bootstrap/scss/code';
@import '~bootstrap/scss/grid';
// @import '~bootstrap/scss/tables';

View File

@ -2,16 +2,6 @@ import { DataSource } from '@prisma/client';
import { JobOptions, JobStatus } from 'bull';
import ms from 'ms';
import { ToggleOption } from './types';
export const defaultDateRangeOptions: ToggleOption[] = [
{ label: 'Today', value: '1d' },
{ label: 'YTD', value: 'ytd' },
{ label: '1Y', value: '1y' },
{ label: '5Y', value: '5y' },
{ label: 'Max', value: 'max' }
];
export const DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f';
export const ghostfolioScraperApiSymbolPrefix = '_GF_';
@ -49,6 +39,7 @@ export const DATA_GATHERING_QUEUE_PRIORITY_LOW = Number.MAX_SAFE_INTEGER;
export const DATA_GATHERING_QUEUE_PRIORITY_HIGH = 1;
export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
export const DEFAULT_LANGUAGE_CODE = 'en';
export const GATHER_ASSET_PROFILE_PROCESS = 'GATHER_ASSET_PROFILE';
export const GATHER_ASSET_PROFILE_PROCESS_OPTIONS: JobOptions = {

View File

@ -132,7 +132,9 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
}
public onDeleteActivity(aId: string) {
const confirmation = confirm('Do you really want to delete this activity?');
const confirmation = confirm(
$localize`Do you really want to delete this activity?`
);
if (confirmation) {
this.activityDeleted.emit(aId);

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "1.178.0",
"version": "1.180.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"scripts": {
@ -13,7 +13,7 @@
"affected:lint": "nx affected:lint",
"affected:test": "nx affected:test",
"angular": "node --max_old_space_size=32768 ./node_modules/@angular/cli/bin/ng",
"build:all": "nx run api:build:production && nx run client:build:production && yarn replace-placeholders-in-build",
"build:all": "nx run api:build:production && nx run client:build:production --localize && yarn replace-placeholders-in-build",
"build:dev": "nx run api:build && nx run client:build && yarn replace-placeholders-in-build",
"build:storybook": "nx run ui:build-storybook",
"clean": "rimraf dist",
@ -29,6 +29,7 @@
"database:validate": "prisma validate",
"dep-graph": "nx dep-graph",
"e2e": "ng e2e",
"extract-locales": "ng extract-i18n client --output-path ./apps/client/src/locales",
"format": "nx format:write",
"format:check": "nx format:check",
"format:write": "nx format:write",
@ -40,7 +41,7 @@
"postinstall": "prisma generate && ngcc --properties es2020 browser module main",
"replace-placeholders-in-build": "node ./replace.build.js",
"start": "node dist/apps/api/main",
"start:client": "ng serve client --hmr -o",
"start:client": "ng serve client --configuration=development-en --hmr -o",
"start:prod": "node apps/api/main",
"start:server": "nx serve api --watch",
"start:storybook": "nx run ui:storybook",
@ -110,6 +111,7 @@
"ionicons": "5.5.1",
"lodash": "4.17.21",
"ms": "3.0.0-canary.1",
"ng-extract-i18n-merge": "2.1.2",
"ngx-device-detector": "3.0.0",
"ngx-markdown": "14.0.1",
"ngx-skeleton-loader": "5.0.0",

121
yarn.lock
View File

@ -18,6 +18,14 @@
"@angular-devkit/core" "14.1.0"
rxjs "6.6.7"
"@angular-devkit/architect@^0.1301.0":
version "0.1301.4"
resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1301.4.tgz#2fc51bcae0dcb581c8be401e2fde7bbd10b43076"
integrity sha512-p6G8CEMnE+gYwxRyEttj3QGsuNJ3Kusi7iwBIzWyf2RpJSdGzXdwUEiRGg6iS0YHFr06/ZFfAWfnM2DQvNm4TA==
dependencies:
"@angular-devkit/core" "13.1.4"
rxjs "6.6.7"
"@angular-devkit/build-angular@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-14.1.0.tgz#f2aaaa4d28c0f39fdcfee6c0241e1bedb576b2d4"
@ -96,6 +104,18 @@
"@angular-devkit/architect" "0.1401.0"
rxjs "6.6.7"
"@angular-devkit/core@13.1.4":
version "13.1.4"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-13.1.4.tgz#b5b6ddd674ae351f83beff2e4a0d702096bdfd47"
integrity sha512-225Gjy4iVxh5Jo9njJnaG75M/Dt95UW+dEPCGWKV5E/++7UUlXlo9sNWq8x2vJm2nhtsPkpnXNOt4pW1mIDwqQ==
dependencies:
ajv "8.8.2"
ajv-formats "2.1.1"
fast-json-stable-stringify "2.1.0"
magic-string "0.25.7"
rxjs "6.6.7"
source-map "0.7.3"
"@angular-devkit/core@14.0.5":
version "14.0.5"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-14.0.5.tgz#19f5940b53aeb0ce56479c44670d3bc3b2df92b1"
@ -118,6 +138,17 @@
rxjs "6.6.7"
source-map "0.7.4"
"@angular-devkit/core@14.1.2", "@angular-devkit/core@^13.0.0 || ^14.0.0":
version "14.1.2"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-14.1.2.tgz#f4c287295065b3845e85a84e29f12629b4a52cf9"
integrity sha512-fIfymD1erjoj1eVh7pa/dvOtUhSd7sEOGuWEJ81HJqdzwZbPWweRu3Nh/9kj/ttUy8xawWfdJHLwyG2KnRu0DA==
dependencies:
ajv "8.11.0"
ajv-formats "2.1.1"
jsonc-parser "3.1.0"
rxjs "6.6.7"
source-map "0.7.4"
"@angular-devkit/schematics@14.0.5":
version "14.0.5"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-14.0.5.tgz#01777d2ad473d35bdfdbbb751521c43421ad9772"
@ -140,6 +171,17 @@
ora "5.4.1"
rxjs "6.6.7"
"@angular-devkit/schematics@14.1.2", "@angular-devkit/schematics@^13.0.0 || ^14.0.0":
version "14.1.2"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-14.1.2.tgz#6fb81aaece4ad1a24f3dd7b213e2f7000b41adbf"
integrity sha512-vC9UA3heXbq9CAkwMXHJfIu0G7Ty2oTJ5PtrrFQpig1PrFnRfr4sg+qRS1CLsEAJYQNo14xV55OQkKEPTij/Gg==
dependencies:
"@angular-devkit/core" "14.1.2"
jsonc-parser "3.1.0"
magic-string "0.26.2"
ora "5.4.1"
rxjs "6.6.7"
"@angular-eslint/bundled-angular-compiler@14.0.2":
version "14.0.2"
resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-14.0.2.tgz#16aed25430b96fac50d069b2e44dae89fa4b12ad"
@ -3705,6 +3747,15 @@
"@angular-devkit/schematics" "14.1.0"
jsonc-parser "3.1.0"
"@schematics/angular@^13.0.0 || ^14.0.0":
version "14.1.2"
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-14.1.2.tgz#edc828c3bf3e5eb9c55d11e560d3fd680819021c"
integrity sha512-vttvYpffkG/cy9aUCXLW+Dc0msUNbyoFXTQRHN/MYX5uNVvXaRCEzWVE3tp87Dt5XlGo2r9e21gNAoY1TyXj3Q==
dependencies:
"@angular-devkit/core" "14.1.2"
"@angular-devkit/schematics" "14.1.2"
jsonc-parser "3.1.0"
"@simplewebauthn/browser@5.2.1":
version "5.2.1"
resolved "https://registry.yarnpkg.com/@simplewebauthn/browser/-/browser-5.2.1.tgz#569252a9f235a99aae90c4d1cc6c441f42637b8e"
@ -5864,6 +5915,16 @@ ajv@8.11.0, ajv@^8.0.0, ajv@^8.8.0:
require-from-string "^2.0.2"
uri-js "^4.2.2"
ajv@8.8.2:
version "8.8.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.8.2.tgz#01b4fef2007a28bf75f0b7fc009f62679de4abbb"
integrity sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==
dependencies:
fast-deep-equal "^3.1.1"
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
uri-js "^4.2.2"
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.9.1:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@ -7679,11 +7740,16 @@ commander@^6.2.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
commander@^8.0.0, commander@^8.3.0:
commander@^8.0.0, commander@^8.3.0, commander@~8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
commander@~7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff"
integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg==
common-path-prefix@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0"
@ -10319,7 +10385,7 @@ fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.2.7, fast-glob@^3.2.9:
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
@ -13113,6 +13179,11 @@ jest@27.5.1:
import-local "^3.0.2"
jest-cli "^27.5.1"
js-levenshtein@~1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
js-sha256@0.9.0, js-sha256@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
@ -13812,6 +13883,13 @@ luxon@^1.23.x, luxon@^1.28.0:
resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf"
integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==
magic-string@0.25.7:
version "0.25.7"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==
dependencies:
sourcemap-codec "^1.4.4"
magic-string@0.26.1:
version "0.26.1"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.1.tgz#ba9b651354fa9512474199acecf9c6dbe93f97fd"
@ -14495,6 +14573,19 @@ nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0:
resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz#26c8a3cee6cc05fbcf1e333cd2fc3e003326c0b5"
integrity sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==
ng-extract-i18n-merge@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ng-extract-i18n-merge/-/ng-extract-i18n-merge-2.1.2.tgz#1a86beccc85cf2fbeb7538009feeb8a6787d602a"
integrity sha512-dRo/oO4m3tr26uE4+DHJh0iTqSV3qVl+WGTSHGKKyAM628dP3hZunhBTs7fCpLs57aEex84cJax8X+Y8wmBXTQ==
dependencies:
"@angular-devkit/architect" "^0.1301.0"
"@angular-devkit/core" "^13.0.0 || ^14.0.0"
"@angular-devkit/schematics" "^13.0.0 || ^14.0.0"
"@schematics/angular" "^13.0.0 || ^14.0.0"
xliff-simple-merge "~0.12.4"
xml_normalize "~0.8.4"
xmldoc "~1.1.2"
ngx-device-detector@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ngx-device-detector/-/ngx-device-detector-3.0.0.tgz#9c5b1db66e03837d5de0e93fe4a1de93948c9c81"
@ -17901,7 +17992,7 @@ source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
sourcemap-codec@^1.4.8:
sourcemap-codec@^1.4.4, sourcemap-codec@^1.4.8:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
@ -19873,16 +19964,40 @@ x-default-browser@^0.4.0:
optionalDependencies:
default-browser-id "^1.0.4"
xliff-simple-merge@~0.12.4:
version "0.12.4"
resolved "https://registry.yarnpkg.com/xliff-simple-merge/-/xliff-simple-merge-0.12.4.tgz#d3e712c71bec4344558b11a5e2578915cee943ec"
integrity sha512-Rk88D62XUoMgyBLEILpgOx3ARnxONLwH/xz+/c1HsRHYPHDnuZhYORkwrBCXsE942esG+J+OUCCOiydXJxY4Ug==
dependencies:
commander "~8.3.0"
js-levenshtein "~1.1.6"
xmldoc "~1.1.2"
xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
xml_normalize@~0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/xml_normalize/-/xml_normalize-0.8.4.tgz#589c88a0c3f0fe7e89c94ae14b82de02ebfc1f2c"
integrity sha512-I+PiD+W/fS7YOWb4THhQfvkzd1mCuTT3KoRt4PTxiWYbH1RY74w5ewcIas8ja9APC8Ho2izg8VcNnTTN35qM9A==
dependencies:
commander "~7.1.0"
xmldoc "~1.1.2"
xmlchars@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xmldoc@~1.1.2:
version "1.1.4"
resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-1.1.4.tgz#ea4e26dca76b1d218a2f777018bce404ba374a86"
integrity sha512-rQshsBGR5s7pUNENTEncpI2LTCuzicri0DyE4SCV5XmS0q81JS8j1iPijP0Q5c4WLGbKh3W92hlOwY6N9ssW1w==
dependencies:
sax "^1.2.4"
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"