Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
f79d60014b | |||
5b7409d08e | |||
6230aa87e2 | |||
8b615d2f56 | |||
4100446cac | |||
ad3e6d637c | |||
aa87262954 | |||
01b6bb5b99 | |||
884b7f4de7 | |||
3f8a2b47f9 | |||
e2e4c9be3c | |||
0f7c6ff0fe | |||
703a96f4db | |||
42c0560422 | |||
eb63802d01 | |||
6d9191a46f | |||
6744245d8b | |||
8f64a77a9d | |||
0d5fc7655b |
28
CHANGELOG.md
28
CHANGELOG.md
@ -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).
|
||||
|
||||
## 2.6.0 - 2023-09-26
|
||||
|
||||
### Added
|
||||
|
||||
- Added the management of tags in the admin control panel
|
||||
- Added a blog post: _Hacktoberfest 2023_
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded `prettier` from version `3.0.2` to `3.0.3`
|
||||
- Upgraded `yahoo-finance2` from version `2.5.0` to `2.7.0`
|
||||
|
||||
## 2.5.0 - 2023-09-23
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for translated activity types in the activities table
|
||||
- Added support for dates in `DD.MM.YYYY` format in the activities import
|
||||
- Set up the language localization for Türkçe (`tr`)
|
||||
|
||||
### Changed
|
||||
|
||||
- Skipped creating queue jobs for asset profiles with `MANUAL` data source on creating a new activity
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue with the cash position in the holdings table
|
||||
|
||||
## 2.4.0 - 2023-09-19
|
||||
|
||||
### Added
|
||||
|
@ -27,7 +27,7 @@ New: [Ghostfolio 2.0](https://ghostfol.io/en/blog/2023/09/ghostfolio-2)
|
||||
|
||||
## Ghostfolio Premium
|
||||
|
||||
Our official **[Ghostfolio Premium](https://ghostfol.io/en/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. The revenue is used for covering the hosting costs.
|
||||
Our official **[Ghostfolio Premium](https://ghostfol.io/en/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. Revenue is used to cover the costs of the hosting infrastructure and to fund ongoing development.
|
||||
|
||||
If you prefer to run Ghostfolio on your own infrastructure, please find further instructions in the [Self-hosting](#self-hosting) section.
|
||||
|
||||
|
@ -39,6 +39,7 @@ import { RedisCacheModule } from './redis-cache/redis-cache.module';
|
||||
import { SitemapModule } from './sitemap/sitemap.module';
|
||||
import { SubscriptionModule } from './subscription/subscription.module';
|
||||
import { SymbolModule } from './symbol/symbol.module';
|
||||
import { TagModule } from './tag/tag.module';
|
||||
import { UserModule } from './user/user.module';
|
||||
|
||||
@Module({
|
||||
@ -101,6 +102,7 @@ import { UserModule } from './user/user.module';
|
||||
SitemapModule,
|
||||
SubscriptionModule,
|
||||
SymbolModule,
|
||||
TagModule,
|
||||
TwitterBotModule,
|
||||
UserModule
|
||||
],
|
||||
|
@ -410,7 +410,7 @@ export class ImportService {
|
||||
currency,
|
||||
userCurrency
|
||||
),
|
||||
//@ts-ignore
|
||||
// @ts-ignore
|
||||
SymbolProfile: assetProfile,
|
||||
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||
value,
|
||||
|
@ -147,8 +147,9 @@ export class OrderController {
|
||||
userId: this.request.user.id
|
||||
});
|
||||
|
||||
if (!order.isDraft) {
|
||||
// Gather symbol data in the background, if not draft
|
||||
if (data.dataSource && !order.isDraft) {
|
||||
// Gather symbol data in the background, if data source is set
|
||||
// (not MANUAL) and not draft
|
||||
this.dataGatheringService.gatherSymbols([
|
||||
{
|
||||
dataSource: data.dataSource,
|
||||
|
@ -123,20 +123,22 @@ export class OrderService {
|
||||
};
|
||||
}
|
||||
|
||||
this.dataGatheringService.addJobToQueue({
|
||||
data: {
|
||||
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
||||
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
||||
},
|
||||
name: GATHER_ASSET_PROFILE_PROCESS,
|
||||
opts: {
|
||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||
jobId: getAssetProfileIdentifier({
|
||||
if (data.SymbolProfile.connectOrCreate.create.dataSource !== 'MANUAL') {
|
||||
this.dataGatheringService.addJobToQueue({
|
||||
data: {
|
||||
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
||||
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
name: GATHER_ASSET_PROFILE_PROCESS,
|
||||
opts: {
|
||||
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
|
||||
jobId: getAssetProfileIdentifier({
|
||||
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
||||
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
delete data.accountId;
|
||||
delete data.assetClass;
|
||||
|
@ -47,6 +47,7 @@ export class PlatformController {
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
return this.platformService.createPlatform(data);
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,18 @@ import { Platform, Prisma } from '@prisma/client';
|
||||
export class PlatformService {
|
||||
public constructor(private readonly prismaService: PrismaService) {}
|
||||
|
||||
public async createPlatform(data: Prisma.PlatformCreateInput) {
|
||||
return this.prismaService.platform.create({
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
public async deletePlatform(
|
||||
where: Prisma.PlatformWhereUniqueInput
|
||||
): Promise<Platform> {
|
||||
return this.prismaService.platform.delete({ where });
|
||||
}
|
||||
|
||||
public async getPlatform(
|
||||
platformWhereUniqueInput: Prisma.PlatformWhereUniqueInput
|
||||
): Promise<Platform> {
|
||||
@ -56,12 +68,6 @@ export class PlatformService {
|
||||
});
|
||||
}
|
||||
|
||||
public async createPlatform(data: Prisma.PlatformCreateInput) {
|
||||
return this.prismaService.platform.create({
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
public async updatePlatform({
|
||||
data,
|
||||
where
|
||||
@ -74,10 +80,4 @@ export class PlatformService {
|
||||
where
|
||||
});
|
||||
}
|
||||
|
||||
public async deletePlatform(
|
||||
where: Prisma.PlatformWhereUniqueInput
|
||||
): Promise<Platform> {
|
||||
return this.prismaService.platform.delete({ where });
|
||||
}
|
||||
}
|
||||
|
@ -173,8 +173,14 @@ export class PortfolioController {
|
||||
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
||||
holdings[symbol] = {
|
||||
...portfolioPosition,
|
||||
assetClass: hasDetails ? portfolioPosition.assetClass : undefined,
|
||||
assetSubClass: hasDetails ? portfolioPosition.assetSubClass : undefined,
|
||||
assetClass:
|
||||
hasDetails || portfolioPosition.assetClass === 'CASH'
|
||||
? portfolioPosition.assetClass
|
||||
: undefined,
|
||||
assetSubClass:
|
||||
hasDetails || portfolioPosition.assetSubClass === 'CASH'
|
||||
? portfolioPosition.assetSubClass
|
||||
: undefined,
|
||||
countries: hasDetails ? portfolioPosition.countries : [],
|
||||
currency: hasDetails ? portfolioPosition.currency : undefined,
|
||||
markets: hasDetails ? portfolioPosition.markets : undefined,
|
||||
|
6
apps/api/src/app/tag/create-tag.dto.ts
Normal file
6
apps/api/src/app/tag/create-tag.dto.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
export class CreateTagDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
}
|
104
apps/api/src/app/tag/tag.controller.ts
Normal file
104
apps/api/src/app/tag/tag.controller.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpException,
|
||||
Inject,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
UseGuards
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Tag } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { CreateTagDto } from './create-tag.dto';
|
||||
import { TagService } from './tag.service';
|
||||
import { UpdateTagDto } from './update-tag.dto';
|
||||
|
||||
@Controller('tag')
|
||||
export class TagController {
|
||||
public constructor(
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||
private readonly tagService: TagService
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getTags() {
|
||||
return this.tagService.getTagsWithActivityCount();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async createTag(@Body() data: CreateTagDto): Promise<Tag> {
|
||||
if (!hasPermission(this.request.user.permissions, permissions.createTag)) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
return this.tagService.createTag(data);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async updateTag(@Param('id') id: string, @Body() data: UpdateTagDto) {
|
||||
if (!hasPermission(this.request.user.permissions, permissions.updateTag)) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
const originalTag = await this.tagService.getTag({
|
||||
id
|
||||
});
|
||||
|
||||
if (!originalTag) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
return this.tagService.updateTag({
|
||||
data: {
|
||||
...data
|
||||
},
|
||||
where: {
|
||||
id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async deleteTag(@Param('id') id: string) {
|
||||
if (!hasPermission(this.request.user.permissions, permissions.deleteTag)) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
const originalTag = await this.tagService.getTag({
|
||||
id
|
||||
});
|
||||
|
||||
if (!originalTag) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
return this.tagService.deleteTag({ id });
|
||||
}
|
||||
}
|
13
apps/api/src/app/tag/tag.module.ts
Normal file
13
apps/api/src/app/tag/tag.module.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { TagController } from './tag.controller';
|
||||
import { TagService } from './tag.service';
|
||||
|
||||
@Module({
|
||||
controllers: [TagController],
|
||||
exports: [TagService],
|
||||
imports: [PrismaModule],
|
||||
providers: [TagService]
|
||||
})
|
||||
export class TagModule {}
|
79
apps/api/src/app/tag/tag.service.ts
Normal file
79
apps/api/src/app/tag/tag.service.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Tag } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class TagService {
|
||||
public constructor(private readonly prismaService: PrismaService) {}
|
||||
|
||||
public async createTag(data: Prisma.TagCreateInput) {
|
||||
return this.prismaService.tag.create({
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteTag(where: Prisma.TagWhereUniqueInput): Promise<Tag> {
|
||||
return this.prismaService.tag.delete({ where });
|
||||
}
|
||||
|
||||
public async getTag(
|
||||
tagWhereUniqueInput: Prisma.TagWhereUniqueInput
|
||||
): Promise<Tag> {
|
||||
return this.prismaService.tag.findUnique({
|
||||
where: tagWhereUniqueInput
|
||||
});
|
||||
}
|
||||
|
||||
public async getTags({
|
||||
cursor,
|
||||
orderBy,
|
||||
skip,
|
||||
take,
|
||||
where
|
||||
}: {
|
||||
cursor?: Prisma.TagWhereUniqueInput;
|
||||
orderBy?: Prisma.TagOrderByWithRelationInput;
|
||||
skip?: number;
|
||||
take?: number;
|
||||
where?: Prisma.TagWhereInput;
|
||||
} = {}) {
|
||||
return this.prismaService.tag.findMany({
|
||||
cursor,
|
||||
orderBy,
|
||||
skip,
|
||||
take,
|
||||
where
|
||||
});
|
||||
}
|
||||
|
||||
public async getTagsWithActivityCount() {
|
||||
const tagsWithOrderCount = await this.prismaService.tag.findMany({
|
||||
include: {
|
||||
_count: {
|
||||
select: { orders: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return tagsWithOrderCount.map(({ _count, id, name }) => {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
activityCount: _count.orders
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async updateTag({
|
||||
data,
|
||||
where
|
||||
}: {
|
||||
data: Prisma.TagUpdateInput;
|
||||
where: Prisma.TagWhereUniqueInput;
|
||||
}): Promise<Tag> {
|
||||
return this.prismaService.tag.update({
|
||||
data,
|
||||
where
|
||||
});
|
||||
}
|
||||
}
|
9
apps/api/src/app/tag/update-tag.dto.ts
Normal file
9
apps/api/src/app/tag/update-tag.dto.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
export class UpdateTagDto {
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
name: string;
|
||||
}
|
@ -58,6 +58,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-capmon</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-copilot-money</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -142,6 +146,10 @@
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-snowball-analytics</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-stockmarketeye</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-sumio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -254,6 +262,10 @@
|
||||
<loc>https://ghostfol.io/en/blog/2023/09/ghostfolio-2</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2023/09/hacktoberfest-2023</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/faq</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -292,6 +304,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-capmon</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-copilot-money</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -376,6 +392,10 @@
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-snowball-analytics</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-stockmarketeye</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-sumio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -558,6 +578,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-campmon</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-copilot-money</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -642,6 +666,10 @@
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-snowball-analytics</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-stockmarketeye</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-sumio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -670,6 +698,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-capmon</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-copilot-money</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -754,6 +786,10 @@
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-snowball-analytics</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-stockmarketeye</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-sumio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -868,4 +904,8 @@
|
||||
<loc>https://ghostfol.io/pt/sobre/politica-de-privacidade</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/tr</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
</urlset>
|
||||
|
@ -18,7 +18,8 @@ const descriptions = {
|
||||
fr: 'Ghostfolio est un dashboard de finances personnelles qui permet de suivre vos actifs comme les actions, les ETF ou les crypto-monnaies sur plusieurs plateformes.',
|
||||
it: 'Ghostfolio è un dashboard di finanza personale per tenere traccia delle vostre attività come azioni, ETF o criptovalute su più piattaforme.',
|
||||
nl: 'Ghostfolio is een persoonlijk financieel dashboard om uw activa zoals aandelen, ETF’s of cryptocurrencies over meerdere platforms bij te houden.',
|
||||
pt: 'Ghostfolio é um dashboard de finanças pessoais para acompanhar os seus activos como acções, ETFs ou criptomoedas em múltiplas plataformas.'
|
||||
pt: 'Ghostfolio é um dashboard de finanças pessoais para acompanhar os seus activos como acções, ETFs ou criptomoedas em múltiplas plataformas.',
|
||||
tr: 'Ghostfolio, hisse senetleri, ETF’ler veya kripto para birimleri gibi varlıklarınızı birden fazla platformda takip etmenizi sağlayan bir kişisel finans panosudur.'
|
||||
};
|
||||
|
||||
const title = 'Ghostfolio – Open Source Wealth Management Software';
|
||||
@ -79,6 +80,10 @@ const locales = {
|
||||
'/en/blog/2023/09/ghostfolio-2': {
|
||||
featureGraphicPath: 'assets/images/blog/ghostfolio-2.jpg',
|
||||
title: `Announcing Ghostfolio 2.0 - ${titleShort}`
|
||||
},
|
||||
'/en/blog/2023/09/hacktoberfest-2023': {
|
||||
featureGraphicPath: 'assets/images/blog/hacktoberfest-2023.png',
|
||||
title: `Hacktoberfest 2023 - ${titleShort}`
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -127,6 +127,10 @@ export class DataGatheringService {
|
||||
uniqueAssets = await this.getUniqueAssets();
|
||||
}
|
||||
|
||||
if (uniqueAssets.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetProfiles =
|
||||
await this.dataProviderService.getAssetProfiles(uniqueAssets);
|
||||
const symbolProfiles =
|
||||
|
@ -63,6 +63,10 @@
|
||||
"baseHref": "/pt/",
|
||||
"localize": ["pt"]
|
||||
},
|
||||
"development-tr": {
|
||||
"baseHref": "/tr/",
|
||||
"localize": ["tr"]
|
||||
},
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
@ -165,6 +169,9 @@
|
||||
"development-pt": {
|
||||
"browserTarget": "client:build:development-pt"
|
||||
},
|
||||
"development-tr": {
|
||||
"browserTarget": "client:build:development-tr"
|
||||
},
|
||||
"production": {
|
||||
"browserTarget": "client:build:production"
|
||||
}
|
||||
@ -182,7 +189,8 @@
|
||||
"messages.fr.xlf",
|
||||
"messages.it.xlf",
|
||||
"messages.nl.xlf",
|
||||
"messages.pt.xlf"
|
||||
"messages.pt.xlf",
|
||||
"messages.tr.xlf"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -226,6 +234,10 @@
|
||||
"pt": {
|
||||
"baseHref": "/pt/",
|
||||
"translation": "apps/client/src/locales/messages.pt.xlf"
|
||||
},
|
||||
"tr": {
|
||||
"baseHref": "/tr/",
|
||||
"translation": "apps/client/src/locales/messages.tr.xlf"
|
||||
}
|
||||
},
|
||||
"sourceLocale": "en"
|
||||
|
@ -152,6 +152,11 @@
|
||||
<li>
|
||||
<a href="../pt" title="Ghostfolio in Português">Português</a>
|
||||
</li>
|
||||
<!--
|
||||
<li>
|
||||
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
|
||||
</li>
|
||||
-->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -72,19 +72,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="info?.tags?.length > 0"
|
||||
class="align-items-start d-flex my-3"
|
||||
>
|
||||
<div class="w-50" i18n>Tags</div>
|
||||
<div class="w-50">
|
||||
<table>
|
||||
<tr *ngFor="let tag of info.tags">
|
||||
<td class="pl-1">{{ tag.name }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex my-3">
|
||||
<div class="w-50" i18n>User Signup</div>
|
||||
<div class="w-50">
|
||||
|
@ -19,7 +19,7 @@ import { get } from 'lodash';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
import { CreateOrUpdatePlatformDialog } from './create-or-update-platform-dialog/create-or-update-account-platform.component';
|
||||
import { CreateOrUpdatePlatformDialog } from './create-or-update-platform-dialog/create-or-update-platform-dialog.component';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@ -114,6 +114,7 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((platforms) => {
|
||||
this.platforms = platforms;
|
||||
|
||||
this.dataSource = new MatTableDataSource(platforms);
|
||||
this.dataSource.sort = this.sort;
|
||||
this.dataSource.sortingDataAccessor = get;
|
||||
@ -130,7 +131,6 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
||||
url: null
|
||||
}
|
||||
},
|
||||
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
@ -170,7 +170,6 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
||||
url
|
||||
}
|
||||
},
|
||||
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
@ -15,8 +15,8 @@ export class CreateOrUpdatePlatformDialog {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
public dialogRef: MatDialogRef<CreateOrUpdatePlatformDialog>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdatePlatformDialogParams
|
||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdatePlatformDialogParams,
|
||||
public dialogRef: MatDialogRef<CreateOrUpdatePlatformDialog>
|
||||
) {}
|
||||
|
||||
public onCancel() {
|
@ -6,7 +6,7 @@ import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
|
||||
import { CreateOrUpdatePlatformDialog } from './create-or-update-account-platform.component';
|
||||
import { CreateOrUpdatePlatformDialog } from './create-or-update-platform-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [CreateOrUpdatePlatformDialog],
|
||||
|
@ -2,14 +2,13 @@
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h2 class="text-center" i18n>Platforms</h2>
|
||||
<gf-admin-platform></gf-admin-platform>
|
||||
<gf-admin-platform />
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h2 class="text-center" i18n>Tags</h2>
|
||||
<gf-admin-tag />
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
@ -2,12 +2,18 @@ import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfAdminPlatformModule } from '@ghostfolio/client/components/admin-platform/admin-platform.module';
|
||||
import { GfAdminTagModule } from '@ghostfolio/client/components/admin-tag/admin-tag.module';
|
||||
|
||||
import { AdminSettingsComponent } from './admin-settings.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdminSettingsComponent],
|
||||
imports: [CommonModule, GfAdminPlatformModule, RouterModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfAdminPlatformModule,
|
||||
GfAdminTagModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAdminSettingsModule {}
|
||||
|
@ -0,0 +1,85 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="d-flex justify-content-end">
|
||||
<a
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[queryParams]="{ createTagDialog: true }"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
Add Tag
|
||||
</a>
|
||||
</div>
|
||||
<table
|
||||
class="gf-table w-100"
|
||||
mat-table
|
||||
matSort
|
||||
matSortActive="name"
|
||||
matSortDirection="asc"
|
||||
[dataSource]="dataSource"
|
||||
>
|
||||
<ng-container matColumnDef="name">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header="name"
|
||||
>
|
||||
<ng-container i18n>Name</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.name }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="activities">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header="activityCount"
|
||||
>
|
||||
<ng-container i18n>Activities</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.activityCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="px-1 text-center"
|
||||
i18n
|
||||
mat-header-cell
|
||||
></th>
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="tagMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-horizontal"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #tagMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onUpdateTag(element)">
|
||||
<ion-icon class="mr-2" name="create-outline"></ion-icon>
|
||||
<span i18n>Edit</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteTag(element.id)">
|
||||
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||
<span i18n>Delete</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,5 @@
|
||||
@import 'apps/client/src/styles/ghostfolio-style';
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
199
apps/client/src/app/components/admin-tag/admin-tag.component.ts
Normal file
199
apps/client/src/app/components/admin-tag/admin-tag.component.ts
Normal file
@ -0,0 +1,199 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto';
|
||||
import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { Tag } from '@prisma/client';
|
||||
import { get } from 'lodash';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
import { CreateOrUpdateTagDialog } from './create-or-update-tag-dialog/create-or-update-tag-dialog.component';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'gf-admin-tag',
|
||||
styleUrls: ['./admin-tag.component.scss'],
|
||||
templateUrl: './admin-tag.component.html'
|
||||
})
|
||||
export class AdminTagComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
public dataSource: MatTableDataSource<Tag> = new MatTableDataSource();
|
||||
public deviceType: string;
|
||||
public displayedColumns = ['name', 'activities', 'actions'];
|
||||
public tags: Tag[];
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private adminService: AdminService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private dialog: MatDialog,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (params['createTagDialog']) {
|
||||
this.openCreateTagDialog();
|
||||
} else if (params['editTagDialog']) {
|
||||
if (this.tags) {
|
||||
const tag = this.tags.find(({ id }) => {
|
||||
return id === params['tagId'];
|
||||
});
|
||||
|
||||
this.openUpdateTagDialog(tag);
|
||||
} else {
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.fetchTags();
|
||||
}
|
||||
|
||||
public onDeleteTag(aId: string) {
|
||||
const confirmation = confirm(
|
||||
$localize`Do you really want to delete this tag?`
|
||||
);
|
||||
|
||||
if (confirmation) {
|
||||
this.deleteTag(aId);
|
||||
}
|
||||
}
|
||||
|
||||
public onUpdateTag({ id }: Tag) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { editTagDialog: true, tagId: id }
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private deleteTag(aId: string) {
|
||||
this.adminService
|
||||
.deleteTag(aId)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.userService
|
||||
.get(true)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe();
|
||||
|
||||
this.fetchTags();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private fetchTags() {
|
||||
this.adminService
|
||||
.fetchTags()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((tags) => {
|
||||
this.tags = tags;
|
||||
this.dataSource = new MatTableDataSource(this.tags);
|
||||
this.dataSource.sort = this.sort;
|
||||
this.dataSource.sortingDataAccessor = get;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
private openCreateTagDialog() {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateTagDialog, {
|
||||
data: {
|
||||
tag: {
|
||||
name: null
|
||||
}
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data) => {
|
||||
const tag: CreateTagDto = data?.tag;
|
||||
|
||||
if (tag) {
|
||||
this.adminService
|
||||
.postTag(tag)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.userService
|
||||
.get(true)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe();
|
||||
|
||||
this.fetchTags();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
}
|
||||
|
||||
private openUpdateTagDialog({ id, name }) {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateTagDialog, {
|
||||
data: {
|
||||
tag: {
|
||||
id,
|
||||
name
|
||||
}
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data) => {
|
||||
const tag: UpdateTagDto = data?.tag;
|
||||
|
||||
if (tag) {
|
||||
this.adminService
|
||||
.putTag(tag)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.userService
|
||||
.get(true)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe();
|
||||
|
||||
this.fetchTags();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
}
|
||||
}
|
26
apps/client/src/app/components/admin-tag/admin-tag.module.ts
Normal file
26
apps/client/src/app/components/admin-tag/admin-tag.module.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { AdminTagComponent } from './admin-tag.component';
|
||||
import { GfCreateOrUpdateTagDialogModule } from './create-or-update-tag-dialog/create-or-update-tag-dialog.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdminTagComponent],
|
||||
exports: [AdminTagComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfCreateOrUpdateTagDialogModule,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAdminTagModule {}
|
@ -0,0 +1,30 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { CreateOrUpdateTagDialogParams } from './interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'h-100' },
|
||||
selector: 'gf-create-or-update-tag-dialog',
|
||||
styleUrls: ['./create-or-update-tag-dialog.scss'],
|
||||
templateUrl: 'create-or-update-tag-dialog.html'
|
||||
})
|
||||
export class CreateOrUpdateTagDialog {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTagDialogParams,
|
||||
public dialogRef: MatDialogRef<CreateOrUpdateTagDialog>
|
||||
) {}
|
||||
|
||||
public onCancel() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<form #addTagForm="ngForm" class="d-flex flex-column h-100">
|
||||
<h1 *ngIf="data.tag.id" i18n mat-dialog-title>Update tag</h1>
|
||||
<h1 *ngIf="!data.tag.id" i18n mat-dialog-title>Add tag</h1>
|
||||
<div class="flex-grow-1 py-3" mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Name</mat-label>
|
||||
<input matInput name="name" required [(ngModel)]="data.tag.name" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="justify-content-end" mat-dialog-actions>
|
||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||
<button
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[disabled]="!addTagForm.form.valid"
|
||||
[mat-dialog-close]="data"
|
||||
>
|
||||
<ng-container i18n>Save</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
@ -0,0 +1,23 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
|
||||
import { CreateOrUpdateTagDialog } from './create-or-update-tag-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [CreateOrUpdateTagDialog],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule
|
||||
]
|
||||
})
|
||||
export class GfCreateOrUpdateTagDialogModule {}
|
@ -0,0 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-mdc-dialog-content {
|
||||
max-height: unset;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import { Tag } from '@prisma/client';
|
||||
|
||||
export interface CreateOrUpdateTagDialogParams {
|
||||
tag: Tag;
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
imports: [MatButtonModule, RouterModule],
|
||||
selector: 'gf-hacktoberfest-2023-page',
|
||||
standalone: true,
|
||||
templateUrl: './hacktoberfest-2023-page.html'
|
||||
})
|
||||
export class Hacktoberfest2023PageComponent {
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
<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">Hacktoberfest 2023</h1>
|
||||
<div class="mb-3 text-muted"><small>2023-09-26</small></div>
|
||||
<img
|
||||
alt="Hacktoberfest 2023 with Ghostfolio Teaser"
|
||||
class="rounded w-100"
|
||||
src="../assets/images/blog/hacktoberfest-2023.png"
|
||||
title="Hacktoberfest 2023 with Ghostfolio"
|
||||
/>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
At Ghostfolio, <a [routerLink]="routerLinkAbout">we</a> are very
|
||||
excited to participate in
|
||||
<a href="https://hacktoberfest.com">Hacktoberfest</a> for the second
|
||||
time, looking forward to connecting with new and enthusiastic
|
||||
open-source contributors. Hacktoberfest is a month-long celebration
|
||||
of open-source projects, their maintainers, and the entire community
|
||||
of contributors. Each October, open source maintainers from all over
|
||||
the world give extra attention to new contributors while guiding
|
||||
them through their first pull requests on
|
||||
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>. This
|
||||
year the event celebrates its 10th anniversary.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">About Ghostfolio</h2>
|
||||
<p>
|
||||
<a href="https://ghostfol.io">Ghostfolio</a> is a modern web
|
||||
application for managing personal finances. The software aggregates
|
||||
your assets and empowers informed decision-making to help you
|
||||
balance your portfolio or plan for future investments.
|
||||
</p>
|
||||
<p>
|
||||
Ghostfolio is written in
|
||||
<a href="https://www.typescriptlang.org">TypeScript</a> and
|
||||
organized as an <a href="https://nx.dev">Nx</a> workspace, utilizing
|
||||
the latest framework releases. The backend is based on
|
||||
<a href="https://nestjs.com">NestJS</a> in combination with
|
||||
<a href="https://www.postgresql.org">PostgreSQL</a> as a database
|
||||
together with <a href="https://www.prisma.io">Prisma</a> and
|
||||
<a href="https://redis.io">Redis</a> for caching. The frontend is
|
||||
built with <a href="https://angular.io">Angular</a>.
|
||||
</p>
|
||||
<p>
|
||||
The software is used daily by a thriving global community. With over
|
||||
<a [routerLink]="['/open']">2’600 stars on GitHub</a> and
|
||||
<a [routerLink]="['/open']">300’000+ pulls on Docker Hub</a>,
|
||||
Ghostfolio has gained widespread recognition for its user-friendly
|
||||
experience and simplicity.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">How to contribute?</h2>
|
||||
<p>
|
||||
Each contribution can make a meaningful impact. Whether it involves
|
||||
implementing new features, resolving bugs, refactoring code,
|
||||
enhancing documentation, adding unit tests, or translating content
|
||||
into another language, you can actively shape our project.
|
||||
</p>
|
||||
<p>
|
||||
Are you not yet familiar with our code base? That is not a problem.
|
||||
We have applied the label <code>hacktoberfest</code> to a few
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3Ahacktoberfest"
|
||||
>issues</a
|
||||
>
|
||||
that are well suited for newcomers.
|
||||
</p>
|
||||
<p>
|
||||
The official Hacktoberfest website provides some valuable
|
||||
<a
|
||||
href="https://hacktoberfest.com/participation/#beginner-resources"
|
||||
>resources for beginners</a
|
||||
>
|
||||
to start contributing in open source.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Get support</h2>
|
||||
<p>
|
||||
If you have further questions or ideas, please join our
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
>Slack</a
|
||||
>
|
||||
community or get in touch on X
|
||||
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a>.
|
||||
</p>
|
||||
<p>
|
||||
We look forward to hearing from you.<br />
|
||||
Thomas from Ghostfolio
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<ul class="list-inline">
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Angular</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">Docker</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">Ghostfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">GitHub</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Hacktoberfest</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">NestJS</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Nx</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">October</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">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">Prisma</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">TypeScript</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">UX</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>
|
||||
</ul>
|
||||
</section>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
Hacktoberfest 2023
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -154,6 +154,15 @@ const routes: Routes = [
|
||||
(c) => c.Ghostfolio2PageComponent
|
||||
),
|
||||
title: 'Ghostfolio 2.0'
|
||||
},
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
path: '2023/09/hacktoberfest-2023',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'./2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component'
|
||||
).then((c) => c.Hacktoberfest2023PageComponent),
|
||||
title: 'Hacktoberfest 2023'
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -8,6 +8,30 @@
|
||||
finance</small
|
||||
>
|
||||
</h1>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex overflow-hidden w-100"
|
||||
href="../en/blog/2023/09/hacktoberfest-2023"
|
||||
>
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="h6 m-0 text-truncate">Hacktoberfest 2023</div>
|
||||
<div class="d-flex text-muted">2023-09-26</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 appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
|
@ -142,11 +142,11 @@
|
||||
>
|
||||
<mat-card-content
|
||||
><a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> is a fully
|
||||
managed Ghostfolio cloud offering for ambitious investors. The revenue
|
||||
is used to cover the hosting infrastructure and to fund the 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
|
||||
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">
|
||||
|
@ -245,7 +245,7 @@
|
||||
<h4 i18n>Multi-Language</h4>
|
||||
<p class="m-0">
|
||||
Use Ghostfolio in multiple languages: English, Dutch, French,
|
||||
German, Italian, Portuguese and Spanish are currently
|
||||
German, Italian, Portuguese, Spanish and Turkish are currently
|
||||
supported.
|
||||
</p>
|
||||
</div>
|
||||
|
@ -80,8 +80,6 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
const { globalPermissions } = this.dataService.fetchInfo();
|
||||
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.impersonationStorageService
|
||||
|
@ -6,8 +6,8 @@
|
||||
<p i18n>
|
||||
Our official Ghostfolio Premium cloud offering is the easiest way to
|
||||
get started. Due to the time it saves, this will be the best option
|
||||
for most people. The revenue is used to cover the hosting
|
||||
infrastructure and to fund the ongoing development of Ghostfolio.
|
||||
for most people. Revenue is used to cover the costs of the hosting
|
||||
infrastructure and to fund ongoing development.
|
||||
</p>
|
||||
<p *ngIf="user?.subscription?.type === 'Basic'">
|
||||
If you plan to open an account at <i>DEGIRO</i>, <i>frankly</i>,
|
||||
|
@ -96,16 +96,16 @@
|
||||
Open Source Software
|
||||
</td>
|
||||
<td class="mat-mdc-cell px-1 py-2">
|
||||
<ng-container *ngIf="product1.isOpenSource === true" i18n
|
||||
<ng-container *ngIf="product1.isOpenSource" i18n
|
||||
>✅ Yes</ng-container
|
||||
><ng-container *ngIf="product1.isOpenSource === false" i18n
|
||||
><ng-container *ngIf="!product1.isOpenSource" i18n
|
||||
>❌ No</ng-container
|
||||
>
|
||||
</td>
|
||||
<td class="mat-mdc-cell px-1 py-2">
|
||||
<ng-container *ngIf="product2.isOpenSource === true" i18n
|
||||
<ng-container *ngIf="product2.isOpenSource" i18n
|
||||
>✅ Yes</ng-container
|
||||
><ng-container *ngIf="product2.isOpenSource === false" i18n
|
||||
><ng-container *ngIf="!product2.isOpenSource" i18n
|
||||
>❌ No
|
||||
</ng-container>
|
||||
</td>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Product } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { AltooPageComponent } from './products/altoo-page.component';
|
||||
import { CapMonPageComponent } from './products/capmon-page.component';
|
||||
import { CopilotMoneyPageComponent } from './products/copilot-money-page.component';
|
||||
import { DeltaPageComponent } from './products/delta-page.component';
|
||||
import { DivvyDiaryPageComponent } from './products/divvydiary-page.component';
|
||||
@ -22,6 +23,7 @@ import { SeekingAlphaPageComponent } from './products/seeking-alpha-page.compone
|
||||
import { SharesightPageComponent } from './products/sharesight-page.component';
|
||||
import { SimplePortfolioPageComponent } from './products/simple-portfolio-page.component';
|
||||
import { SnowballAnalyticsPageComponent } from './products/snowball-analytics-page.component';
|
||||
import { StockMarketEyePageComponent } from './products/stockmarketeye-page.component';
|
||||
import { SumioPageComponent } from './products/sumio-page.component';
|
||||
import { UtlunaPageComponent } from './products/utluna-page.component';
|
||||
import { YeekateePageComponent } from './products/yeekatee-page.component';
|
||||
@ -45,7 +47,7 @@ export const products: Product[] = [
|
||||
],
|
||||
name: 'Ghostfolio',
|
||||
origin: $localize`Switzerland`,
|
||||
pricingPerYear: '$19',
|
||||
pricingPerYear: '$24',
|
||||
region: $localize`Global`,
|
||||
slogan: 'Open Source Wealth Management',
|
||||
useAnonymously: true
|
||||
@ -54,18 +56,25 @@ export const products: Product[] = [
|
||||
component: AltooPageComponent,
|
||||
founded: 2017,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'altoo',
|
||||
name: 'Altoo Wealth Platform',
|
||||
origin: $localize`Switzerland`,
|
||||
slogan: 'Simplicity for Complex Wealth'
|
||||
},
|
||||
{
|
||||
component: CapMonPageComponent,
|
||||
founded: 2022,
|
||||
key: 'capmon',
|
||||
name: 'CapMon.org',
|
||||
origin: $localize`Germany`,
|
||||
note: 'Sunset in 2023',
|
||||
slogan: 'Next Generation Assets Tracking'
|
||||
},
|
||||
{
|
||||
component: CopilotMoneyPageComponent,
|
||||
founded: 2019,
|
||||
hasFreePlan: false,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'copilot-money',
|
||||
name: 'Copilot Money',
|
||||
origin: $localize`United States`,
|
||||
@ -77,7 +86,6 @@ export const products: Product[] = [
|
||||
founded: 2017,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'delta',
|
||||
name: 'Delta Investment Tracker',
|
||||
note: 'Acquired by eToro',
|
||||
@ -89,7 +97,6 @@ export const products: Product[] = [
|
||||
founded: 2019,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'divvydiary',
|
||||
languages: ['Deutsch', 'English'],
|
||||
name: 'DivvyDiary',
|
||||
@ -102,7 +109,6 @@ export const products: Product[] = [
|
||||
founded: 2020,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'exirio',
|
||||
name: 'Exirio',
|
||||
origin: $localize`United States`,
|
||||
@ -113,7 +119,6 @@ export const products: Product[] = [
|
||||
component: FolisharePageComponent,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'folishare',
|
||||
languages: ['Deutsch', 'English'],
|
||||
name: 'folishare',
|
||||
@ -126,7 +131,6 @@ export const products: Product[] = [
|
||||
founded: 2020,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'getquin',
|
||||
languages: ['Deutsch', 'English'],
|
||||
name: 'getquin',
|
||||
@ -138,7 +142,6 @@ export const products: Product[] = [
|
||||
component: GoSpatzPageComponent,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'gospatz',
|
||||
name: 'goSPATZ',
|
||||
origin: $localize`Germany`,
|
||||
@ -149,7 +152,6 @@ export const products: Product[] = [
|
||||
founded: 2011,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'justetf',
|
||||
name: 'justETF',
|
||||
origin: $localize`Germany`,
|
||||
@ -161,7 +163,6 @@ export const products: Product[] = [
|
||||
founded: 2019,
|
||||
hasFreePlan: false,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'kubera',
|
||||
name: 'Kubera®',
|
||||
origin: $localize`United States`,
|
||||
@ -173,7 +174,6 @@ export const products: Product[] = [
|
||||
founded: 2022,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'markets.sh',
|
||||
languages: ['English'],
|
||||
name: 'markets.sh',
|
||||
@ -186,7 +186,6 @@ export const products: Product[] = [
|
||||
component: MaybeFinancePageComponent,
|
||||
founded: 2021,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'maybe-finance',
|
||||
languages: ['English'],
|
||||
name: 'Maybe Finance',
|
||||
@ -200,7 +199,6 @@ export const products: Product[] = [
|
||||
component: MonsePageComponent,
|
||||
hasFreePlan: false,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'monse',
|
||||
name: 'Monse',
|
||||
pricingPerYear: '$60',
|
||||
@ -211,7 +209,6 @@ export const products: Product[] = [
|
||||
founded: 2020,
|
||||
hasSelfHostingAbility: false,
|
||||
hasFreePlan: true,
|
||||
isOpenSource: false,
|
||||
key: 'parqet',
|
||||
name: 'Parqet',
|
||||
note: 'Originally named as Tresor One',
|
||||
@ -224,7 +221,6 @@ export const products: Product[] = [
|
||||
component: PlannixPageComponent,
|
||||
founded: 2023,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'plannix',
|
||||
name: 'Plannix',
|
||||
origin: $localize`Italy`,
|
||||
@ -234,7 +230,6 @@ export const products: Product[] = [
|
||||
component: PortfolioDividendTrackerPageComponent,
|
||||
hasFreePlan: false,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'portfolio-dividend-tracker',
|
||||
languages: ['English', 'Nederlands'],
|
||||
name: 'Portfolio Dividend Tracker',
|
||||
@ -247,7 +242,6 @@ export const products: Product[] = [
|
||||
founded: 2021,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'portseido',
|
||||
languages: ['Deutsch', 'English', 'Français', 'Nederlands'],
|
||||
name: 'Portseido',
|
||||
@ -260,7 +254,6 @@ export const products: Product[] = [
|
||||
founded: 2021,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: true,
|
||||
isOpenSource: false,
|
||||
key: 'projectionlab',
|
||||
name: 'ProjectionLab',
|
||||
origin: $localize`United States`,
|
||||
@ -272,7 +265,6 @@ export const products: Product[] = [
|
||||
founded: 2004,
|
||||
hasFreePlan: false,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'seeking-alpha',
|
||||
name: 'Seeking Alpha',
|
||||
origin: $localize`United States`,
|
||||
@ -284,7 +276,6 @@ export const products: Product[] = [
|
||||
founded: 2007,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'sharesight',
|
||||
name: 'Sharesight',
|
||||
origin: $localize`New Zealand`,
|
||||
@ -296,7 +287,6 @@ export const products: Product[] = [
|
||||
component: SimplePortfolioPageComponent,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'simple-portfolio',
|
||||
name: 'Simple Portfolio',
|
||||
origin: $localize`Czech Republic`,
|
||||
@ -308,18 +298,25 @@ export const products: Product[] = [
|
||||
founded: 2021,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'snowball-analytics',
|
||||
name: 'Snowball Analytics',
|
||||
origin: 'France',
|
||||
origin: $localize`France`,
|
||||
pricingPerYear: '$80',
|
||||
slogan: 'Simple and powerful portfolio tracker'
|
||||
},
|
||||
{
|
||||
component: StockMarketEyePageComponent,
|
||||
founded: 2008,
|
||||
key: 'stockmarketeye',
|
||||
name: 'StockMarketEye',
|
||||
origin: $localize`France`,
|
||||
note: 'Sunset in 2023',
|
||||
slogan: 'A Powerful Portfolio & Investment Tracking App'
|
||||
},
|
||||
{
|
||||
component: SumioPageComponent,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'sumio',
|
||||
name: 'Sumio',
|
||||
origin: $localize`Czech Republic`,
|
||||
@ -330,7 +327,6 @@ export const products: Product[] = [
|
||||
component: UtlunaPageComponent,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'utluna',
|
||||
languages: ['Deutsch', 'English', 'Français'],
|
||||
name: 'Utluna',
|
||||
@ -343,7 +339,6 @@ export const products: Product[] = [
|
||||
component: YeekateePageComponent,
|
||||
founded: 2021,
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'yeekatee',
|
||||
name: 'yeekatee',
|
||||
origin: $localize`Switzerland`,
|
||||
|
@ -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-capmon-page',
|
||||
standalone: true,
|
||||
styleUrls: ['../product-page-template.scss'],
|
||||
templateUrl: '../product-page-template.html'
|
||||
})
|
||||
export class CapMonPageComponent {
|
||||
public product1 = products.find(({ key }) => {
|
||||
return key === 'ghostfolio';
|
||||
});
|
||||
|
||||
public product2 = products.find(({ key }) => {
|
||||
return key === 'capmon';
|
||||
});
|
||||
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public routerLinkResourcesPersonalFinanceTools = [
|
||||
'/' + $localize`resources`,
|
||||
'personal-finance-tools'
|
||||
];
|
||||
}
|
@ -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-stockmarketeye-page',
|
||||
standalone: true,
|
||||
styleUrls: ['../product-page-template.scss'],
|
||||
templateUrl: '../product-page-template.html'
|
||||
})
|
||||
export class StockMarketEyePageComponent {
|
||||
public product1 = products.find(({ key }) => {
|
||||
return key === 'ghostfolio';
|
||||
});
|
||||
|
||||
public product2 = products.find(({ key }) => {
|
||||
return key === 'stockmarketeye';
|
||||
});
|
||||
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public routerLinkResourcesPersonalFinanceTools = [
|
||||
'/' + $localize`resources`,
|
||||
'personal-finance-tools'
|
||||
];
|
||||
}
|
@ -66,7 +66,8 @@ export class UserAccountPageComponent implements OnDestroy, OnInit {
|
||||
'fr',
|
||||
'it',
|
||||
'nl',
|
||||
'pt'
|
||||
'pt',
|
||||
'tr'
|
||||
];
|
||||
public price: number;
|
||||
public priceId: string;
|
||||
|
@ -160,6 +160,10 @@
|
||||
>Português (<ng-container i18n>Community</ng-container
|
||||
>)</mat-option
|
||||
>
|
||||
<mat-option value="tr"
|
||||
>Türkçe (<ng-container i18n>Community</ng-container
|
||||
>)</mat-option
|
||||
>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
@ -4,6 +4,8 @@ import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-pr
|
||||
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
|
||||
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
|
||||
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
|
||||
import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto';
|
||||
import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto';
|
||||
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
@ -15,7 +17,7 @@ import {
|
||||
Filter,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { DataSource, MarketData, Platform, Prisma } from '@prisma/client';
|
||||
import { DataSource, MarketData, Platform, Prisma, Tag } from '@prisma/client';
|
||||
import { JobStatus } from 'bull';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { Observable, map } from 'rxjs';
|
||||
@ -64,6 +66,10 @@ export class AdminService {
|
||||
);
|
||||
}
|
||||
|
||||
public deleteTag(aId: string) {
|
||||
return this.http.delete<void>(`/api/v1/tag/${aId}`);
|
||||
}
|
||||
|
||||
public fetchAdminData() {
|
||||
return this.http.get<AdminData>('/api/v1/admin');
|
||||
}
|
||||
@ -139,6 +145,10 @@ export class AdminService {
|
||||
return this.http.get<Platform[]>('/api/v1/platform');
|
||||
}
|
||||
|
||||
public fetchTags() {
|
||||
return this.http.get<Tag[]>('/api/v1/tag');
|
||||
}
|
||||
|
||||
public gather7Days() {
|
||||
return this.http.post<void>('/api/v1/admin/gather', {});
|
||||
}
|
||||
@ -208,6 +218,10 @@ export class AdminService {
|
||||
return this.http.post<Platform>(`/api/v1/platform`, aPlatform);
|
||||
}
|
||||
|
||||
public postTag(aTag: CreateTagDto) {
|
||||
return this.http.post<Tag>(`/api/v1/tag`, aTag);
|
||||
}
|
||||
|
||||
public putMarketData({
|
||||
dataSource,
|
||||
date,
|
||||
@ -233,4 +247,8 @@ export class AdminService {
|
||||
aPlatform
|
||||
);
|
||||
}
|
||||
|
||||
public putTag(aTag: UpdateTagDto) {
|
||||
return this.http.put<Tag>(`/api/v1/tag/${aTag.id}`, aTag);
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ import { Injectable } from '@angular/core';
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { parseDate as parseDateHelper } from '@ghostfolio/common/helper';
|
||||
import { Account, DataSource, Type } from '@prisma/client';
|
||||
import { isMatch, parse, parseISO } from 'date-fns';
|
||||
import { isFinite } from 'lodash';
|
||||
import { parse as csvToJson } from 'papaparse';
|
||||
import { EMPTY } from 'rxjs';
|
||||
@ -219,31 +219,12 @@ export class ImportActivitiesService {
|
||||
item: any;
|
||||
}) {
|
||||
item = this.lowercaseKeys(item);
|
||||
let date: string;
|
||||
|
||||
for (const key of ImportActivitiesService.DATE_KEYS) {
|
||||
if (item[key]) {
|
||||
if (isMatch(item[key], 'dd-MM-yyyy') && item[key].length === 10) {
|
||||
// Check length to only match yyyy (and not yy)
|
||||
date = parse(item[key], 'dd-MM-yyyy', new Date()).toISOString();
|
||||
} else if (
|
||||
isMatch(item[key], 'dd/MM/yyyy') &&
|
||||
item[key].length === 10
|
||||
) {
|
||||
// Check length to only match yyyy (and not yy)
|
||||
date = parse(item[key], 'dd/MM/yyyy', new Date()).toISOString();
|
||||
} else if (isMatch(item[key], 'yyyyMMdd') && item[key].length === 8) {
|
||||
// Check length to only match yyyy (and not yy)
|
||||
date = parse(item[key], 'yyyyMMdd', new Date()).toISOString();
|
||||
} else {
|
||||
try {
|
||||
date = parseISO(item[key]).toISOString();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (date) {
|
||||
return date;
|
||||
}
|
||||
try {
|
||||
return parseDateHelper(item[key].toString()).toISOString();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
|
BIN
apps/client/src/assets/images/blog/hacktoberfest-2023.png
Normal file
BIN
apps/client/src/assets/images/blog/hacktoberfest-2023.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
@ -1,5 +1,5 @@
|
||||
{
|
||||
"createdAt": "2023-09-18T13:16:22.059Z",
|
||||
"createdAt": "2023-09-25T08:15:38.055Z",
|
||||
"data": [
|
||||
{
|
||||
"name": "Appsmith",
|
||||
@ -81,6 +81,11 @@
|
||||
"description": "Open-source monitoring platform with beautiful status pages",
|
||||
"href": "https://www.openstatus.dev"
|
||||
},
|
||||
{
|
||||
"name": "Papermark",
|
||||
"description": "Open-Source Docsend Alternative to securely share documents with real-time analytics.",
|
||||
"href": "https://www.papermark.io/"
|
||||
},
|
||||
{
|
||||
"name": "Requestly",
|
||||
"description": "Makes frontend development cycle 10x faster with API Client, Mock Server, Intercept & Modify HTTP Requests and Session Replays.",
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
9929
apps/client/src/locales/messages.tr.xlf
Normal file
9929
apps/client/src/locales/messages.tr.xlf
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -101,7 +101,8 @@ export const SUPPORTED_LANGUAGE_CODES = [
|
||||
'fr',
|
||||
'it',
|
||||
'nl',
|
||||
'pt'
|
||||
'pt',
|
||||
'tr'
|
||||
];
|
||||
|
||||
export const UNKNOWN_KEY = 'UNKNOWN';
|
||||
|
@ -1,8 +1,16 @@
|
||||
import * as currencies from '@dinero.js/currencies';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
|
||||
import { de, es, fr, it, nl, pt } from 'date-fns/locale';
|
||||
import {
|
||||
getDate,
|
||||
getMonth,
|
||||
getYear,
|
||||
isMatch,
|
||||
parse,
|
||||
parseISO,
|
||||
subDays
|
||||
} from 'date-fns';
|
||||
import { de, es, fr, it, nl, pt, tr } from 'date-fns/locale';
|
||||
|
||||
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
|
||||
import { Benchmark, UniqueAsset } from './interfaces';
|
||||
@ -96,6 +104,8 @@ export function getDateFnsLocale(aLanguageCode: string) {
|
||||
return nl;
|
||||
} else if (aLanguageCode === 'pt') {
|
||||
return pt;
|
||||
} else if (aLanguageCode === 'tr') {
|
||||
return tr;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@ -282,8 +292,34 @@ export const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
export const DATE_FORMAT_MONTHLY = 'MMMM yyyy';
|
||||
export const DATE_FORMAT_YEARLY = 'yyyy';
|
||||
|
||||
export function parseDate(date: string) {
|
||||
return parse(date, DATE_FORMAT, new Date());
|
||||
export function parseDate(date: string): Date | null {
|
||||
// Transform 'yyyyMMdd' format to supported format by parse function
|
||||
if (date?.length === 8) {
|
||||
const match = date.match(/^(\d{4})(\d{2})(\d{2})$/);
|
||||
|
||||
if (match) {
|
||||
const [, year, month, day] = match;
|
||||
date = `${year}-${month}-${day}`;
|
||||
}
|
||||
}
|
||||
|
||||
const dateFormat = [
|
||||
'dd-MM-yyyy',
|
||||
'dd/MM/yyyy',
|
||||
'dd.MM.yyyy',
|
||||
'yyyy-MM-dd',
|
||||
'yyyy/MM/dd',
|
||||
'yyyy.MM.dd',
|
||||
'yyyyMMdd'
|
||||
].find((format) => {
|
||||
return isMatch(date, format) && format.length === date.length;
|
||||
});
|
||||
|
||||
if (dateFormat) {
|
||||
return parse(date, dateFormat, new Date());
|
||||
}
|
||||
|
||||
return parseISO(date);
|
||||
}
|
||||
|
||||
export function prettifySymbol(aSymbol: string): string {
|
||||
|
@ -3,7 +3,7 @@ export interface Product {
|
||||
founded?: number;
|
||||
hasFreePlan?: boolean;
|
||||
hasSelfHostingAbility?: boolean;
|
||||
isOpenSource: boolean;
|
||||
isOpenSource?: boolean;
|
||||
key: string;
|
||||
languages?: string[];
|
||||
name: string;
|
||||
|
@ -7,12 +7,14 @@ export const permissions = {
|
||||
createAccount: 'createAccount',
|
||||
createOrder: 'createOrder',
|
||||
createPlatform: 'createPlatform',
|
||||
createTag: 'createTag',
|
||||
createUserAccount: 'createUserAccount',
|
||||
deleteAccess: 'deleteAccess',
|
||||
deleteAccount: 'deleteAcccount',
|
||||
deleteAuthDevice: 'deleteAuthDevice',
|
||||
deleteOrder: 'deleteOrder',
|
||||
deletePlatform: 'deletePlatform',
|
||||
deleteTag: 'deleteTag',
|
||||
deleteUser: 'deleteUser',
|
||||
enableFearAndGreedIndex: 'enableFearAndGreedIndex',
|
||||
enableImport: 'enableImport',
|
||||
@ -29,6 +31,7 @@ export const permissions = {
|
||||
updateAuthDevice: 'updateAuthDevice',
|
||||
updateOrder: 'updateOrder',
|
||||
updatePlatform: 'updatePlatform',
|
||||
updateTag: 'updateTag',
|
||||
updateUserSettings: 'updateUserSettings',
|
||||
updateViewMode: 'updateViewMode'
|
||||
};
|
||||
@ -42,16 +45,19 @@ export function getPermissions(aRole: Role): string[] {
|
||||
permissions.createAccount,
|
||||
permissions.createOrder,
|
||||
permissions.createPlatform,
|
||||
permissions.createTag,
|
||||
permissions.deleteAccess,
|
||||
permissions.deleteAccount,
|
||||
permissions.deleteAuthDevice,
|
||||
permissions.deleteOrder,
|
||||
permissions.deletePlatform,
|
||||
permissions.deleteTag,
|
||||
permissions.deleteUser,
|
||||
permissions.updateAccount,
|
||||
permissions.updateAuthDevice,
|
||||
permissions.updateOrder,
|
||||
permissions.updatePlatform,
|
||||
permissions.updateTag,
|
||||
permissions.updateUserSettings,
|
||||
permissions.updateViewMode
|
||||
];
|
||||
|
@ -156,44 +156,7 @@
|
||||
<ng-container i18n>Type</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
<div
|
||||
class="d-inline-flex p-1 type-badge"
|
||||
[ngClass]="{
|
||||
buy: element.type === 'BUY',
|
||||
dividend: element.type === 'DIVIDEND',
|
||||
fee: element.type === 'FEE',
|
||||
interest: element.type === 'INTEREST',
|
||||
item: element.type === 'ITEM',
|
||||
liability: element.type === 'LIABILITY',
|
||||
sell: element.type === 'SELL'
|
||||
}"
|
||||
>
|
||||
<ion-icon
|
||||
*ngIf="element.type === 'BUY'"
|
||||
name="arrow-up-circle-outline"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="element.type === 'DIVIDEND' || element.type === 'INTEREST'"
|
||||
name="add-circle-outline"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="element.type === 'FEE'"
|
||||
name="hammer-outline"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="element.type === 'ITEM'"
|
||||
name="cube-outline"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="element.type === 'LIABILITY'"
|
||||
name="flame-outline"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="element.type === 'SELL'"
|
||||
name="arrow-down-circle-outline"
|
||||
></ion-icon>
|
||||
<span class="d-none d-lg-block mx-1">{{ element.type }}</span>
|
||||
</div>
|
||||
<gf-activity-type [activityType]="element.type"></gf-activity-type>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
|
||||
</ng-container>
|
||||
|
@ -14,57 +14,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-row {
|
||||
.type-badge {
|
||||
background-color: rgba(var(--palette-foreground-text), 0.05);
|
||||
border-radius: 1rem;
|
||||
line-height: 1em;
|
||||
|
||||
ion-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&.buy {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
&.dividend {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
&.fee {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
&.interest {
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
&.item {
|
||||
color: var(--purple);
|
||||
}
|
||||
|
||||
&.liability {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
&.sell {
|
||||
color: var(--orange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
.mat-mdc-table {
|
||||
.type-badge {
|
||||
background-color: rgba(
|
||||
var(--palette-foreground-text-dark),
|
||||
0.1
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import { RouterModule } from '@angular/router';
|
||||
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
|
||||
import { GfActivityTypeModule } from '@ghostfolio/ui/activity-type';
|
||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
@ -23,6 +24,7 @@ import { ActivitiesTableComponent } from './activities-table.component';
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfActivitiesFilterModule,
|
||||
GfActivityTypeModule,
|
||||
GfNoTransactionsInfoModule,
|
||||
GfSymbolIconModule,
|
||||
GfSymbolModule,
|
||||
|
32
libs/ui/src/lib/activity-type/activity-type.component.html
Normal file
32
libs/ui/src/lib/activity-type/activity-type.component.html
Normal file
@ -0,0 +1,32 @@
|
||||
<div
|
||||
class="d-inline-flex p-1 activity-type-badge"
|
||||
[ngClass]="{
|
||||
buy: activityType === 'BUY',
|
||||
dividend: activityType === 'DIVIDEND',
|
||||
fee: activityType === 'FEE',
|
||||
interest: activityType === 'INTEREST',
|
||||
item: activityType === 'ITEM',
|
||||
liability: activityType === 'LIABILITY',
|
||||
sell: activityType === 'SELL'
|
||||
}"
|
||||
>
|
||||
<ion-icon
|
||||
*ngIf="activityType === 'BUY'"
|
||||
name="arrow-up-circle-outline"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="activityType === 'DIVIDEND' || activityType === 'INTEREST'"
|
||||
name="add-circle-outline"
|
||||
></ion-icon>
|
||||
<ion-icon *ngIf="activityType === 'FEE'" name="hammer-outline"></ion-icon>
|
||||
<ion-icon *ngIf="activityType === 'ITEM'" name="cube-outline"></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="activityType === 'LIABILITY'"
|
||||
name="flame-outline"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="activityType === 'SELL'"
|
||||
name="arrow-down-circle-outline"
|
||||
></ion-icon>
|
||||
<span class="d-none d-lg-block mx-1">{{ activityTypeLabel }}</span>
|
||||
</div>
|
47
libs/ui/src/lib/activity-type/activity-type.component.scss
Normal file
47
libs/ui/src/lib/activity-type/activity-type.component.scss
Normal file
@ -0,0 +1,47 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.activity-type-badge {
|
||||
background-color: rgba(var(--palette-foreground-text), 0.05);
|
||||
border-radius: 1rem;
|
||||
line-height: 1em;
|
||||
|
||||
ion-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&.buy {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
&.dividend {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
&.fee {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
&.interest {
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
&.item {
|
||||
color: var(--purple);
|
||||
}
|
||||
|
||||
&.liability {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
&.sell {
|
||||
color: var(--orange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
.activity-type-badge {
|
||||
background-color: rgba(var(--palette-foreground-text-dark), 0.1) !important;
|
||||
}
|
||||
}
|
26
libs/ui/src/lib/activity-type/activity-type.component.ts
Normal file
26
libs/ui/src/lib/activity-type/activity-type.component.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges
|
||||
} from '@angular/core';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
import { Type as ActivityType } from '@prisma/client';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'gf-activity-type',
|
||||
styleUrls: ['./activity-type.component.scss'],
|
||||
templateUrl: './activity-type.component.html'
|
||||
})
|
||||
export class ActivityTypeComponent implements OnChanges {
|
||||
@Input() activityType: ActivityType;
|
||||
|
||||
public activityTypeLabel: string;
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public ngOnChanges() {
|
||||
this.activityTypeLabel = translate(this.activityType);
|
||||
}
|
||||
}
|
12
libs/ui/src/lib/activity-type/activity-type.module.ts
Normal file
12
libs/ui/src/lib/activity-type/activity-type.module.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
|
||||
import { ActivityTypeComponent } from './activity-type.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ActivityTypeComponent],
|
||||
exports: [ActivityTypeComponent],
|
||||
imports: [CommonModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfActivityTypeModule {}
|
1
libs/ui/src/lib/activity-type/index.ts
Normal file
1
libs/ui/src/lib/activity-type/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './activity-type.module';
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "2.4.0",
|
||||
"version": "2.6.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": "https://github.com/ghostfolio/ghostfolio",
|
||||
@ -129,7 +129,7 @@
|
||||
"svgmap": "2.6.0",
|
||||
"twitter-api-v2": "1.14.2",
|
||||
"uuid": "9.0.0",
|
||||
"yahoo-finance2": "2.5.0",
|
||||
"yahoo-finance2": "2.7.0",
|
||||
"zone.js": "0.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -189,7 +189,7 @@
|
||||
"jest-preset-angular": "13.1.1",
|
||||
"nx": "16.7.4",
|
||||
"nx-cloud": "16.3.0",
|
||||
"prettier": "3.0.2",
|
||||
"prettier": "3.0.3",
|
||||
"prettier-plugin-organize-attributes": "1.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
|
@ -1,5 +1,5 @@
|
||||
Date,Code,Currency,Price,Quantity,Action,Fee
|
||||
16-09-2021,MSFT,USD,298.580,5,buy,19.00
|
||||
17/11/2021,MSFT,USD,0.62,5,dividend,0.00
|
||||
16/09/2021,MSFT,USD,298.580,5,buy,19.00
|
||||
01/01/2022,Penthouse Apartment,USD,500000.0,1,item,0.00
|
||||
06/06/2050,MSFT,USD,0.00,0,buy,0.00
|
||||
01.01.2022,Penthouse Apartment,USD,500000.0,1,item,0.00
|
||||
20500606,MSFT,USD,0.00,0,buy,0.00
|
||||
|
|
24
yarn.lock
24
yarn.lock
@ -15881,10 +15881,10 @@ prettier-plugin-organize-attributes@1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-attributes/-/prettier-plugin-organize-attributes-1.0.0.tgz#037870ee3111b3c1d6371f677b64888de353cc63"
|
||||
integrity sha512-+NmameaLxbCcylEXsKPmawtzla5EE6ECqvGkpfQz4KM847fXDifB1gFnPQEpoADAq6IXg+cMI8Z0ISJEXa6fhg==
|
||||
|
||||
prettier@3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.2.tgz#78fcecd6d870551aa5547437cdae39d4701dca5b"
|
||||
integrity sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==
|
||||
prettier@3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643"
|
||||
integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==
|
||||
|
||||
prettier@^2.8.0:
|
||||
version "2.8.8"
|
||||
@ -17889,6 +17889,13 @@ toidentifier@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||
|
||||
tough-cookie-file-store@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie-file-store/-/tough-cookie-file-store-2.0.3.tgz#788f7a6fe5cd8f61a1afb71b2f0b964ebf914b80"
|
||||
integrity sha512-sMpZVcmFf6EYFHFFl+SYH4W1/OnXBYMGDsv2IlbQ2caHyFElW/UR/gpj/KYU1JwmP4dE9xqwv2+vWcmlXHojSw==
|
||||
dependencies:
|
||||
tough-cookie "^4.0.0"
|
||||
|
||||
tough-cookie@^4.0.0, tough-cookie@^4.1.2:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf"
|
||||
@ -19035,16 +19042,17 @@ y18n@^5.0.5:
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
||||
|
||||
yahoo-finance2@2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.5.0.tgz#847834b33d24dc8ce96357401aba7dae1bcfda9f"
|
||||
integrity sha512-YTniHzTx17lrs7tUonFZMWvY0dF4UJSrPkrTNMqNrb+la7Nde/5KY7mQFf+8VdXhngPup2V9ex27M2WK3ADtbw==
|
||||
yahoo-finance2@2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.7.0.tgz#2f3d078409616495a6f9357b1cce1c797903b82c"
|
||||
integrity sha512-kh/t3P72MDLyf1jCEPTVZXtIwW8tuxhEg2zWHhPINxnBCT6cToQjF59dWnHNosPji5iUmtvhMWLmMbWJkBBSSw==
|
||||
dependencies:
|
||||
"@types/tough-cookie" "^4.0.2"
|
||||
ajv "8.10.0"
|
||||
ajv-formats "2.1.1"
|
||||
node-fetch "^2.6.1"
|
||||
tough-cookie "^4.1.2"
|
||||
tough-cookie-file-store "^2.0.3"
|
||||
|
||||
yallist@^3.0.2:
|
||||
version "3.1.1"
|
||||
|
Reference in New Issue
Block a user